From c8ccbbe88e588050ea06e61cc5d75549653f2c73 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 12 Nov 2025 12:52:21 +0100 Subject: [PATCH 1/2] [msbuild/dotnet] Add support for listing the devices and simulators available to run on. This consists of two parts: * Add an MSBuild target that lists all the available devices (`ComputeAvailableDevices`), by returning them in a `$(Devices)` item group. * Add support for the `$(Device)` property to specify the device or simulator to use. Regarding the device list, we'll filter the returned list by: * Platform (don't return an Apple TV simulator for an iOS app). * Minimum OS version (not return an iOS 18.0 device when the app's minimum OS version is 26.0). * Only devices that are actually available, as reported by `devicectl` or `simctl`. References: * https://github.com/dotnet/android/pull/10576 * https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md Fixes https://github.com/dotnet/macios/issues/23995. --- docs/building-apps/build-properties.md | 14 + docs/building-apps/build-targets.md | 31 +- dotnet/targets/Xamarin.Shared.Sdk.targets | 32 +- .../Tasks/GetAvailableDevices.cs | 409 ++++++++ .../Xamarin.MacDev.Tasks.csproj | 3 + msbuild/Xamarin.Shared/Xamarin.Shared.targets | 4 +- tests/common/shared-dotnet.mk | 2 +- .../TaskTests/GetAvailableDevicesTest.cs | 905 ++++++++++++++++++ tools/common/JsonExtensions.cs | 107 +++ 9 files changed, 1501 insertions(+), 6 deletions(-) create mode 100644 msbuild/Xamarin.MacDev.Tasks/Tasks/GetAvailableDevices.cs create mode 100644 tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs create mode 100644 tools/common/JsonExtensions.cs diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 6aa958278b11..c577481ec68e 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -344,6 +344,20 @@ Only applicable to macOS and Mac Catalyst projects. See [BuildIpa](#buildipa) for iOS and tvOS projects. +## Device + +Specifies which mobile device or emulator to target when using `dotnet run +--device ` or MSBuild targets that interact with devices (such as +`Run`, `Install`, or `Uninstall`). + +The value can be anything the command-line tools `simctl` or `devicectl` +accept for the device name; this is typically either the UDID or the name of +the device. For example, for the device `My iOS Device` with UDID `0000-aaaabbbb`, use +either `-p:Device="My iOS Device"` or `-p:Device=0000-aaaabbbb`. + +For more information about device selection, see the +[.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md). + ## DeviceSpecificBuild If the build should be specific to the selected device. diff --git a/docs/building-apps/build-targets.md b/docs/building-apps/build-targets.md index fb548e6691ce..202695ab341f 100644 --- a/docs/building-apps/build-targets.md +++ b/docs/building-apps/build-targets.md @@ -16,11 +16,38 @@ Builds the source code within a project and all dependencies. Removes all files generated by the build process. +## ComputeAvailableDevices + +Queries and returns a list of available iOS or tvOS devices and simulators that can be used with `dotnet run`. + +This target is called automatically by the .NET SDK's `dotnet run` command to +support device selection via the `--device` option. It returns a `@(Devices)`` +item group where each device has the following metadata: + +- **Description**: The name of the device (e.g., "iPhone 16 - iOS 26.0" for simulators, "My iPhone 16" for physical devices) +- **Type**: Either "Device" or "Simulator" +- **OSVersion**: The OS version if the device +- **UDID**: The UDID of the device + +For example, to list all available devices: + +```shell +$ dotnet build -t:ComputeAvailableDevices +``` + +This target is part of the [.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md) and enables commands like: + +```shell +$ dotnet run --device UDID +``` + +Added in .NET 11. + ## Run Builds the source code within a project and all dependencies, and then deploys and runs it -on a default simulator/device. A specific deployment target can be set by using the `$(_DeviceName)` property. +on a default simulator/device. A specific deployment target can be set by using the `$(Device)` property. ```dotnetcli -dotnet build -t:Run project.csproj -p:_DeviceName=$(MY_DEVICE_UDID) +dotnet build -t:Run project.csproj -p:Device=$(MY_DEVICE_UDID) ``` diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index c545cadcee69..b88f6b4ff475 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -2407,7 +2407,8 @@ $(XamarinRelativeSdkRootDirectory)tools\bin\mlaunch <_RelativeMlaunchPath Condition="'$(_RelativeMlaunchPath)' == ''">$(RelativeMlaunchPath) - $(_DeviceName) + + $(_DeviceName) + + + + + + + + <_CreateAppManifest>$(_CanOutputAppBundle) <_CreateAppManifest Condition="'$(IsAppDistribution)' == 'true'">false @@ -3234,6 +3234,8 @@ Copyright (C) 2018 Microsoft. All rights reserved. <_AppXpcServicesRelativePath Condition="'$(_PlatformName)' == 'macOS' Or '$(_PlatformName)' == 'MacCatalyst'">Contents\XPCServices\ <_AppXpcServicesRelativePath Condition="'$(_PlatformName)' == 'iOS' Or '$(_PlatformName)' == 'tvOS' Or '$(_PlatformName)' == 'watchOS'">XPCServices\ <_AppXpcServicesPath>$(_AppBundlePath)$(_AppXpcServicesRelativePath) + + <_AppBundleManifestPath>$(_AppBundlePath)$(_AppBundleManifestRelativePath)Info.plist diff --git a/tests/common/shared-dotnet.mk b/tests/common/shared-dotnet.mk index 13fadac854ff..140c04d4bf7e 100644 --- a/tests/common/shared-dotnet.mk +++ b/tests/common/shared-dotnet.mk @@ -178,7 +178,7 @@ run-bare: # Get the list of applicable simulators, and pick the first in the list. # Make sure to have a matching simulator runtime installed, otherwise this won't work. -run-old: RUN_ARGUMENTS=-p:_DeviceName=$(shell xcrun simctl list devices "$(PLATFORM) $(MIN_$(PLATFORM_UPPERCASE)_SIMULATOR_VERSION)" -j | jq -c '.[][][].udid' | head -1 | sed 's/"//g') +run-old: RUN_ARGUMENTS=-p:Device=$(shell xcrun simctl list devices "$(PLATFORM) $(MIN_$(PLATFORM_UPPERCASE)_SIMULATOR_VERSION)" -j | jq -c '.[][][].udid' | head -1 | sed 's/"//g') run-old: export RUNTIMEIDENTIFIER= run-old: $(MAKE) run diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs new file mode 100644 index 000000000000..0617d4758d23 --- /dev/null +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs @@ -0,0 +1,905 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using NUnit.Framework; + +using Xamarin.Tests; +using Xamarin.Utils; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + [TestFixture] + public class GetAvailableDevicesTests : TestBase { + class GetAvailableDevicesTaskWrapper : GetAvailableDevices { + public string SimCtlJson = string.Empty; + public string DeviceCtlJson = string.Empty; + protected override async System.Threading.Tasks.Task ExecuteCtlAsync (params string [] args) + { + switch (args [0]) { + case "simctl": + return SimCtlJson; + case "devicectl": + return DeviceCtlJson; + default: + throw new Exception (); + } + } + } + + GetAvailableDevicesTaskWrapper CreateTask (ApplePlatform platform, string simctlJson, string devicectlJson, string appManifest = "") + { + var task = new GetAvailableDevicesTaskWrapper () { + SimCtlJson = simctlJson, + DeviceCtlJson = devicectlJson, + }; + task.SdkDevPath = Configuration.xcode_root; + task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); + + if (!string.IsNullOrEmpty (appManifest)) { + var tmpdir = Cache.CreateTemporaryDirectory (); + var appManifestPath = Path.Combine (tmpdir, "Info.plist"); + File.WriteAllText (appManifestPath, appManifest); + task.AppBundleManifestPath = appManifestPath; + } + + return task; + } + + [Test] + [TestCase ("", "")] + [TestCase ("{}", "{}")] + [TestCase ("[]", "[]")] + [TestCase ("{ \"devicetypes\": {}, \"runtimes\": {}, \"devices\": {} }", "")] + [TestCase ("{ \"devicetypes\": {}, \"runtimes\": {} }", "")] + [TestCase ("{ \"devicetypes\": {}, \"devices\": {} }", "")] + [TestCase ("{ \"devices\": {} }", "")] + [TestCase ("", "{\"result\" : {} }")] + [TestCase ("", "{\"result\" : { \"devices\": {} } }")] + public void EmptyJsons (string simctl, string devicectl) + { + var platform = ApplePlatform.iOS; + var task = CreateTask (platform, simctl, devicectl); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.That (task.Devices.Count, Is.EqualTo (0), "Devices should be empty."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (0), "No devices should have been discarded."); + } + + [Test] + public void DeviceCtl1 () + { + var platform = ApplePlatform.iOS; + var task = CreateTask (platform, "", DEVICECTL_JSON_1); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (3), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (1), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Device 1 Name mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 Platform mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Device 2 mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Device 2 Platform mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [2].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Device 3 mismatch."); + Assert.That (task.Devices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Device 3 Name mismatch."); + Assert.That (task.Devices [2].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Device 3 Platform mismatch."); + Assert.That (task.Devices [2].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 1 Description mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 1 reason mismatch."); + }); + } + + [Test] + public void SimCtl1 () + { + var platform = ApplePlatform.iOS; + var task = CreateTask (platform, SIMCTL_JSON_1, ""); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (2), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (3), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 1 mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Device 1 Name mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 2 mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 2 OSVersion mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 1 mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Discarded Device 1 Name mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'tvOS' does not match the requested platform 'iOS'"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 2 mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 2 Name mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Device 3 mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Device 3 Name mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Device 3 discarded reason mismatch."); + }); + } + [Test] + public void Ctl1 () + { + var platform = ApplePlatform.iOS; + var task = CreateTask (platform, SIMCTL_JSON_1, DEVICECTL_JSON_1); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (5), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (4), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Device 1 Name mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 2 OSVersion mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [2].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Device 3 Name mismatch."); + Assert.That (task.Devices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Device 3 OSVersion mismatch."); + Assert.That (task.Devices [2].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("DiscardedReason"), Is.Empty, "Device 3 discarded reason mismatch."); + + Assert.That (task.Devices [3].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Device 4 UDID mismatch."); + Assert.That (task.Devices [3].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Device 4 Name mismatch."); + Assert.That (task.Devices [3].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Device 4 OSVersion mismatch."); + Assert.That (task.Devices [3].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Device 4 UDID mismatch."); + Assert.That (task.Devices [3].GetMetadata ("DiscardedReason"), Is.Empty, "Device 4 discarded reason mismatch."); + + Assert.That (task.Devices [4].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 5 UDID mismatch."); + Assert.That (task.Devices [4].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Device 5 Name mismatch."); + Assert.That (task.Devices [4].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 5 OSVersion mismatch."); + Assert.That (task.Devices [4].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 5 UDID mismatch."); + Assert.That (task.Devices [4].GetMetadata ("DiscardedReason"), Is.Empty, "Device 5 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 1 Description mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 2 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Discarded Device 2 Description mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'tvOS' does not match the requested platform 'iOS'"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 3 Name mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 3 reason mismatch."); + + Assert.That (task.DiscardedDevices [3].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Device 4 Name mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 4 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Device 4 discarded reason mismatch."); + }); + } + + [Test] + public void Ctl1_iPhone () + { + var platform = ApplePlatform.iOS; + var appManifestXml = + """ + + + + UIDeviceFamily + + 1 + + + + """; + var task = CreateTask (platform, SIMCTL_JSON_1, DEVICECTL_JSON_1, appManifestXml); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (5), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (4), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Device 1 Name mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 2 OSVersion mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [2].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Device 3 Name mismatch."); + Assert.That (task.Devices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Device 3 OSVersion mismatch."); + Assert.That (task.Devices [2].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [3].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Device 4 UDID mismatch."); + Assert.That (task.Devices [3].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Device 4 Name mismatch."); + Assert.That (task.Devices [3].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Device 4 OSVersion mismatch."); + Assert.That (task.Devices [3].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Device 4 UDID mismatch."); + Assert.That (task.Devices [3].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [4].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 5 UDID mismatch."); + Assert.That (task.Devices [4].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Device 5 Name mismatch."); + Assert.That (task.Devices [4].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 5 OSVersion mismatch."); + Assert.That (task.Devices [4].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Device 5 UDID mismatch."); + Assert.That (task.Devices [4].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 1 Description mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 2 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Discarded Device 2 Description mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'tvOS' does not match the requested platform 'iOS'"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 3 Name mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 3 reason mismatch."); + + Assert.That (task.DiscardedDevices [3].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Discarded Device 4 Name mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 4 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Discarded Device 4 discarded reason mismatch."); + }); + } + + [Test] + public void Ctl1_iPad () + { + var platform = ApplePlatform.iOS; + var appManifestXml = + """ + + + + UIMinimumOSVersion + 16.0 + UIDeviceFamily + + 2 + + + + """; + var task = CreateTask (platform, SIMCTL_JSON_1, DEVICECTL_JSON_1, appManifestXml); + + + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (2), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (7), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Device 1 Description mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Device 2 OSVersion mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 1 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Discarded Device 1 Description mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not an iPad, but the app only supports iPads"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Discarded Device 2 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Discarded Device 2 Description mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not an iPad, but the app only supports iPads"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 3 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 3 Description mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 3 reason mismatch."); + + Assert.That (task.DiscardedDevices [3].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 4 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Discarded Device 4 Description mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 4 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'tvOS' does not match the requested platform 'iOS'"), "Discarded Device 4 reason mismatch."); + + Assert.That (task.DiscardedDevices [4].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 5 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 5 Description mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 5 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 5 UDID mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 5 reason mismatch."); + + Assert.That (task.DiscardedDevices [5].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 6 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Discarded Device 6 Description mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 6 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 6 UDID mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not an iPad, but the app only supports iPads"), "Discarded Device 6 reason mismatch."); + + Assert.That (task.DiscardedDevices [6].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 7 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Discarded Device 7 Description mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 7 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 7 UDID mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Discarded Device 7 reason mismatch."); + }); + } + + [Test] + public void Ctl1_OSVersion () + { + var platform = ApplePlatform.iOS; + var task = CreateTask (platform, SIMCTL_JSON_1, DEVICECTL_JSON_1); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var appManifestPath = Path.Combine (tmpdir, "Info.plist"); + var appManifestXml = + """ + + + + MinimumOSVersion + 26.0 + + + """; + File.WriteAllText (appManifestPath, appManifestXml); + task.AppBundleManifestPath = appManifestPath; + + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (3), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (6), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Device 1 Name mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.Devices [1].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Device 2 Name mismatch."); + Assert.That (task.Devices [1].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 2 OSVersion mismatch."); + Assert.That (task.Devices [1].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Device 2 UDID mismatch."); + Assert.That (task.Devices [1].GetMetadata ("DiscardedReason"), Is.Empty, "Device 3 discarded reason mismatch."); + + Assert.That (task.Devices [2].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Device 3 Name mismatch."); + Assert.That (task.Devices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Device 3 OSVersion mismatch."); + Assert.That (task.Devices [2].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Device 3 UDID mismatch."); + Assert.That (task.Devices [2].GetMetadata ("DiscardedReason"), Is.Empty, "Device 3 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Discarded Device 1 Name mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device OS version '18.7.1' is lower than the app's minimum OS version '26.0'"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 2 Name mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Discarded Device 3 Name mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'tvOS' does not match the requested platform 'iOS'"), "Discarded Device 3 reason mismatch."); + + Assert.That (task.DiscardedDevices [3].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 4 Name mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 4 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 4 reason mismatch."); + + Assert.That (task.DiscardedDevices [4].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 5 UDID mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Discarded Device 5 Name mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 5 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 5 UDID mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device OS version '13.0.0' is lower than the app's minimum OS version '26.0'"), "Discarded Device 5 reason mismatch."); + + Assert.That (task.DiscardedDevices [5].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 6 UDID mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Discarded Device 6 Name mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 6 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 6 UDID mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Discarded Device 6 reason mismatch."); + }); + } + + [Test] + public void Ctl1_AppleTV () + { + var platform = ApplePlatform.TVOS; + var task = CreateTask (platform, SIMCTL_JSON_1, DEVICECTL_JSON_1); + Assert.IsTrue (task.Execute (), "Task should have succeeded."); + Assert.Multiple (() => { + Assert.That (task.Devices.Count, Is.EqualTo (1), "Devices count mismatch."); + Assert.That (task.DiscardedDevices.Count, Is.EqualTo (8), "Discarded device count mismatch."); + + Assert.That (task.Devices [0].ItemSpec, Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Device 1 ItemSpec mismatch."); + Assert.That (task.Devices [0].GetMetadata ("Description"), Is.EqualTo ("Apple TV - tvOS 26.1"), "Device 1 Description mismatch."); + Assert.That (task.Devices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Device 1 OSVersion mismatch."); + Assert.That (task.Devices [0].GetMetadata ("UDID"), Is.EqualTo ("60ED31BD-80CE-420A-B0CB-756D2CD38201"), "Device 1 UDID mismatch."); + Assert.That (task.Devices [0].GetMetadata ("DiscardedReason"), Is.Empty, "Device 1 discarded reason mismatch."); + + Assert.That (task.DiscardedDevices [0].ItemSpec, Is.EqualTo ("00008001-012301230123ABCD"), "Discarded Device 1 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPad Pro 3rd Gen"), "Discarded Device 1 Description mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("OSVersion"), Is.EqualTo ("26.0"), "Discarded Device 1 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("UDID"), Is.EqualTo ("00008001-012301230123ABCD"), "Discarded Device 1 UDID mismatch."); + Assert.That (task.DiscardedDevices [0].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'iOS' does not match the requested platform 'tvOS'"), "Discarded Device 1 reason mismatch."); + + Assert.That (task.DiscardedDevices [1].ItemSpec, Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 2 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 13"), "Discarded Device 2 Description mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("OSVersion"), Is.EqualTo ("18.7.1"), "Discarded Device 2 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("UDID"), Is.EqualTo ("00008002-012301230123ABCD"), "Discarded Device 2 UDID mismatch."); + Assert.That (task.DiscardedDevices [1].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'iOS' does not match the requested platform 'tvOS'"), "Discarded Device 2 reason mismatch."); + + Assert.That (task.DiscardedDevices [2].ItemSpec, Is.EqualTo ("00008003-012301230123ABCD"), "Discarded Device 3 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("Description"), Is.EqualTo ("Rolf's iPhone 15"), "Discarded Device 3 Description mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 3 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("UDID"), Is.EqualTo ("00008003-012301230123ABCD"), "Discarded Device 3 UDID mismatch."); + Assert.That (task.DiscardedDevices [2].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'iOS' does not match the requested platform 'tvOS'"), "Discarded Device 3 reason mismatch."); + + Assert.That (task.DiscardedDevices [3].ItemSpec, Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 4 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("Description"), Is.EqualTo ("Rolf’s Apple Watch Series 7"), "Discarded Device 4 Description mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("OSVersion"), Is.EqualTo ("11.5"), "Discarded Device 4 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("UDID"), Is.EqualTo ("00008004-012301230123ABCD"), "Discarded Device 4 UDID mismatch."); + Assert.That (task.DiscardedDevices [3].GetMetadata ("DiscardedReason"), Is.EqualTo ("'appleWatch' devices are not supported"), "Discarded Device 4 reason mismatch."); + + Assert.That (task.DiscardedDevices [4].ItemSpec, Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 5 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("Description"), Is.EqualTo ("iPhone 17 Pro"), "Discarded Device 5 Description mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("OSVersion"), Is.EqualTo (""), "Discarded Device 5 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("UDID"), Is.EqualTo ("D4D95709-144A-4CAA-8469-89566EC1C935"), "Discarded Device 5 UDID mismatch."); + Assert.That (task.DiscardedDevices [4].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device is not available: runtime profile not found using \"System\" match policy"), "Discarded Device 5 reason mismatch."); + + Assert.That (task.DiscardedDevices [5].ItemSpec, Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 6 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("Description"), Is.EqualTo ("iPhone 11 - iOS 26.1"), "Discarded Device 6 Description mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 6 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("UDID"), Is.EqualTo ("D40CE982-3E65-4756-8162-90EFE50AF7FA"), "Discarded Device 6 UDID mismatch."); + Assert.That (task.DiscardedDevices [5].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'iOS' does not match the requested platform 'tvOS'"), "Discarded Device 6 reason mismatch."); + + Assert.That (task.DiscardedDevices [6].ItemSpec, Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Discarded Device 7 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 13-inch (M5)"), "Discarded Device 7 Description mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 7 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("UDID"), Is.EqualTo ("3F1C114D-FC3D-481A-9CA1-499EE1339390"), "Discarded Device 7 UDID mismatch."); + Assert.That (task.DiscardedDevices [6].GetMetadata ("DiscardedReason"), Is.EqualTo ("Device platform 'iOS' does not match the requested platform 'tvOS'"), "Discarded Device 7 reason mismatch."); + + Assert.That (task.DiscardedDevices [7].ItemSpec, Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 8 ItemSpec mismatch."); + Assert.That (task.DiscardedDevices [7].GetMetadata ("Description"), Is.EqualTo ("iPad Pro 11-inch (M5)"), "Discarded Device 8 Description mismatch."); + Assert.That (task.DiscardedDevices [7].GetMetadata ("OSVersion"), Is.EqualTo ("26.1"), "Discarded Device 8 OSVersion mismatch."); + Assert.That (task.DiscardedDevices [7].GetMetadata ("UDID"), Is.EqualTo ("F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4"), "Discarded Device 8 UDID mismatch."); + Assert.That (task.DiscardedDevices [7].GetMetadata ("DiscardedReason"), Is.EqualTo ("Unknown device type identifier 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB'"), "Discarded Device 8 reason mismatch."); + }); + } + + const string DEVICECTL_JSON_1 = + """ + { + "info" : { + "arguments" : [ + "devicectl", + "list", + "devices", + "--json-output", + "x.json" + ], + "commandType" : "devicectl.list.devices", + "environment" : { + "TERM" : "xterm-256color" + }, + "jsonVersion" : 2, + "outcome" : "success", + "version" : "477.39" + }, + "result" : { + "devices" : [ + { + "connectionProperties" : { + "tunnelState" : "unavailable" + }, + "deviceProperties" : { + "name" : "Rolf's iPad Pro 3rd Gen", + "osBuildUpdate" : "23A341", + "osVersionNumber" : "26.0", + "releaseType" : "Beta" + }, + "hardwareProperties" : { + "cpuType" : { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + "deviceType" : "iPad", + "ecid" : 18446744073709551615, + "hardwareModel" : "J317AP", + "internalStorageCapacity" : 64000000000, + "isProductionFused" : true, + "marketingName" : "iPad Pro (11-inch)", + "platform" : "iOS", + "productType" : "iPad8,1", + "reality" : "physical", + "serialNumber" : "SERIAL_1", + "supportedCPUTypes" : [ + { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + { + "name" : "arm64", + "subType" : 0, + "type" : 16777228 + } + ], + "supportedDeviceFamilies" : [ + 1, + 2 + ], + "thinningProductType" : "iPad8,1", + "udid" : "00008001-012301230123ABCD" + }, + "identifier" : "11111111-AAAA-BBBB-CCCC-DDDDDDDDDDDD", + "tags" : [ + + ], + "visibilityClass" : "default" + }, + { + "connectionProperties" : { + "pairingState" : "paired", + }, + "deviceProperties" : { + "bootState" : "booted", + "name" : "Rolf's iPhone 13", + "osBuildUpdate" : "22H31", + "osVersionNumber" : "18.7.1" + }, + "hardwareProperties" : { + "cpuType" : { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + "deviceType" : "iPhone", + "ecid" : 0, + "hardwareModel" : "D63AP", + "internalStorageCapacity" : 128000000000, + "isProductionFused" : true, + "marketingName" : "iPhone 13 Pro", + "platform" : "iOS", + "productType" : "iPhone14,2", + "reality" : "physical", + "serialNumber" : "SERIAL_2", + "supportedCPUTypes" : [ + { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + { + "name" : "arm64", + "subType" : 0, + "type" : 16777228 + }, + { + "name" : "armv8", + "subType" : 13, + "type" : 12 + } + ], + "supportedDeviceFamilies" : [ + 1 + ], + "thinningProductType" : "iPhone14,2", + "udid" : "00008002-012301230123ABCD" + }, + "identifier" : "22222222-AAAA-BBBB-CCCC-DDDDDDDDDDDD", + "tags" : [ + + ], + "visibilityClass" : "default" + }, + { + "connectionProperties" : { + "pairingState" : "paired", + "tunnelState" : "unavailable" + }, + "deviceProperties" : { + "name" : "Rolf's iPhone 15", + "osBuildUpdate" : "23B85", + "osVersionNumber" : "26.1" + }, + "hardwareProperties" : { + "cpuType" : { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + "deviceType" : "iPhone", + "ecid" : 1, + "hardwareModel" : "D83AP", + "internalStorageCapacity" : 128000000000, + "isProductionFused" : true, + "marketingName" : "iPhone 15 Pro", + "platform" : "iOS", + "productType" : "iPhone16,1", + "reality" : "physical", + "serialNumber" : "SERIAL_3", + "supportedCPUTypes" : [ + { + "name" : "arm64e", + "subType" : 2, + "type" : 16777228 + }, + { + "name" : "arm64", + "subType" : 0, + "type" : 16777228 + } + ], + "supportedDeviceFamilies" : [ + 1 + ], + "thinningProductType" : "iPhone16,1", + "udid" : "00008003-012301230123ABCD" + }, + "identifier" : "33333333-AAAA-BBBB-CCCC-DDDDDDDDDDDD", + "tags" : [ + + ], + "visibilityClass" : "default" + }, + { + "connectionProperties" : { + "pairingState" : "paired", + "transportType" : "localNetwork", + "tunnelState" : "disconnected", + "tunnelTransportProtocol" : "tcp" + }, + "deviceProperties" : { + "name" : "Rolf’s Apple Watch Series 7", + "osBuildUpdate" : "22T572", + "osVersionNumber" : "11.5", + }, + "hardwareProperties" : { + "cpuType" : { + "name" : "arm64_32", + "subType" : 1, + "type" : 33554444 + }, + "deviceType" : "appleWatch", + "ecid" : 2, + "hardwareModel" : "N187sAP", + "isProductionFused" : true, + "marketingName" : "Apple Watch Series 7", + "platform" : "watchOS", + "productType" : "Watch6,6", + "reality" : "physical", + "serialNumber" : "SERIAL_4", + "thinningProductType" : "Watch6,6", + "udid" : "00008004-012301230123ABCD" + }, + "identifier" : "44444444-AAAA-BBBB-CCCC-DDDDDDDDDDDD", + "tags" : [ + + ], + "visibilityClass" : "default" + } + ] + } + } + """; + + const string SIMCTL_JSON_1 = + """ + { + "devicetypes" : [ + { + "productFamily" : "iPhone", + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 11.simdevicetype", + "maxRuntimeVersion" : 4294967295, + "maxRuntimeVersionString" : "65535.255.255", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11", + "modelIdentifier" : "iPhone12,1", + "minRuntimeVersionString" : "13.0.0", + "minRuntimeVersion" : 851968, + "name" : "iPhone 11" + }, + { + "productFamily" : "iPad", + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPad Pro 13-inch (M5).simdevicetype", + "maxRuntimeVersion" : 4294967295, + "maxRuntimeVersionString" : "65535.255.255", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-12GB", + "modelIdentifier" : "iPad17,4", + "minRuntimeVersionString" : "26.0.0", + "minRuntimeVersion" : 1703936, + "name" : "iPad Pro 13-inch (M5)" + }, + { + "productFamily" : "Apple TV", + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/Apple TV 4K (3rd generation).simdevicetype", + "maxRuntimeVersion" : 4294967295, + "maxRuntimeVersionString" : "65535.255.255", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K", + "modelIdentifier" : "AppleTV14,1", + "minRuntimeVersionString" : "16.1.0", + "minRuntimeVersion" : 1048832, + "name" : "Apple TV 4K (3rd generation)" + }, + ], + "runtimes" : [ + { + "isAvailable" : true, + "version" : "26.1", + "isInternal" : false, + "buildversion" : "23B80", + "supportedArchitectures" : [ + "arm64" + ], + "supportedDeviceTypes" : [ + { + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 17 Pro.simdevicetype", + "name" : "iPhone 17 Pro", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro", + "productFamily" : "iPhone" + } + ], + "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-26-1", + "platform" : "iOS", + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_23B80\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 26.1.simruntime", + "runtimeRoot" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_23B80\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 26.1.simruntime\/Contents\/Resources\/RuntimeRoot", + "lastUsage" : { + "arm64" : "2025-11-11T18:49:51Z" + }, + "name" : "iOS 26.1" + }, + { + "isAvailable" : true, + "version" : "26.1", + "isInternal" : false, + "buildversion" : "23J579", + "supportedArchitectures" : [ + "arm64" + ], + "supportedDeviceTypes" : [ + { + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/Apple TV 4K (3rd generation).simdevicetype", + "name" : "Apple TV 4K (3rd generation)", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K", + "productFamily" : "Apple TV" + }, + ], + "identifier" : "com.apple.CoreSimulator.SimRuntime.tvOS-26-1", + "platform" : "tvOS", + "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/tvOS_23J579\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/tvOS 26.1.simruntime", + "runtimeRoot" : "\/Library\/Developer\/CoreSimulator\/Volumes\/tvOS_23J579\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/tvOS 26.1.simruntime\/Contents\/Resources\/RuntimeRoot", + "lastUsage" : { + "arm64" : "2025-11-11T18:49:38Z" + }, + "name" : "tvOS 26.1" + } + ], + "devices" : { + "com.apple.CoreSimulator.SimRuntime.tvOS-26-1" : [ + { + "lastBootedAt" : "2025-11-03T15:42:10Z", + "dataPath" : "\/Users\/rolf\/Library\/Developer\/CoreSimulator\/Devices\/60ED31BD-80CE-420A-B0CB-756D2CD38201\/data", + "dataPathSize" : 1172033536, + "logPath" : "\/Users\/rolf\/Library\/Logs\/CoreSimulator\/60ED31BD-80CE-420A-B0CB-756D2CD38201", + "udid" : "60ED31BD-80CE-420A-B0CB-756D2CD38201", + "isAvailable" : true, + "logPathSize" : 266240, + "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K", + "state" : "Shutdown", + "name" : "Apple TV - tvOS 26.1" + } + ], + "com.apple.CoreSimulator.SimRuntime.iOS-26-0" : [ + { + "lastBootedAt" : "2025-11-04T08:20:55Z", + "dataPath" : "\/Users\/rolf\/Library\/Developer\/CoreSimulator\/Devices\/D4D95709-144A-4CAA-8469-89566EC1C935\/data", + "dataPathSize" : 1880317952, + "logPath" : "\/Users\/rolf\/Library\/Logs\/CoreSimulator\/D4D95709-144A-4CAA-8469-89566EC1C935", + "udid" : "D4D95709-144A-4CAA-8469-89566EC1C935", + "isAvailable" : false, + "availabilityError" : "runtime profile not found using \"System\" match policy", + "logPathSize" : 253952, + "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro", + "state" : "Shutdown", + "name" : "iPhone 17 Pro" + }, + ], + "com.apple.CoreSimulator.SimRuntime.iOS-26-1" : [ + { + "lastBootedAt" : "2025-11-06T12:53:03Z", + "dataPath" : "\/Users\/rolf\/Library\/Developer\/CoreSimulator\/Devices\/D40CE982-3E65-4756-8162-90EFE50AF7FA\/data", + "dataPathSize" : 2274861056, + "logPath" : "\/Users\/rolf\/Library\/Logs\/CoreSimulator\/D40CE982-3E65-4756-8162-90EFE50AF7FA", + "udid" : "D40CE982-3E65-4756-8162-90EFE50AF7FA", + "isAvailable" : true, + "logPathSize" : 253952, + "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11", + "state" : "Shutdown", + "name" : "iPhone 11 - iOS 26.1" + }, + { + "dataPath" : "\/Users\/rolf\/Library\/Developer\/CoreSimulator\/Devices\/3F1C114D-FC3D-481A-9CA1-499EE1339390\/data", + "dataPathSize" : 18337792, + "logPath" : "\/Users\/rolf\/Library\/Logs\/CoreSimulator\/3F1C114D-FC3D-481A-9CA1-499EE1339390", + "udid" : "3F1C114D-FC3D-481A-9CA1-499EE1339390", + "isAvailable" : true, + "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-12GB", + "state" : "Shutdown", + "name" : "iPad Pro 13-inch (M5)" + }, + { + "dataPath" : "\/Users\/rolf\/Library\/Developer\/CoreSimulator\/Devices\/F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4\/data", + "dataPathSize" : 18337792, + "logPath" : "\/Users\/rolf\/Library\/Logs\/CoreSimulator\/F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4", + "udid" : "F8BEDB0B-441A-4D05-AED6-E9724DEA6BF4", + "isAvailable" : true, + "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M5-12GB", + "state" : "Shutdown", + "name" : "iPad Pro 11-inch (M5)" + } + ] + }, + "pairs" : { + + } + } + """; + } +} diff --git a/tools/common/JsonExtensions.cs b/tools/common/JsonExtensions.cs new file mode 100644 index 000000000000..ebefb6028dc8 --- /dev/null +++ b/tools/common/JsonExtensions.cs @@ -0,0 +1,107 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; + +namespace Xamarin.Utils; + +public static class JsonExtensions { + public static JsonElement? GetNullableProperty (this JsonElement element, string propertyName) + { + if (element.TryGetProperty (propertyName, out var value)) + return value; + return null; + } + + public static JsonElement? GetNullableProperty (this JsonElement? element, string propertyName) + { + if (element?.TryGetProperty (propertyName, out var value) == true) + return value; + return null; + } + + public static string? GetStringProperty (this JsonElement? element, string propertyName) + { + return GetNullableProperty (element, propertyName)?.GetString (); + } + + public static string? GetStringProperty (this JsonElement element, params string [] propertyName) + { + return FindProperty (element, propertyName)?.GetString (); + } + + public static string GetStringPropertyOrEmpty (this JsonElement? element, string propertyName) + { + return GetStringProperty (element, propertyName) ?? string.Empty; + } + + public static string GetStringPropertyOrEmpty (this JsonElement element, params string [] propertyName) + { + return GetStringProperty (element, propertyName) ?? string.Empty; + } + + public static long? GetInt64Property (this JsonElement? element, params string [] nodes) + { + return FindProperty (element, nodes)?.GetInt64 (); + } + + public static ulong? GetUInt64Property (this JsonElement? element, params string [] nodes) + { + return FindProperty (element, nodes)?.GetUInt64 (); + } + + public static int? GetInt32Property (this JsonElement element, params string [] nodes) + { + return FindProperty (element, nodes)?.GetInt32 (); + } + + public static uint? GetUInt32Property (this JsonElement element, params string [] nodes) + { + return FindProperty (element, nodes)?.GetUInt32 (); + } + + public static bool? GetBooleanProperty (this JsonElement element, params string [] nodes) + { + return FindProperty (element, nodes)?.GetBoolean (); + } + + public static JsonElement? FindProperty (this JsonDocument doc, params string [] nodes) + { + return FindProperty (doc.RootElement, nodes); + } + + public static JsonElement? FindProperty (this JsonElement? element, params string [] nodes) + { + if (element is null) + return null; + return FindProperty (element.Value, nodes); + } + + public static JsonElement? FindProperty (this JsonElement element, params string [] nodes) + { + foreach (var node in nodes) { + if (element.ValueKind != JsonValueKind.Object) + return null; + + if (!element.TryGetProperty (node, out element)) + return null; + } + return element; + } + + public static bool TryGetProperty (this JsonDocument element, string propertyName, out JsonElement value) + { + value = default; + + if (element.RootElement.ValueKind != JsonValueKind.Object) + return false; + + return element.RootElement.TryGetProperty (propertyName, out value); + } + + public static IEnumerable EnumerateIfArray (this JsonElement element) + { + if (element.ValueKind == JsonValueKind.Array) { + foreach (var item in element.EnumerateArray ()) + yield return item; + } + } +} From a396aa99e72f86f01eeb6f0133591320865e8dc4 Mon Sep 17 00:00:00 2001 From: GitHub Actions Autoformatter Date: Tue, 18 Nov 2025 11:17:45 +0000 Subject: [PATCH 2/2] Auto-format source code --- .../Tasks/GetAvailableDevices.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetAvailableDevices.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetAvailableDevices.cs index f2bc2e2d2be7..76dae40ce96a 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/GetAvailableDevices.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/GetAvailableDevices.cs @@ -148,7 +148,7 @@ protected virtual async System.Threading.Tasks.Task ExecuteCtlAsync (par } async System.Threading.Tasks.Task ExecuteCtlToJsonAsync (params string [] args) - { + { var json = await ExecuteCtlAsync (args); var options = new JsonDocumentOptions { AllowTrailingCommas = true, @@ -286,7 +286,7 @@ System.Threading.Tasks.Task> RunSimCtlAsync () var hasRuntime = runtimes.TryGetValue (runtimeName, out var runtimeElement); var runtimePlatform = hasRuntime ? runtimeElement.GetStringProperty ("platform") ?? string.Empty : string.Empty; var runtimeVersion = hasRuntime ? runtimeElement.GetStringProperty ("version") ?? string.Empty : string.Empty; - var supportedArchitectures = hasRuntime ? runtimeElement.GetProperty ("supportedArchitectures").EnumerateIfArray ().Select (v => v.GetString ()) : Enumerable.Empty(); + var supportedArchitectures = hasRuntime ? runtimeElement.GetProperty ("supportedArchitectures").EnumerateIfArray ().Select (v => v.GetString ()) : Enumerable.Empty (); foreach (var element in runtime.Value.EnumerateIfArray ()) { var udid = element.GetStringProperty ("udid") ?? string.Empty; var isAvailable = element.GetBooleanProperty ("isAvailable") ?? false; @@ -309,7 +309,7 @@ System.Threading.Tasks.Task> RunSimCtlAsync () item.SetMetadata ("Type", "Simulator"); item.SetMetadata ("OSVersion", runtimeVersion); item.SetMetadata ("UDID", udid); - + var discardedReason = ""; var runtimeIdentifier = ""; if (isAvailable) { @@ -387,9 +387,9 @@ System.Threading.Tasks.Task> RunSimCtlAsync () } Version.TryParse (deviceTypeElement.GetStringProperty ("minRuntimeVersionString"), out minimumOSVersion); Version.TryParse (deviceTypeElement.GetStringProperty ("maxRuntimeVersionString"), out maximumOSVersion); - } else { - discardedReason = $"Unknown device type identifier '{deviceTypeIdentifier}'"; - } + } else { + discardedReason = $"Unknown device type identifier '{deviceTypeIdentifier}'"; + } } rv.Add (new DeviceInfo (item, runtimeIdentifier, platform, deviceType, minimumOSVersion, maximumOSVersion, discardedReason));