#!/usr/bin/env vpython3
# Copyright 2021 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 xcode_util.py."""

import logging
import mock
import os
import unittest

import constants
import test_runner_errors
import test_runner_test
import xcode_util


_XCODEBUILD_VERSION_OUTPUT_12 = b"""Xcode 12.4
Build version 12D4e
"""
_XCODEBUILD_VERSION_OUTPUT_13 = b"""Xcode 13.0
Build version 13A5155e
"""
_XCODEBUILD_VERSION_OUTPUT_15 = b"""Xcode 15.0
Build version 15A5209g
"""

_XCODEBUILD_VERSION_OUTPUT_16 = b"""Xcode 16.0
Build version 16A5171c
"""

ADD_SIMULATOR_RUNTIME_OUTPUT = 'ramdomid (iOS 15.0)'

RUNTIME_15_0 = {
  "build": "20C52",
  "deletable": True,
  "identifier": "F58C93A6-E6F2-4AC3-BBED-CA9195B186D3",
  "kind": "Disk Image",
  "signatureState": "Verified",
  "sizeBytes": 6407069874,
  "state": "Ready",
  "version": "15.0",
}


class XcodeUtilTest(test_runner_test.TestCase):
  """Test class for xcode_util functions."""

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

  @mock.patch(
    'subprocess.check_output', return_value=_XCODEBUILD_VERSION_OUTPUT_13
  )
  def test_version(self, _):
    """Tests xcode_util.version()"""
    version, build_version = xcode_util.version()
    self.assertEqual(version, '13.0')
    self.assertEqual(build_version, '13a5155e')

  @mock.patch(
    'subprocess.check_output', return_value=_XCODEBUILD_VERSION_OUTPUT_12
  )
  def test_using_xcode_12(self, _):
    """Tests xcode_util.using_xcode_11_or_higher"""
    self.assertTrue(xcode_util.using_xcode_11_or_higher())
    self.assertFalse(xcode_util.using_xcode_13_or_higher())
    self.assertFalse(xcode_util.using_xcode_15_or_higher())
    self.assertFalse(xcode_util.using_xcode_16_or_higher())

  @mock.patch(
    'subprocess.check_output', return_value=_XCODEBUILD_VERSION_OUTPUT_13
  )
  def test_using_xcode_13(self, _):
    """Tests xcode_util.using_xcode_13_or_higher"""
    self.assertTrue(xcode_util.using_xcode_11_or_higher())
    self.assertTrue(xcode_util.using_xcode_13_or_higher())
    self.assertFalse(xcode_util.using_xcode_15_or_higher())
    self.assertFalse(xcode_util.using_xcode_16_or_higher())

  @mock.patch(
    'subprocess.check_output', return_value=_XCODEBUILD_VERSION_OUTPUT_15
  )
  def test_using_xcode_15(self, _):
    """Tests xcode_util.using_xcode_15_or_higher"""
    self.assertTrue(xcode_util.using_xcode_11_or_higher())
    self.assertTrue(xcode_util.using_xcode_13_or_higher())
    self.assertTrue(xcode_util.using_xcode_15_or_higher())
    self.assertFalse(xcode_util.using_xcode_16_or_higher())

  @mock.patch(
    'subprocess.check_output', return_value=_XCODEBUILD_VERSION_OUTPUT_16
  )
  def test_using_xcode_16(self, _):
    """Tests xcode_util.using_xcode_16_or_higher"""
    self.assertTrue(xcode_util.using_xcode_11_or_higher())
    self.assertTrue(xcode_util.using_xcode_13_or_higher())
    self.assertTrue(xcode_util.using_xcode_15_or_higher())
    self.assertTrue(xcode_util.using_xcode_16_or_higher())


class InstallTest(XcodeUtilTest):
  """Test class for xcode_util.install function."""

  def setUp(self):
    super(InstallTest, self).setUp()
    self.mac_toolchain = 'mac_toolchain'
    self.xcode_build_version = 'TestXcodeVersion'
    self.xcode_app_path = 'test/path/Xcode.app'
    self.runtime_cache_folder = 'test/path/Runtime'
    self.platform_version = '14.4'

  @mock.patch('xcode_util.move_runtime', autospec=True)
  @mock.patch('xcode_util._install_runtime', autospec=True)
  @mock.patch('xcode_util._install_xcode', autospec=True)
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  def test_new_mactoolchain_legacy_xcode(
    self,
    mock_macos_13_or_higher,
    mock_install_xcode,
    mock_install_runtime,
    mock_move_runtime,
  ):
    mock_macos_13_or_higher.return_value = False
    self.mock(xcode_util, '_is_legacy_xcode_package', lambda path: True)

    is_legacy_xcode = xcode_util.install(
      self.mac_toolchain, self.xcode_build_version, self.xcode_app_path
    )

    self.assertTrue(is_legacy_xcode, 'install should return true')
    mock_install_xcode.assert_called_with(
      'mac_toolchain', 'TestXcodeVersion', 'test/path/Xcode.app'
    )
    self.assertFalse(
      mock_install_runtime.called, '_install_runtime shouldn\'t be called'
    )
    self.assertFalse(
      mock_move_runtime.called, 'move_runtime shouldn\'t be called'
    )

  @mock.patch('xcode_util.move_runtime', autospec=True)
  @mock.patch('xcode_util._install_runtime')
  @mock.patch('xcode_util._install_xcode')
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  def test_new_mactoolchain_new_xcode(
    self,
    mock_macos_13_or_higher,
    mock_install_xcode,
    mock_install_runtime,
    mock_move_runtime,
  ):
    mock_macos_13_or_higher.return_value = False
    self.mock(xcode_util, '_is_legacy_xcode_package', lambda path: False)

    is_legacy_xcode = xcode_util.install(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      runtime_cache_folder=self.runtime_cache_folder,
      ios_version=self.platform_version,
    )

    self.assertFalse(is_legacy_xcode, 'install should return False')
    mock_install_xcode.assert_called_with(
      'mac_toolchain', 'TestXcodeVersion', 'test/path/Xcode.app'
    )
    mock_install_runtime.assert_called_with(
      'mac_toolchain', 'test/path/Runtime', 'TestXcodeVersion', '14.4'
    )
    mock_move_runtime.assert_called_with(
      'test/path/Runtime', 'test/path/Xcode.app'
    )

  @mock.patch('xcode_util.move_runtime', autospec=True)
  @mock.patch('xcode_util._install_runtime')
  @mock.patch('xcode_util._install_xcode')
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  def test_new_mactoolchain_new_xcode_no_runtime(
    self,
    mock_macos_13_or_higher,
    mock_install_xcode,
    mock_install_runtime,
    mock_move_runtime,
  ):
    mock_macos_13_or_higher.return_value = False
    self.mock(xcode_util, '_is_legacy_xcode_package', lambda path: False)

    is_legacy_xcode = xcode_util.install(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      runtime_cache_folder=None,
      ios_version=None,
    )

    self.assertFalse(is_legacy_xcode, 'install should return False')
    mock_install_xcode.assert_called_with(
      'mac_toolchain', 'TestXcodeVersion', 'test/path/Xcode.app'
    )
    self.assertFalse(mock_install_runtime.called)
    self.assertFalse(mock_move_runtime.called)

  @mock.patch('xcode_util.move_runtime', autospec=True)
  @mock.patch('xcode_util._install_runtime')
  @mock.patch('xcode_util._install_xcode')
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  def test_new_mactoolchain_new_xcode_macos13_good_xcode_cache(
    self,
    mock_os_path_exists,
    mock_macos_13_or_higher,
    mock_install_xcode,
    mock_install_runtime,
    mock_move_runtime,
  ):
    mock_macos_13_or_higher.return_value = True
    mock_os_path_exists.return_value = False
    self.mock(xcode_util, '_is_legacy_xcode_package', lambda path: False)

    is_legacy_xcode = xcode_util.install(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      runtime_cache_folder=None,
      ios_version=None,
    )

    self.assertTrue(is_legacy_xcode, 'install should return True')
    mock_install_xcode.assert_called_with(
      'mac_toolchain', 'TestXcodeVersion', 'test/path/Xcode.app'
    )
    self.assertFalse(mock_install_runtime.called)
    self.assertFalse(mock_move_runtime.called)

  @mock.patch('xcode_util.move_runtime', autospec=True)
  @mock.patch('xcode_util._install_runtime')
  @mock.patch('xcode_util._install_xcode')
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('os.mkdir', autospec=True)
  def test_new_mactoolchain_new_xcode_macos13_codesign_failed(
    self,
    mock_mkdir,
    mock_rmtree,
    mock_os_path_exists,
    mock_macos_13_or_higher,
    mock_install_xcode,
    mock_install_runtime,
    mock_move_runtime,
  ):
    mock_macos_13_or_higher.return_value = True
    mock_os_path_exists.return_value = True
    self.mock(xcode_util, '_is_legacy_xcode_package', lambda path: False)

    is_legacy_xcode = xcode_util.install(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      runtime_cache_folder=None,
      ios_version=None,
    )

    mock_rmtree.assert_called_with(self.xcode_app_path)
    mock_mkdir.assert_called_with(self.xcode_app_path)
    self.assertTrue(is_legacy_xcode, 'install should return True')
    mock_install_xcode.assert_called_with(
      'mac_toolchain', 'TestXcodeVersion', 'test/path/Xcode.app'
    )
    self.assertFalse(mock_install_runtime.called)
    self.assertFalse(mock_move_runtime.called)

  def test_install_runtime_dmg_with_builtin_runtime(self):
    with mock.patch('xcode_util.is_runtime_builtin', return_value=True):
      with mock.patch(
        'xcode_util._install_runtime_dmg'
      ) as mock__install_runtime_dmg:
        with mock.patch(
          'iossim_util.add_simulator_runtime'
        ) as mock_add_simulator_runtime:
          with mock.patch(
            'iossim_util.override_default_iphonesim_runtime'
          ) as mock_override_default_iphonesim_runtime:
            result = xcode_util.install_runtime_dmg(
              mac_toolchain='mac_toolchain',
              runtime_cache_folder='/path/to/runtime_cache_folder',
              platform_type=constants.IOSPlatformType.IPHONEOS,
              platform_version='15.0',
              xcode_build_version='14a123',
            )

    self.assertFalse(mock__install_runtime_dmg.called)
    self.assertFalse(mock_add_simulator_runtime.called)
    self.assertFalse(mock_override_default_iphonesim_runtime.called)

  @mock.patch('xcode_util.using_xcode_16_or_higher', return_value=False)
  @mock.patch('iossim_util.get_simulator_runtime_info', return_value=None)
  def test_install_runtime_dmg_with_non_builtin_runtime(self, _, __):
    with mock.patch('xcode_util.is_runtime_builtin', return_value=False):
      with mock.patch(
        'iossim_util.delete_least_recently_used_simulator_runtimes'
      ) as mock_delete_least_recently_used_simulator_runtimes:
        with mock.patch(
          'xcode_util.get_latest_runtime_build_cipd', return_value='20C52'
        ) as mock_get_latest_runtime_build_cipd:
          with mock.patch(
            'iossim_util.get_simulator_runtime_info_by_build', return_value=None
          ) as mock_get_simulator_runtime_info_by_build:
            with mock.patch(
              'xcode_util._install_runtime_dmg'
            ) as mock__install_runtime_dmg:
              with mock.patch(
                'iossim_util.add_simulator_runtime',
                return_value=ADD_SIMULATOR_RUNTIME_OUTPUT,
              ) as mock_add_simulator_runtime:
                with mock.patch(
                  'xcode_util.get_runtime_dmg_name',
                  return_value='/path/to/runtime_cache_folder/test.dmg',
                ):
                  with mock.patch(
                    'iossim_util.override_default_iphonesim_runtime'
                  ) as mock_override_default_iphonesim_runtime:
                    with mock.patch('os.environ.get', return_value=True):
                      with mock.patch(
                        'iossim_util.delete_stale_simulator_runtimes'
                      ) as mock_delete_stale_simulator_runtimes:
                        result = xcode_util.install_runtime_dmg(
                          mac_toolchain='mac_toolchain',
                          runtime_cache_folder='/path/to/runtime_cache_folder',
                          platform_type=constants.IOSPlatformType.IPHONEOS,
                          platform_version='15.0',
                          xcode_build_version='15a123',
                        )

    mock_delete_least_recently_used_simulator_runtimes.assert_called_once_with()
    mock_delete_stale_simulator_runtimes.assert_called_once_with()
    mock_get_simulator_runtime_info_by_build.assert_called_once_with('20C52')
    mock__install_runtime_dmg.assert_called_once_with(
      'mac_toolchain',
      '/path/to/runtime_cache_folder',
      constants.IOSPlatformType.IPHONEOS,
      '15.0',
      '15a123',
    )
    mock_add_simulator_runtime.assert_called_once_with(
      '/path/to/runtime_cache_folder/test.dmg'
    )
    mock_override_default_iphonesim_runtime.assert_called_once_with(
      ADD_SIMULATOR_RUNTIME_OUTPUT, '15.0'
    )

  @mock.patch('xcode_util.using_xcode_16_or_higher', return_value=False)
  def test_install_runtime_dmg_duplicate_ios_version(self, _):
    with mock.patch('xcode_util.is_runtime_builtin', return_value=False):
      with mock.patch(
        'iossim_util.delete_least_recently_used_simulator_runtimes'
      ) as mock_delete_least_recently_used_simulator_runtimes:
        with mock.patch(
          'xcode_util.get_latest_runtime_build_cipd', return_value='20C52'
        ) as mock_get_latest_runtime_build_cipd:
          with mock.patch(
            'iossim_util.get_simulator_runtime_info_by_build', return_value=None
          ) as mock_get_simulator_runtime_info_by_build:
            with mock.patch(
              'xcode_util._install_runtime_dmg'
            ) as mock__install_runtime_dmg:
              with mock.patch(
                'iossim_util.add_simulator_runtime',
                return_value=ADD_SIMULATOR_RUNTIME_OUTPUT,
              ) as mock_add_simulator_runtime:
                with mock.patch(
                  'xcode_util.get_runtime_dmg_name',
                  return_value='/path/to/runtime_cache_folder/test.dmg',
                ):
                  with mock.patch(
                    'iossim_util.override_default_iphonesim_runtime'
                  ) as mock_override_default_iphonesim_runtime:
                    with mock.patch('os.environ.get', return_value=True):
                      with mock.patch(
                        'iossim_util.delete_stale_simulator_runtimes'
                      ) as mock_delete_stale_simulator_runtimes:
                        with mock.patch(
                          'iossim_util.get_simulator_runtime_info'
                        ) as mock_get_simulator_runtime_info:
                          with mock.patch(
                            'iossim_util.delete_simulator_runtime'
                          ) as mock_delete_simulator_runtime:
                            mock_get_simulator_runtime_info.return_value = (
                              RUNTIME_15_0
                            )
                            result = xcode_util.install_runtime_dmg(
                              mac_toolchain='mac_toolchain',
                              runtime_cache_folder='/path/to/runtime_cache_folder',
                              platform_type=constants.IOSPlatformType.IPHONEOS,
                              platform_version='15.0',
                              xcode_build_version='15a123',
                            )

    mock_delete_least_recently_used_simulator_runtimes.assert_called_once_with()
    mock_delete_stale_simulator_runtimes.assert_called_once_with()
    mock_get_simulator_runtime_info_by_build.assert_called_once_with('20C52')
    mock_get_simulator_runtime_info.assert_called_with(
      constants.IOSPlatformType.IPHONEOS, '15.0'
    )
    mock_delete_simulator_runtime.assert_called_once_with(
      RUNTIME_15_0['identifier'], True
    )
    mock__install_runtime_dmg.assert_called_once_with(
      'mac_toolchain',
      '/path/to/runtime_cache_folder',
      constants.IOSPlatformType.IPHONEOS,
      '15.0',
      '15a123',
    )
    mock_add_simulator_runtime.assert_called_once_with(
      '/path/to/runtime_cache_folder/test.dmg'
    )
    mock_override_default_iphonesim_runtime.assert_called_once_with(
      ADD_SIMULATOR_RUNTIME_OUTPUT, '15.0'
    )

  @mock.patch('xcode_util.using_xcode_16_or_higher', return_value=False)
  def test_install_runtime_dmg_already_exists(self, _):
    with mock.patch('xcode_util.is_runtime_builtin', return_value=False):
      with mock.patch(
        'iossim_util.delete_least_recently_used_simulator_runtimes'
      ) as mock_delete_least_recently_used_simulator_runtimes:
        with mock.patch(
          'xcode_util.get_latest_runtime_build_cipd', return_value='20C52'
        ) as mock_get_latest_runtime_build_cipd:
          with mock.patch(
            'iossim_util.get_simulator_runtime_info_by_build',
            return_value=RUNTIME_15_0,
          ) as mock_get_simulator_runtime_info_by_build:
            with mock.patch(
              'xcode_util._install_runtime_dmg'
            ) as mock__install_runtime_dmg:
              with mock.patch('os.environ.get', return_value=True):
                result = xcode_util.install_runtime_dmg(
                  mac_toolchain='mac_toolchain',
                  runtime_cache_folder='/path/to/runtime_cache_folder',
                  platform_type=constants.IOSPlatformType.IPHONEOS,
                  platform_version='15.0',
                  xcode_build_version='15a123',
                )

    mock_delete_least_recently_used_simulator_runtimes.assert_not_called()
    mock_get_simulator_runtime_info_by_build.assert_called_once_with('20C52')
    mock__install_runtime_dmg.assert_not_called()

  @mock.patch('xcode_util.ensure_xcode_ready_in_apps')
  @mock.patch('xcode_util.is_local_run', return_value=True)
  @mock.patch('xcode_util.version', return_value=('', 'TestXcodeVersion'))
  @mock.patch(
    'iossim_util.get_simulator_runtime_info_by_build', return_value=object()
  )
  @mock.patch(
    'iossim_util.get_platform_type_by_platform',
    return_value=constants.IOSPlatformType.IPHONEOS,
  )
  @mock.patch('xcode_util.get_latest_runtime_build_cipd', return_value=object())
  @mock.patch('xcode_util.install')
  def test_local_run(
    self,
    mock_install,
    mock_get_latest_runtime_build_cipd,
    _1,
    _2,
    _3,
    mock_is_local_run,
    mock_ensure_xcode_ready_in_apps,
  ):
    platform_version = '14.4'

    install_success = xcode_util.install_xcode(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      '',
      'iPhone 11',
      platform_version,
    )
    self.assertTrue(install_success)
    mock_get_latest_runtime_build_cipd.assert_called_once_with(
      self.xcode_build_version,
      constants.IOSPlatformType.IPHONEOS,
      platform_version,
    )
    self.assertFalse(mock_install.called)
    mock_ensure_xcode_ready_in_apps.assert_not_called()

  @mock.patch('xcode_util.ensure_xcode_ready_in_apps')
  @mock.patch('xcode_util.is_local_run', return_value=True)
  @mock.patch('xcode_util.version', return_value=('', 'TestXcodeVersion'))
  @mock.patch(
    'iossim_util.get_simulator_runtime_info_by_build', return_value=object()
  )
  @mock.patch('iossim_util.get_platform_type_by_platform', autospec=True)
  @mock.patch('xcode_util.get_latest_runtime_build_cipd', return_value=None)
  @mock.patch('xcode_util.install')
  def test_local_run_no_cipd_runtime(
    self,
    mock_install,
    mock_get_latest_runtime_build_cipd,
    mock_get_platform_type_by_platform,
    _1,
    _2,
    mock_is_local_run,
    mock_ensure_xcode_ready_in_apps,
  ):
    platform_version = '14.4'

    mock_get_platform_type_by_platform.return_value = (
      constants.IOSPlatformType.IPHONEOS
    )
    install_success = xcode_util.install_xcode(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      '',
      'iPhone 11',
      platform_version,
    )
    self.assertTrue(install_success)
    mock_get_latest_runtime_build_cipd.assert_called_once_with(
      self.xcode_build_version,
      constants.IOSPlatformType.IPHONEOS,
      platform_version,
    )
    self.assertFalse(mock_install.called)
    mock_ensure_xcode_ready_in_apps.assert_not_called()

    mock_get_platform_type_by_platform.return_value = (
      constants.IOSPlatformType.TVOS
    )
    install_success = xcode_util.install_xcode(
      self.mac_toolchain,
      self.xcode_build_version,
      self.xcode_app_path,
      '',
      'Apple TV 4K',
      platform_version,
    )
    self.assertTrue(install_success)
    self.assertEqual(2, mock_get_latest_runtime_build_cipd.call_count)
    mock_get_latest_runtime_build_cipd.assert_called_with(
      self.xcode_build_version, constants.IOSPlatformType.TVOS, platform_version
    )
    self.assertFalse(mock_install.called)
    mock_ensure_xcode_ready_in_apps.assert_not_called()


class HelperFunctionTests(XcodeUtilTest):
  """Test class for xcode_util misc util functions."""

  def setUp(self):
    super(HelperFunctionTests, self).setUp()
    self.xcode_runtime_dir_rel_path = (
      'Contents/Developer/'
      'Platforms/iPhoneOS.platform/Library/Developer/'
      'CoreSimulator/Profiles/Runtimes'
    )
    self.xcode_runtime_rel_path = (
      'Contents/Developer/'
      'Platforms/iPhoneOS.platform/Library/Developer/'
      'CoreSimulator/Profiles/Runtimes/iOS.simruntime'
    )

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  def test_is_legacy_xcode_package_legacy(self, mock_glob, mock_rmtree):
    test_xcode_path = 'test/path/Xcode.app/'
    runtime_names = ['iOS.simruntime', 'iOS 12.4.simruntime']
    xcode_runtime_paths = [
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, runtime_name
      )
      for runtime_name in runtime_names
    ]
    mock_glob.return_value = xcode_runtime_paths
    self.assertTrue(xcode_util._is_legacy_xcode_package(test_xcode_path))
    mock_glob.assert_called_with(
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, '*.simruntime'
      )
    )
    self.assertFalse(mock_rmtree.called)

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  def test_is_legacy_xcode_package_no_runtime(
    self, mock_macos_13_or_higher, mock_glob, mock_rmtree
  ):
    mock_macos_13_or_higher.return_value = False
    test_xcode_path = 'test/path/Xcode.app/'
    xcode_runtime_paths = []
    mock_glob.return_value = xcode_runtime_paths
    self.assertFalse(xcode_util._is_legacy_xcode_package(test_xcode_path))
    mock_glob.assert_called_with(
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, '*.simruntime'
      )
    )
    self.assertFalse(mock_rmtree.called)

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  @mock.patch('mac_util.is_macos_13_or_higher', autospec=True)
  def test_is_legacy_xcode_package_single_runtime(
    self, mock_macos_13_or_higher, mock_glob, mock_rmtree
  ):
    mock_macos_13_or_higher.return_value = False
    test_xcode_path = 'test/path/Xcode.app/'
    runtime_names = ['iOS.simruntime']
    xcode_runtime_paths = [
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, runtime_name
      )
      for runtime_name in runtime_names
    ]
    mock_glob.return_value = xcode_runtime_paths
    self.assertFalse(xcode_util._is_legacy_xcode_package(test_xcode_path))
    mock_glob.assert_called_with(
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, '*.simruntime'
      )
    )
    mock_rmtree.assert_called_with(
      os.path.join(
        test_xcode_path, self.xcode_runtime_dir_rel_path, 'iOS.simruntime'
      )
    )

  def test_is_runtime_builtin_with_builtin_runtime(self):
    with mock.patch(
      'iossim_util.get_simulator_runtime_info',
      return_value={
        'kind': 'Bundled with Xcode',
      },
    ):
      result = xcode_util.is_runtime_builtin(
        constants.IOSPlatformType.IPHONEOS, '15.0'
      )

    self.assertTrue(result)

  def test_is_runtime_builtin_with_non_builtin_runtime(self):
    with mock.patch(
      'iossim_util.get_simulator_runtime_info',
      return_value={
        'kind': 'Disk Image',
      },
    ):
      result = xcode_util.is_runtime_builtin(
        constants.IOSPlatformType.IPHONEOS, '15.0'
      )

    self.assertFalse(result)

  def test_is_runtime_builtin_with_no_runtime_info(self):
    with mock.patch(
      'iossim_util.get_simulator_runtime_info', return_value=None
    ):
      result = xcode_util.is_runtime_builtin(
        constants.IOSPlatformType.IPHONEOS, '15.0'
      )

    self.assertFalse(result)

  def test_convert_platform_version_to_cipd_ref(self):
    self.assertEqual(
      "ios-14-4",
      xcode_util.convert_platform_version_to_cipd_ref(
        constants.IOSPlatformType.IPHONEOS, "14.4"
      ),
    )
    self.assertEqual(
      "tvos-14-4",
      xcode_util.convert_platform_version_to_cipd_ref(
        constants.IOSPlatformType.TVOS, "14.4"
      ),
    )

  def test_get_latest_runtime_build_cipd_with_ios_version(self):
    output1 = """
      Tags:
        ios_runtime_build:21C62
        ios_runtime_version:ios-17-2"""
    output2 = """
      Tags:
        ios_runtime_build:21A342
        ios_runtime_version:ios-15-0"""
    with mock.patch('xcode_util.describe_cipd_ref') as mock_describe_cipd_ref:
      mock_describe_cipd_ref.side_effect = [output1, output2]
      result = xcode_util.get_latest_runtime_build_cipd(
        '14c18', constants.IOSPlatformType.IPHONEOS, '15.0'
      )
    self.assertEqual(result, '21A342')

  def test_get_latest_runtime_build_cipd_with_xcode_version(self):
    with mock.patch(
      'xcode_util.describe_cipd_ref', return_value='ios_runtime_build:21A342'
    ):
      result = xcode_util.get_latest_runtime_build_cipd(
        '14c18', constants.IOSPlatformType.IPHONEOS, '15.0'
      )
    self.assertEqual(result, '21A342')

  def test_get_latest_runtime_build_cipd_return_no_match(self):
    with mock.patch('xcode_util.describe_cipd_ref') as mock_describe_cipd_ref:
      mock_describe_cipd_ref.side_effect = ['', '']
      result_ios = xcode_util.get_latest_runtime_build_cipd(
        '14c18', constants.IOSPlatformType.IPHONEOS, '15.0'
      )
      mock_describe_cipd_ref.side_effect = ['', '']
      result_tvos = xcode_util.get_latest_runtime_build_cipd(
        '14c18', constants.IOSPlatformType.TVOS, '15.0'
      )
    self.assertIsNone(result_ios)
    self.assertIsNone(result_tvos)

  @mock.patch('os.path.exists')
  def test_check_xcode_exists_in_apps_exists(self, mock_exists):
    mock_exists.return_value = True
    result = xcode_util.check_xcode_exists_in_apps("16f6")
    expected_path = "/Applications/xcode_16f6.app"
    mock_exists.assert_called_once_with(expected_path)
    self.assertTrue(result)

  @mock.patch('os.path.exists')
  def test_check_xcode_exists_in_apps_not_exist(self, mock_exists):
    mock_exists.return_value = False
    result = xcode_util.check_xcode_exists_in_apps("15.0")
    expected_path = "/Applications/xcode_15.0.app"
    mock_exists.assert_called_once_with(expected_path)
    self.assertFalse(result)

  @mock.patch('xcode_util.check_xcode_exists_in_apps')
  @mock.patch('xcode_util.select')
  def test_ensure_xcode_ready_app_not_found(
    self, mock_select, mock_check_xcode
  ):
    """Test case when the requested Xcode app does not exist."""
    mock_check_xcode.return_value = False

    xcode_util.ensure_xcode_ready_in_apps('123')

    mock_check_xcode.assert_called_once_with('123')
    mock_select.assert_not_called()

  @mock.patch('xcode_util.check_xcode_exists_in_apps')
  @mock.patch('xcode_util.select')
  def test_ensure_xcode_ready_app_found(self, mock_select, mock_check_xcode):
    """Test case when the requested Xcode app exists."""
    mock_check_xcode.return_value = True

    xcode_util.ensure_xcode_ready_in_apps('123')

    mock_check_xcode.assert_called_once_with('123')
    mock_select.assert_called_once_with('/Applications/xcode_123.app')


class MoveRuntimeTests(XcodeUtilTest):
  """Test class for xcode_util.move_runtime function."""

  def setUp(self):
    super(MoveRuntimeTests, self).setUp()
    self.runtime_cache_folder = 'test/path/Runtime'
    self.xcode_app_path = 'test/path/Xcode.app'

  @mock.patch('shutil.move', autospec=True)
  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  def test_move_runtime_into_xcode(self, mock_glob, mock_rmtree, mock_move):

    mock_glob.side_effect = [['test/path/Runtime/iOS.simruntime'], []]

    xcode_util.move_runtime(self.runtime_cache_folder, self.xcode_app_path)

    xcode_runtime_path = (
      'test/path/Xcode.app/Contents/Developer/'
      'Platforms/iPhoneOS.platform/Library/Developer/'
      'CoreSimulator/Profiles/Runtimes/iOS.simruntime'
    )
    calls = [
      mock.call('test/path/Runtime/*.simruntime'),
      mock.call(
        (
          'test/path/Xcode.app/Contents/Developer/'
          'Platforms/iPhoneOS.platform/Library/Developer/'
          'CoreSimulator/Profiles/Runtimes/*.simruntime'
        )
      ),
    ]
    mock_glob.assert_has_calls(calls)
    self.assertFalse(mock_rmtree.called)
    mock_move.assert_called_with(
      'test/path/Runtime/iOS.simruntime', xcode_runtime_path
    )

  @mock.patch('shutil.move', autospec=True)
  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  def test_move_runtime_multiple_in_src(
    self, mock_glob, mock_rmtree, mock_move
  ):
    mock_glob.side_effect = [
      [
        'test/path/Runtime/iOS.simruntime',
        'test/path/Runtime/iOS 13.4.simruntime',
      ],
      [],
    ]

    with self.assertRaises(test_runner_errors.IOSRuntimeHandlingError):
      xcode_util.move_runtime(self.runtime_cache_folder, self.xcode_app_path)
    mock_glob.assert_called_with('test/path/Runtime/*.simruntime')
    self.assertFalse(mock_rmtree.called)
    self.assertFalse(mock_move.called)

  @mock.patch('shutil.move', autospec=True)
  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  def test_move_runtime_remove_from_dst(
    self, mock_glob, mock_rmtree, mock_move
  ):

    mock_glob.side_effect = [
      ['test/path/Runtime/iOS.simruntime'],
      [
        (
          'test/path/Xcode.app/Contents/Developer/'
          'Platforms/iPhoneOS.platform/Library/Developer/'
          'CoreSimulator/Profiles/Runtimes/iOS.simruntime'
        )
      ],
    ]

    xcode_util.move_runtime(self.runtime_cache_folder, self.xcode_app_path)

    xcode_runtime_path = (
      'test/path/Xcode.app/Contents/Developer/'
      'Platforms/iPhoneOS.platform/Library/Developer/'
      'CoreSimulator/Profiles/Runtimes/iOS.simruntime'
    )
    calls = [
      mock.call('test/path/Runtime/*.simruntime'),
      mock.call(
        (
          'test/path/Xcode.app/Contents/Developer/'
          'Platforms/iPhoneOS.platform/Library/Developer/'
          'CoreSimulator/Profiles/Runtimes/*.simruntime'
        )
      ),
    ]
    mock_glob.assert_has_calls(calls)
    mock_rmtree.assert_called_with(xcode_runtime_path)
    mock_move.assert_called_with(
      'test/path/Runtime/iOS.simruntime', xcode_runtime_path
    )


class MacToolchainInvocationTests(XcodeUtilTest):
  """Test class for xcode_util functions invoking mac_toolchain."""

  def setUp(self):
    super(MacToolchainInvocationTests, self).setUp()
    self.mac_toolchain = 'mac_toolchain'
    self.xcode_build_version = 'TestXcodeVersion'
    self.xcode_app_path = 'test/path/Xcode.app'
    self.runtime_cache_folder = 'test/path/Runtime'
    self.platform_version = '14.4'

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  @mock.patch('subprocess.check_call', autospec=True)
  def test_install_runtime_no_cache(
    self, mock_check_output, mock_glob, mock_exists, mock_rmtree
  ):
    mock_glob.return_value = []
    mock_exists.return_value = False

    xcode_util._install_runtime(
      self.mac_toolchain,
      self.runtime_cache_folder,
      self.xcode_build_version,
      self.platform_version,
    )

    mock_glob.assert_called_with('test/path/Runtime/*.simruntime')
    calls = [
      mock.call('test/path/Runtime/.cipd'),
      mock.call('test/path/Runtime/.xcode_versions'),
    ]
    mock_exists.assert_has_calls(calls)
    self.assertFalse(mock_rmtree.called)
    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install-runtime',
        '-xcode-version',
        'testxcodeversion',
        '-runtime-version',
        'ios-14-4',
        '-output-dir',
        'test/path/Runtime',
      ],
      stderr=-2,
    )

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  @mock.patch('subprocess.check_call', autospec=True)
  def test_install_runtime_has_cache(
    self, mock_check_output, mock_glob, mock_exists, mock_rmtree
  ):
    mock_glob.return_value = ['test/path/Runtime/iOS.simruntime']
    xcode_util._install_runtime(
      self.mac_toolchain,
      self.runtime_cache_folder,
      self.xcode_build_version,
      self.platform_version,
    )

    mock_glob.assert_called_with('test/path/Runtime/*.simruntime')
    self.assertFalse(mock_exists.called)
    self.assertFalse(mock_rmtree.called)
    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install-runtime',
        '-xcode-version',
        'testxcodeversion',
        '-runtime-version',
        'ios-14-4',
        '-output-dir',
        'test/path/Runtime',
      ],
      stderr=-2,
    )

  @mock.patch('shutil.rmtree', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('glob.glob', autospec=True)
  @mock.patch('subprocess.check_call', autospec=True)
  def test_install_runtime_incorrect_cache(
    self, mock_check_output, mock_glob, mock_exists, mock_rmtree
  ):
    mock_glob.return_value = []
    mock_exists.return_value = True

    xcode_util._install_runtime(
      self.mac_toolchain,
      self.runtime_cache_folder,
      self.xcode_build_version,
      self.platform_version,
    )

    mock_glob.assert_called_with('test/path/Runtime/*.simruntime')
    calls = [
      mock.call('test/path/Runtime/.cipd'),
      mock.call('test/path/Runtime/.xcode_versions'),
    ]
    mock_exists.assert_has_calls(calls)
    mock_rmtree.assert_has_calls(calls)

    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install-runtime',
        '-xcode-version',
        'testxcodeversion',
        '-runtime-version',
        'ios-14-4',
        '-output-dir',
        'test/path/Runtime',
      ],
      stderr=-2,
    )

  @mock.patch('subprocess.check_call', autospec=True)
  def test_install_xcode_new_mac_toolchain(self, mock_check_output):
    xcode_util._install_xcode(
      self.mac_toolchain, self.xcode_build_version, self.xcode_app_path
    )
    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install',
        '-kind',
        'ios',
        '-xcode-version',
        'testxcodeversion',
        '-output-dir',
        'test/path/Xcode.app',
        '-with-runtime=False',
      ],
      stderr=-2,
    )

  @mock.patch('subprocess.check_call', autospec=True)
  def test_install_runtime_dmg(self, mock_check_output):
    xcode_util._install_runtime_dmg(
      self.mac_toolchain,
      self.runtime_cache_folder,
      constants.IOSPlatformType.IPHONEOS,
      self.platform_version,
      self.xcode_build_version,
    )

    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install-runtime-dmg',
        '-runtime-version',
        'ios-14-4',
        '-runtime-type',
        'ios',
        '-xcode-version',
        self.xcode_build_version,
        '-output-dir',
        'test/path/Runtime',
      ],
      stderr=-2,
    )
    xcode_util._install_runtime_dmg(
      self.mac_toolchain,
      self.runtime_cache_folder,
      constants.IOSPlatformType.TVOS,
      self.platform_version,
      self.xcode_build_version,
    )

    mock_check_output.assert_called_with(
      [
        'mac_toolchain',
        'install-runtime-dmg',
        '-runtime-version',
        'tvos-14-4',
        '-runtime-type',
        'tvos',
        '-xcode-version',
        self.xcode_build_version,
        '-output-dir',
        'test/path/Runtime',
      ],
      stderr=-2,
    )


if __name__ == '__main__':
  logging.basicConfig(
    format='[%(asctime)s:%(levelname)s] %(message)s',
    level=logging.DEBUG,
    datefmt='%I:%M:%S',
  )
  unittest.main()
