Repository: beeware/briefcase
Pull request #745 (2022-05-25 15:39:06)

fix: Trim simulator os when sorting for version
`get_simulators` is returning `OS Version` and it is being used as a key for sorting which is causing an issue. Splitting it and using version number for sorting.

Updated `get_simulators` return value in tests.

Closes https://github.com/beeware/briefcase/issues/528

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [ ] All new features have been tested
- [ ] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct



Patch:
diff --git a/changes/528.bugfix.rst b/changes/528.bugfix.rst
new file mode 100644
index 00000000..5d4add0c
--- /dev/null
+++ b/changes/528.bugfix.rst
@@ -0,0 +1,1 @@
+Corrected a problem with the iOS simulator detecting the iOS version when only a device name is provided.
diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py
index 610da43b..f397d5ba 100644
--- a/src/briefcase/platforms/iOS/xcode.py
+++ b/src/briefcase/platforms/iOS/xcode.py
@@ -107,7 +107,16 @@ class iOSXcodeMixin(iOSXcodePassiveMixin):
                 # A device name::version.
                 device, iOS_version = udid_or_device.split('::')
                 try:
-                    devices = simulators[iOS_version]
+                    # Convert the simulator dict into a dict where
+                    # the iOS versions are lower cased, then do a lookup
+                    # on the lower case name provided by the user.
+                    # However, also return the *unmodified* iOS version string
+                    # so we can convert the user-provided iOS version into the
+                    # "clean", official capitalization.
+                    iOS_version, devices = {
+                        clean_iOS_version.lower(): (clean_iOS_version, details)
+                        for clean_iOS_version, details in simulators.items()
+                    }[iOS_version.lower()]
                     try:
                         # Do a reverse lookup for UDID, based on a
                         # case-insensitive name lookup.
@@ -129,9 +138,12 @@ class iOSXcodeMixin(iOSXcodePassiveMixin):
                 device = udid_or_device
 
                 # Search iOS versions, looking for most recent version first.
+                # The iOS version string will be something like "iOS 15.4";
+                # Drop the prefix (if it exists), convert into the tuple (15, 4),
+                # and sort numerically.
                 for iOS_version, devices in sorted(
                     simulators.items(),
-                    key=lambda item: tuple(int(v) for v in item[0].split('.')),
+                    key=lambda item: tuple(int(v) for v in item[0].split()[-1].split('.')),
                     reverse=True
                 ):
                     try:


Test patch:
diff --git a/tests/platforms/iOS/xcode/mixin/test_select_target_device.py b/tests/platforms/iOS/xcode/mixin/test_select_target_device.py
index 16ee8278..93ce856f 100644
--- a/tests/platforms/iOS/xcode/mixin/test_select_target_device.py
+++ b/tests/platforms/iOS/xcode/mixin/test_select_target_device.py
@@ -38,7 +38,7 @@ def test_explicit_device_udid(dummy_command):
     "If the user nominates a device UDID at the command line, it is used."
     # get_simulators will return some options.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -50,13 +50,45 @@ def test_explicit_device_udid(dummy_command):
     udid, iOS_version, device = result
 
     assert udid == '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D'
-    assert iOS_version == '13.2'
+    assert iOS_version == 'iOS 13.2'
     assert device == 'iPhone 11'
 
 
-def test_explicit_device_name(dummy_command):
+def test_explicit_device_name_should_find_highest_version(dummy_command):
+    "If an explicit device name is provided (case insensitive) it is used."
+    # get_simulators will return multiple options on 2 iOS versions.
+    dummy_command.get_simulators.return_value = {
+        'iOS 10.3': {
+            '0BB80120-FA02-4597-A1BA-DB8CDE4F086D': 'iPhone 5s',
+            'D7BBAD14-38FD-48F5-ACFD-B1193F829216': 'iPhone 6',
+            '6998CA09-44B5-4963-8F80-265412D99683': 'iPhone 7',
+            '68A6E340-8376-47C5-9468-C9A005C88209': 'iPhone 8',
+        },
+        'iOS 13.2': {
+            'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
+            '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
+            'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
+        }
+    }
+
+    # Try to load an iPhone 8.
+    # It matches case insensitive, and finds the highest iOS version.
+    udid, iOS_version, device = dummy_command.select_target_device('iphone 8')
+
+    assert udid == 'C9A005C8-9468-47C5-8376-68A6E3408209'
+    assert iOS_version == 'iOS 13.2'
+    assert device == 'iPhone 8'
+
+    # User input was not solicited
+    assert dummy_command.input.prompts == []
+
+
+def test_explicit_device_name_should_find_highest_version_no_os(dummy_command):
     "If an explicit device name is provided (case insensitive) it is used."
     # get_simulators will return multiple options on 2 iOS versions.
+    # In older versions of Xcode, the OS name wasn't included in the version
+    # string. Ensure that this still works, even though it doesn't happen in
+    # practice (as of May 2022)
     dummy_command.get_simulators.return_value = {
         '10.3': {
             '0BB80120-FA02-4597-A1BA-DB8CDE4F086D': 'iPhone 5s',
@@ -87,25 +119,25 @@ def test_explicit_device_name_and_version(dummy_command):
     "If there are multiple options on multiple devices, two user inputs are needed."
     # get_simulators will return multiple options on 2 iOS versions.
     dummy_command.get_simulators.return_value = {
-        '10.3': {
+        'iOS 10.3': {
             '0BB80120-FA02-4597-A1BA-DB8CDE4F086D': 'iPhone 5s',
             'D7BBAD14-38FD-48F5-ACFD-B1193F829216': 'iPhone 6',
             '6998CA09-44B5-4963-8F80-265412D99683': 'iPhone 7',
             '68A6E340-8376-47C5-9468-C9A005C88209': 'iPhone 8',
         },
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
         }
     }
 
-    # Specify an iPhone 8 on 10.3
+    # Specify an iPhone 8 on iOS 10.3
     # It matches case insensitive, and finds the explicit iOS version.
-    udid, iOS_version, device = dummy_command.select_target_device('iphone 8::10.3')
+    udid, iOS_version, device = dummy_command.select_target_device('iphone 8::ios 10.3')
 
     assert udid == '68A6E340-8376-47C5-9468-C9A005C88209'
-    assert iOS_version == '10.3'
+    assert iOS_version == 'iOS 10.3'
     assert device == 'iPhone 8'
 
     # User input was not solicited
@@ -116,7 +148,7 @@ def test_invalid_explicit_device_udid(dummy_command):
     "If the user nominates an invalid device UDID, an error is raised"
     # get_simulators will some options.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -135,7 +167,7 @@ def test_invalid_explicit_device_name(dummy_command):
     "If the user nominates an invalid device name, an error is raised"
     # get_simulators will some options.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -154,7 +186,7 @@ def test_invalid_explicit_device_name_and_version(dummy_command):
     "If the user nominates an invalid device name and version, an error is raised"
     # get_simulators will some options.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -171,7 +203,7 @@ def test_implied_device(dummy_command):
     "If there's only one device, no input is required; the device is returned."
     # get_simulators will return one option.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
         }
     }
@@ -179,7 +211,7 @@ def test_implied_device(dummy_command):
     udid, iOS_version, device = dummy_command.select_target_device()
 
     assert udid == '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D'
-    assert iOS_version == '13.2'
+    assert iOS_version == 'iOS 13.2'
     assert device == 'iPhone 11'
 
     # No user input was solicited
@@ -190,7 +222,7 @@ def test_implied_os(dummy_command):
     "If there is only one OS option, it's implied."
     # get_simulators will return multiple options on 1 iOS version.
     dummy_command.get_simulators.return_value = {
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -203,7 +235,7 @@ def test_implied_os(dummy_command):
     udid, iOS_version, device = dummy_command.select_target_device()
 
     assert udid == 'EEEBA06C-81F9-407C-885A-2261306DB2BE'
-    assert iOS_version == '13.2'
+    assert iOS_version == 'iOS 13.2'
     assert device == 'iPhone 11 Pro Max'
 
     # User input was solicited once
@@ -214,12 +246,12 @@ def test_multiple_os_implied_device(dummy_command):
     "If there are multiple OS options, but only one device on the chosen OS, device is implied."
     # get_simulators will return multiple options on 1 iOS version.
     dummy_command.get_simulators.return_value = {
-        '10.3': {
+        'iOS 10.3': {
             '0BB80120-FA02-4597-A1BA-DB8CDE4F086D': 'iPhone 5s',
             'D7BBAD14-38FD-48F5-ACFD-B1193F829216': 'iPhone 6',
             '6998CA09-44B5-4963-8F80-265412D99683': 'iPhone 7',
         },
-        '13.2': {
+        'iOS 13.2': {
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
         }
     }
@@ -231,7 +263,7 @@ def test_multiple_os_implied_device(dummy_command):
     udid, iOS_version, device = dummy_command.select_target_device()
 
     assert udid == '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D'
-    assert iOS_version == '13.2'
+    assert iOS_version == 'iOS 13.2'
     assert device == 'iPhone 11'
 
     # User input was solicited once
@@ -242,12 +274,12 @@ def test_os_and_device_options(dummy_command):
     "If there are multiple options on multiple devices, two user inputs are needed."
     # get_simulators will return multiple options on 2 iOS versions.
     dummy_command.get_simulators.return_value = {
-        '10.3': {
+        'iOS 10.3': {
             '0BB80120-FA02-4597-A1BA-DB8CDE4F086D': 'iPhone 5s',
             'D7BBAD14-38FD-48F5-ACFD-B1193F829216': 'iPhone 6',
             '6998CA09-44B5-4963-8F80-265412D99683': 'iPhone 7',
         },
-        '13.2': {
+        'iOS 13.2': {
             'C9A005C8-9468-47C5-8376-68A6E3408209': 'iPhone 8',
             '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D': 'iPhone 11',
             'EEEBA06C-81F9-407C-885A-2261306DB2BE': 'iPhone 11 Pro Max',
@@ -260,7 +292,7 @@ def test_os_and_device_options(dummy_command):
     udid, iOS_version, device = dummy_command.select_target_device()
 
     assert udid == '2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D'
-    assert iOS_version == '13.2'
+    assert iOS_version == 'iOS 13.2'
     assert device == 'iPhone 11'
 
     # User input was solicited twice
@@ -280,7 +312,7 @@ def test_no_devices_for_os(dummy_command):
     "If there are no devices for an OS version, raise an error."
     # get_simulators returns no devices for iOS 13.2
     dummy_command.get_simulators.return_value = {
-        '13.2': {}
+        'iOS 13.2': {}
     }
 
     with pytest.raises(BriefcaseCommandError):