chromium/ios/build/bots/scripts/iossim_util_test.py

#!/usr/bin/env vpython3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unittests for iossim_util.py."""

import mock
import unittest

import iossim_util
import mac_util
import os
import subprocess
import test_runner
import test_runner_test

SIMULATORS_LIST = {
    'devices': {
        'com.apple.CoreSimulator.SimRuntime.iOS-11-4': [{
            'isAvailable':
                True,
            'name':
                'iPhone 5s 11.4 test simulator',
            'deviceTypeIdentifier':
                'com.apple.CoreSimulator.SimDeviceType.iPhone-5s',
            'state':
                'Shutdown',
            'udid':
                'E4E66320-177A-450A-9BA1-488D85B7278E'
        }],
        'com.apple.CoreSimulator.SimRuntime.iOS-13-2': [{
            'isAvailable':
                True,
            'name':
                'iPhone X 13.2.2 test simulator',
            'deviceTypeIdentifier':
                'com.apple.CoreSimulator.SimDeviceType.iPhone-X',
            'state':
                'Shutdown',
            'udid':
                'E4E66321-177A-450A-9BA1-488D85B7278E'
        }, {
            'isAvailable':
                True,
            'name':
                'iPhone 11 13.2.2 test simulator',
            'deviceTypeIdentifier':
                'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
            'state':
                'Shutdown',
            'udid':
                'A4E66321-177A-450A-9BA1-488D85B7278E'
        }]
    },
    'devicetypes': [
        {
            'name': 'iPhone 5s',
            'bundlePath': '/path/iPhone 4s/Content',
            'identifier': 'com.apple.CoreSimulator.SimDeviceType.iPhone-5s'
        },
        {
            'name': 'iPhone X',
            'bundlePath': '/path/iPhone X/Content',
            'identifier': 'com.apple.CoreSimulator.SimDeviceType.iPhone-X'
        },
        {
            'name': 'iPhone 11',
            'bundlePath': '/path/iPhone 11/Content',
            'identifier': 'com.apple.CoreSimulator.SimDeviceType.iPhone-11'
        },
    ],
    'pairs': [],
    'runtimes': [
        {
            "buildversion": "15F79",
            "bundlePath": "/path/Runtimes/iOS 11.4.simruntime",
            "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-11-4",
            "isAvailable": True,
            "name": "iOS 11.4",
            "version": "11.4"
        },
        {
            "buildversion": "17A844",
            "bundlePath": "/path/Runtimes/iOS 13.1.simruntime",
            "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-1",
            "isAvailable": True,
            "name": "iOS 13.1",
            "version": "13.1"
        },
        {
            "buildversion": "17B102",
            "bundlePath": "/path/Runtimes/iOS.simruntime",
            "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-2",
            "isAvailable": True,
            "name": "iOS 13.2",
            "version": "13.2.2"
        },
    ]
}

RUNTIMES_LIST = {
    "111111": {
        "build": "21A111111",
        "deletable": True,
        "identifier": "111111",
        "kind": "Disk Image",
        "lastUsedAt": "2023-09-28T21:57:06Z",
        "mountPath": "path/to/mount",
        "path": "path/to/runtime/111111.dmg",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/111111.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-1",
        "signatureState": "Verified",
        "sizeBytes": 3595537104,
        "state": "Ready",
        "version": "13.1"
    },
    "222222": {
        "build": "21A222222",
        "deletable": False,
        "identifier": "222222",
        "kind": "Bundled with Xcode",
        "path": "path/to/runtime/222222.simruntime",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/222222.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-13-2",
        "signatureState": "Unknown",
        "sizeBytes": 14115082240,
        "state": "Ready",
        "version": "13.2"
    },
    "333333": {
        "build": "21A333333",
        "deletable": True,
        "identifier": "333333",
        "kind": "Legacy Download",
        "path": "path/to/runtime/333333.simruntime",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/333333.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-11-4",
        "signatureState": "Unknown",
        "sizeBytes": 12596879360,
        "state": "Ready",
        "version": "11.4"
    },
    "444444": {
        "build": "21A444444",
        "deletable": True,
        "identifier": "444444",
        "kind": "Disk Image",
        "lastUsedAt": "2022-09-28T21:57:06Z",
        "path": "path/to/runtime/444444.simruntime",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/444444.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-14-1",
        "signatureState": "Unknown",
        "sizeBytes": 12596879361,
        "state": "Ready",
        "version": "14.1"
    },
    "555555": {
        "build": "555555",
        "deletable": True,
        "identifier": "555555",
        "kind": "Disk Image",
        "path": "path/to/runtime/555555.simruntime",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/555555.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-15-1",
        "signatureState": "Unknown",
        "sizeBytes": 12596879362,
        "state": "Ready",
        "version": "15.1"
    }
}

IOS18_RUNTIMES_LIST = {
    "111111": {
        "build": "22A5297f",
        "deletable": True,
        "identifier": "111111",
        "kind": "Disk Image",
        "lastUsedAt": "2024-06-26T16:57:51Z",
        "mountPath": "path/to/mount/iOS_22A5297f",
        "path": "path/to/runtime/111111.dmg",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/iOS 18.0.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
        "signatureState": "Verified",
        "sizeBytes": 8291822059,
        "state": "Ready",
        "version": "18.0"
    },
    "222222": {
        "build": "22A5282m",
        "deletable": True,
        "identifier": "222222",
        "kind": "Disk Image",
        "lastUsedAt": "2024-06-26T14:56:35Z",
        "mountPath": "path/to/mount/iOS_22A5282m",
        "parentIdentifier": "333333",
        "parentImagePath": "path/to/parent/image/090-28824-040.dmg",
        "parentMountPath": "path/to/parent/mount//SimRuntimeBundle-333333",
        "path": "path/to/runtime/090-28222-040.dmg",
        "platformIdentifier": "com.apple.platform.iphonesimulator",
        "runtimeBundlePath": "path/to/bundle/runtime/iOS 18.0.simruntime",
        "runtimeIdentifier": "com.apple.CoreSimulator.SimRuntime.iOS-18-0",
        "signatureState": "Verified",
        "sizeBytes": 8461564223,
        "state": "Ready",
        "version": "18.0"
    }
}

RUNTIMES_MATCH_LIST = {
    "appletvos17.0": {
        "chosenRuntimeBuild": "21J11111",
        "defaultBuild": "21J11111",
        "platform": "com.apple.platform.appletvos",
        "preferredBuild": "21J11111",
        "sdkBuild": "21J11111",
        "sdkVersion": "13.1"
    },
    "iphoneos17.0": {
        "chosenRuntimeBuild": "21A111112",
        "defaultBuild": "21A111112",
        "platform": "com.apple.platform.iphoneos",
        "preferredBuild": "21A111111",
        "sdkBuild": "21A111112",
        "sdkVersion": "13.1"
    }
}

ADD_RUNTIME_OUTPUT = "111111 (iOS17.0)"


@mock.patch.object(
    iossim_util, 'get_simulator_list', return_value=SIMULATORS_LIST)
@mock.patch.object(
    iossim_util, 'get_simulator_runtime_list', return_value=RUNTIMES_LIST)
class GetiOSSimUtil(test_runner_test.TestCase):
  """Tests for iossim_util.py."""

  def setUp(self):
    super(GetiOSSimUtil, self).setUp()

  def test_get_simulator_runtime_by_version(self, _, _2):
    """Ensures correctness of filter."""
    self.assertEqual(
        'com.apple.CoreSimulator.SimRuntime.iOS-13-2',
        iossim_util.get_simulator_runtime_by_version(
            iossim_util.get_simulator_list(), '13.2.2'))

  def test_get_simulator_runtime_by_version_not_found(self, _, _2):
    """Ensures that SimulatorNotFoundError raises if no runtime."""
    with self.assertRaises(test_runner.SimulatorNotFoundError) as context:
      iossim_util.get_simulator_runtime_by_version(
          iossim_util.get_simulator_list(), '13.3')
    expected_message = ('Simulator does not exist: Not found '
                        '"13.3" SDK in runtimes')
    self.assertTrue(expected_message in str(context.exception))

  def test_get_simulator_device_type_by_platform(self, _, _2):
    """Ensures correctness of filter."""
    self.assertEqual(
        'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
        iossim_util.get_simulator_device_type_by_platform(
            iossim_util.get_simulator_list(), 'iPhone 11'))

  def test_get_simulator_device_type_by_platform_not_found(self, _, _2):
    """Ensures that SimulatorNotFoundError raises if no platform."""
    with self.assertRaises(test_runner.SimulatorNotFoundError) as context:
      iossim_util.get_simulator_device_type_by_platform(
          iossim_util.get_simulator_list(), 'iPhone XI')
    expected_message = ('Simulator does not exist: Not found device '
                        '"iPhone XI" in devicetypes')
    self.assertTrue(expected_message in str(context.exception))

  def test_get_simulator_runtime_by_device_udid(self, _, _2):
    """Ensures correctness of filter."""
    self.assertEqual(
        'com.apple.CoreSimulator.SimRuntime.iOS-13-2',
        iossim_util.get_simulator_runtime_by_device_udid(
            'E4E66321-177A-450A-9BA1-488D85B7278E'))

  def test_get_simulator_runtime_by_device_udid_not_found(self, _, _2):
    """Ensures that SimulatorNotFoundError raises if no device with UDID."""
    with self.assertRaises(test_runner.SimulatorNotFoundError) as context:
      iossim_util.get_simulator_runtime_by_device_udid('non_existing_UDID')
    expected_message = ('Simulator does not exist: Not found simulator with '
                        '"non_existing_UDID" UDID in devices')
    self.assertTrue(expected_message in str(context.exception))

  def test_get_simulator_udids_by_platform_and_version(self, _, _2):
    """Ensures correctness of filter."""
    self.assertEqual(['A4E66321-177A-450A-9BA1-488D85B7278E'],
                     iossim_util.get_simulator_udids_by_platform_and_version(
                         'iPhone 11', '13.2.2'))

  def test_get_simulator_udids_by_platform_and_version_not_found(self, _, _2):
    """Ensures that filter returns empty list if no device with version."""
    self.assertEqual([],
                     iossim_util.get_simulator_udids_by_platform_and_version(
                         'iPhone 11', '13.1'))

  @mock.patch('subprocess.check_output', autospec=True)
  def test_create_device_by_platform_and_version(self, subprocess_mock, _, _2):
    """Ensures that command is correct."""
    subprocess_mock.return_value = b'NEW_UDID'
    self.assertEqual(
        'NEW_UDID',
        iossim_util.create_device_by_platform_and_version(
            'iPhone 11', '13.2.2'))
    self.assertEqual([
        'xcrun', 'simctl', 'create', 'iPhone 11 13.2.2 test simulator',
        'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
        'com.apple.CoreSimulator.SimRuntime.iOS-13-2'
    ], subprocess_mock.call_args[0][0])

  @mock.patch('subprocess.check_output', autospec=True)
  def test_delete_simulator_by_udid(self, subprocess_mock, _, _2):
    """Ensures that command is correct."""
    iossim_util.delete_simulator_by_udid('UDID')
    self.assertEqual(['xcrun', 'simctl', 'delete', 'UDID'],
                     subprocess_mock.call_args[0][0])

  @mock.patch('subprocess.check_call', autospec=True)
  def test_wipe_simulator_by_platform_and_version(self, subprocess_mock, _, _2):
    """Ensures that command is correct."""
    iossim_util.wipe_simulator_by_udid('A4E66321-177A-450A-9BA1-488D85B7278E')
    self.assertEqual(
        ['xcrun', 'simctl', 'erase', 'A4E66321-177A-450A-9BA1-488D85B7278E'],
        subprocess_mock.call_args[0][0])

  @mock.patch('subprocess.check_output', autospec=True)
  def test_get_home_directory(self, subprocess_mock, _, _2):
    """Ensures that command is correct."""
    subprocess_mock.return_value = b'HOME_DIRECTORY'
    self.assertEqual('HOME_DIRECTORY',
                     iossim_util.get_home_directory('iPhone 11', '13.2.2'))
    self.assertEqual([
        'xcrun', 'simctl', 'getenv', 'A4E66321-177A-450A-9BA1-488D85B7278E',
        'HOME'
    ], subprocess_mock.call_args[0][0])

  @mock.patch.object(iossim_util, 'create_device_by_platform_and_version')
  def test_no_new_sim_created_when_one_exists(self, mock_create, _, _2):
    """Ensures no simulator is created when one in desired dimension exists."""
    self.assertEqual('A4E66321-177A-450A-9BA1-488D85B7278E',
                     iossim_util.get_simulator('iPhone 11', '13.2.2'))
    self.assertFalse(mock_create.called)

  def test_copy_cert(self, _, _2):
    """Ensures right commands are issued to copy cert"""
    self.mock(os.path, 'exists', lambda *args: True)

    check_call_mock = mock.Mock()
    self.mock(subprocess, 'check_call', check_call_mock)
    iossim_util.copy_trusted_certificate('test/cert', 'UDID')

    calls = [
        mock.call(['xcrun', 'simctl', 'boot', 'UDID']),
        mock.call([
            'xcrun', 'simctl', 'keychain', 'UDID', 'add-root-cert', 'test/cert'
        ]),
        mock.call(['xcrun', 'simctl', 'shutdown', 'UDID'])
    ]

    check_call_mock.assert_has_calls(calls)
    # ensure subprocess.check_call was only called 3 times
    self.assertEqual(check_call_mock.call_count, 3)

  def test_get_simulator_runtime_info_by_build(self, _, _2):
    runtime = iossim_util.get_simulator_runtime_info_by_build('21A111111')
    self.assertIsNotNone(runtime)
    self.assertEqual(runtime['version'], '13.1')
    self.assertEqual(runtime['identifier'], '111111')

  def test_get_simulator_runtime_info_by_id(self, _, _2):
    runtime = iossim_util.get_simulator_runtime_info_by_id('111111')
    self.assertIsNotNone(runtime)
    self.assertEqual(runtime['version'], '13.1')
    self.assertEqual(runtime['identifier'], '111111')

  def test_get_simulator_runtime_info(self, _, _2):
    runtime = iossim_util.get_simulator_runtime_info('13.1')
    self.assertIsNotNone(runtime)
    self.assertEqual(runtime['version'], '13.1')

  @mock.patch.object(iossim_util, 'get_simulator_runtime_match_list')
  def test_override_default_iphonesim_runtime(self, override_runtime_mock, _,
                                              _2):
    check_call_mock = mock.Mock()
    self.mock(subprocess, 'check_call', check_call_mock)
    override_runtime_mock.return_value = RUNTIMES_MATCH_LIST
    iossim_util.override_default_iphonesim_runtime(ADD_RUNTIME_OUTPUT, '17.0')
    calls = [
        mock.call([
            'xcrun', 'simctl', 'runtime', 'match', 'set', 'iphoneos17.0',
            '21A111111', '--sdkBuild', '21A111112'
        ]),
    ]
    check_call_mock.assert_has_calls(calls)

  @mock.patch.object(iossim_util, 'get_simulator_runtime_match_list')
  def test_override_default_iphonesim_runtime_not_found(self,
                                                        override_runtime_mock,
                                                        _, _2):
    check_call_mock = mock.Mock()
    self.mock(subprocess, 'check_call', check_call_mock)
    override_runtime_mock.return_value = RUNTIMES_MATCH_LIST
    iossim_util.override_default_iphonesim_runtime('random string', '17.0')
    check_call_mock.assert_not_called()

  def test_add_simulator_runtime(self, _, _2):
    check_output_mock = mock.Mock()
    self.mock(subprocess, 'check_output', check_output_mock)
    dmg_path = 'path/to/dmg'
    iossim_util.add_simulator_runtime(dmg_path)

    calls = [
        mock.call(['xcrun', 'simctl', 'runtime', 'add', dmg_path]),
    ]

    check_output_mock.assert_has_calls(calls)
    self.assertEqual(check_output_mock.call_count, 1)

  def test_delete_simulator_runtime(self, _, _2):
    check_output_mock = mock.Mock()
    self.mock(subprocess, 'check_output', check_output_mock)
    runtime_id = '111111'
    iossim_util.delete_simulator_runtime(runtime_id)

    calls = [
        mock.call(['xcrun', 'simctl', 'runtime', 'delete', runtime_id]),
    ]

    check_output_mock.assert_has_calls(calls)
    self.assertEqual(check_output_mock.call_count, 1)

  def test_delete_simulator_runtime_and_wait_success(self, _, _2):
    with mock.patch('iossim_util.get_simulator_runtime_info_by_id') \
        as mock_get_runtime_info_by_id, \
        mock.patch('iossim_util.get_simulator_runtime_info') \
        as mock_get_runtime_info, \
        mock.patch('iossim_util.delete_simulator_runtime') \
        as mock_delete_runtime:
      mock_get_runtime_info_by_id.side_effect = [
          {
              'identifier': '111111',
              'state': 'deleting'
          },
          None,
      ]
      mock_get_runtime_info.return_value = {
          'identifier': '111111',
          'state': 'ready'
      }
      mock_delete_runtime.return_value = None

      iossim_util.delete_simulator_runtime_and_wait('13.1')

      mock_get_runtime_info.assert_called_with('13.1')
      mock_delete_runtime.assert_called_once_with('111111', True)

  def test_delete_least_recently_used_simulator_runtimes(self, _, _2):
    with mock.patch('iossim_util.delete_simulator_runtime') \
       as mock_delete_simulator_runtime:
      iossim_util.delete_least_recently_used_simulator_runtimes(1)

      calls = [
          mock.call('444444', True),
          mock.call('555555', True),
      ]

      self.assertEqual(mock_delete_simulator_runtime.call_count, 2)
      mock_delete_simulator_runtime.assert_has_calls(calls, any_order=True)

  def test_delete_least_recently_used_simulator_runtimes_no_op(self, _, _2):
    with mock.patch('iossim_util.delete_simulator_runtime') \
       as mock_delete_simulator_runtime:
      iossim_util.delete_least_recently_used_simulator_runtimes()

      self.assertEqual(mock_delete_simulator_runtime.call_count, 0)

  def test_delete_other_ios18_runtimes(self, mock_get_simulator_runtime_list,
                                       _):
    mock_get_simulator_runtime_list.return_value = IOS18_RUNTIMES_LIST
    with mock.patch('iossim_util.delete_simulator_runtime') \
       as mock_delete_simulator_runtime:
      iossim_util.delete_other_ios18_runtimes('22A5297f')

      self.assertEqual(mock_delete_simulator_runtime.call_count, 1)
      mock_delete_simulator_runtime.assert_has_calls(
          [mock.call('222222', True)], any_order=True)

  def test_disable_hardware_keyboard(self, _, _2):
    """Ensures right commands are issued to disable hardware keyboard"""

    self.mock(os.path, 'exists', lambda *args: False)
    self.mock(os.path, 'expanduser', lambda *args: 'PATH')

    check_call_mock = mock.Mock()
    self.mock(subprocess, 'check_call', check_call_mock)
    check_calls = [
        mock.call(['plutil', '-create', 'binary1', 'PATH']),
        mock.call(
            ['plutil', '-insert', 'DevicePreferences', '-dictionary', 'PATH']),
        mock.call([
            'plutil', '-insert', 'DevicePreferences.UDID', '-dictionary', 'PATH'
        ]),
        mock.call([
            'plutil', '-replace',
            'DevicePreferences.UDID.ConnectHardwareKeyboard', '-bool', 'NO',
            'PATH'
        ])
    ]

    dict_mock = mock.Mock()
    self.mock(mac_util, 'plist_as_dict', dict_mock)

    # file does not exist
    dict_mock.return_value = ({}, None)
    iossim_util.disable_hardware_keyboard('UDID')
    check_call_mock.assert_has_calls(check_calls)

    # file exists but is empty
    self.mock(os.path, 'exists', lambda *args: True)
    check_call_mock.reset_mock()
    dict_mock.return_value = ({}, None)
    iossim_util.disable_hardware_keyboard('UDID')
    check_call_mock.assert_has_calls(check_calls[1:])

    #Device Prefs Dictionary Exists but not Prefs for UDID
    check_call_mock.reset_mock()
    dict_mock.return_value = ({'DevicePreferences': {}}, None)
    iossim_util.disable_hardware_keyboard('UDID')
    check_call_mock.assert_has_calls(check_calls[2:])

    # Prefs for UDID exists
    check_call_mock.reset_mock()
    dict_mock.return_value = ({'DevicePreferences': {'UDID': {}}}, None)
    iossim_util.disable_hardware_keyboard('UDID')
    check_call_mock.assert_has_calls(check_calls[3:])

  def test_disable_simulator_keyboard_tutorial(self, _, _2):
    with mock.patch('iossim_util.boot_simulator_if_not_booted') \
        as mock_boot_simulator:
      # boots successfully
      mock_boot_simulator.return_value = None

      check_call_mock = mock.Mock()
      self.mock(subprocess, 'check_call', check_call_mock)
      udid = "1111111"
      check_calls = [
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences',
              'DidShowContinuousPathIntroduction', '1'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences',
              'KeyboardDidShowProductivityTutorial', '1'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences',
              'DidShowGestureKeyboardIntroduction', '1'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences',
              'UIKeyboardDidShowInternationalInfoIntroduction', '1'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences', 'KeyboardAutocorrection', '0'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences', 'KeyboardPrediction', '0'
          ]),
          mock.call([
              'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
              'com.apple.keyboard.preferences', 'KeyboardShowPredictionBar', '0'
          ])
      ]

      iossim_util.disable_simulator_keyboard_tutorial(udid)
      check_call_mock.assert_has_calls(check_calls)


if __name__ == '__main__':
  unittest.main()