1# Copyright 2019 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import json
6import logging
7import subprocess
8
9import test_runner
10
11LOGGER = logging.getLogger(__name__)
12
13
14def _compose_simulator_name(platform, version):
15  """Composes the name of simulator of platform and version strings."""
16  return '%s %s test simulator' % (platform, version)
17
18
19def get_simulator_list():
20  """Gets list of available simulator as a dictionary."""
21  return json.loads(subprocess.check_output(['xcrun', 'simctl', 'list', '-j']))
22
23
24def get_simulator(platform, version):
25  """Gets a simulator or creates a new one if not exist by platform and version.
26
27  Args:
28    platform: (str) A platform name, e.g. "iPhone 11 Pro"
29    version: (str) A version name, e.g. "13.4"
30
31  Returns:
32    A udid of a simulator device.
33  """
34  udids = get_simulator_udids_by_platform_and_version(platform, version)
35  if udids:
36    return udids[0]
37  return create_device_by_platform_and_version(platform, version)
38
39
40def get_simulator_device_type_by_platform(simulators, platform):
41  """Gets device type identifier for platform.
42
43  Args:
44    simulators: (dict) A list of available simulators.
45    platform: (str) A platform name, e.g. "iPhone 11 Pro"
46
47  Returns:
48    Simulator device type identifier string of the platform.
49    e.g. 'com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro'
50
51  Raises:
52    test_runner.SimulatorNotFoundError when the platform can't be found.
53  """
54  for devicetype in simulators['devicetypes']:
55    if devicetype['name'] == platform:
56      return devicetype['identifier']
57  raise test_runner.SimulatorNotFoundError(
58      'Not found device "%s" in devicetypes %s' %
59      (platform, simulators['devicetypes']))
60
61
62def get_simulator_runtime_by_version(simulators, version):
63  """Gets runtime based on iOS version.
64
65  Args:
66    simulators: (dict) A list of available simulators.
67    version: (str) A version name, e.g. "13.4"
68
69  Returns:
70    Simulator runtime identifier string of the version.
71    e.g. 'com.apple.CoreSimulator.SimRuntime.iOS-13-4'
72
73  Raises:
74    test_runner.SimulatorNotFoundError when the version can't be found.
75  """
76  for runtime in simulators['runtimes']:
77    if runtime['version'] == version and 'iOS' in runtime['name']:
78      return runtime['identifier']
79  raise test_runner.SimulatorNotFoundError('Not found "%s" SDK in runtimes %s' %
80                                           (version, simulators['runtimes']))
81
82
83def get_simulator_runtime_by_device_udid(simulator_udid):
84  """Gets simulator runtime based on simulator UDID.
85
86  Args:
87    simulator_udid: (str) UDID of a simulator.
88  """
89  simulator_list = get_simulator_list()['devices']
90  for runtime, simulators in simulator_list.items():
91    for device in simulators:
92      if simulator_udid == device['udid']:
93        return runtime
94  raise test_runner.SimulatorNotFoundError(
95      'Not found simulator with "%s" UDID in devices %s' % (simulator_udid,
96                                                            simulator_list))
97
98
99def get_simulator_udids_by_platform_and_version(platform, version):
100  """Gets list of simulators UDID based on platform name and iOS version.
101
102    Args:
103      platform: (str) A platform name, e.g. "iPhone 11"
104      version: (str) A version name, e.g. "13.2.2"
105  """
106  simulators = get_simulator_list()
107  devices = simulators['devices']
108  sdk_id = get_simulator_runtime_by_version(simulators, version)
109  results = []
110  for device in devices.get(sdk_id, []):
111    if device['name'] == _compose_simulator_name(platform, version):
112      results.append(device['udid'])
113  return results
114
115
116def create_device_by_platform_and_version(platform, version):
117  """Creates a simulator and returns UDID of it.
118
119    Args:
120      platform: (str) A platform name, e.g. "iPhone 11"
121      version: (str) A version name, e.g. "13.2.2"
122  """
123  name = _compose_simulator_name(platform, version)
124  LOGGER.info('Creating simulator %s', name)
125  simulators = get_simulator_list()
126  device_type = get_simulator_device_type_by_platform(simulators, platform)
127  runtime = get_simulator_runtime_by_version(simulators, version)
128  try:
129    udid = subprocess.check_output(
130        ['xcrun', 'simctl', 'create', name, device_type, runtime],
131        stderr=subprocess.STDOUT).rstrip()
132    LOGGER.info('Created simulator in first attempt with UDID: %s', udid)
133    # Sometimes above command fails to create a simulator. Verify it and retry
134    # once if first attempt failed.
135    if not is_device_with_udid_simulator(udid):
136      # Try to delete once to avoid duplicate in case of race condition.
137      delete_simulator_by_udid(udid)
138      udid = subprocess.check_output(
139          ['xcrun', 'simctl', 'create', name, device_type, runtime],
140          stderr=subprocess.STDOUT).rstrip()
141      LOGGER.info('Created simulator in second attempt with UDID: %s', udid)
142    return udid
143  except subprocess.CalledProcessError as e:
144    LOGGER.error('Error when creating simulator "%s": %s' % (name, e.output))
145    raise e
146
147
148def delete_simulator_by_udid(udid):
149  """Deletes simulator by its udid.
150
151  Args:
152    udid: (str) UDID of simulator.
153  """
154  LOGGER.info('Deleting simulator %s', udid)
155  try:
156    subprocess.check_output(['xcrun', 'simctl', 'delete', udid],
157                            stderr=subprocess.STDOUT)
158  except subprocess.CalledProcessError as e:
159    # Logging error instead of throwing so we don't cause failures in case
160    # this was indeed failing to clean up.
161    message = 'Failed to delete simulator %s with error %s' % (udid, e.output)
162    LOGGER.error(message)
163
164
165def wipe_simulator_by_udid(udid):
166  """Wipes simulators by its udid.
167
168  Args:
169    udid: (str) UDID of simulator.
170  """
171  for _, devices in get_simulator_list()['devices'].items():
172    for device in devices:
173      if device['udid'] != udid:
174        continue
175      try:
176        LOGGER.info('Shutdown simulator %s ', device)
177        if device['state'] != 'Shutdown':
178          subprocess.check_call(['xcrun', 'simctl', 'shutdown', device['udid']])
179      except subprocess.CalledProcessError as ex:
180        LOGGER.error('Shutdown failed %s ', ex)
181      subprocess.check_call(['xcrun', 'simctl', 'erase', device['udid']])
182
183
184def get_home_directory(platform, version):
185  """Gets directory where simulators are stored.
186
187  Args:
188    platform: (str) A platform name, e.g. "iPhone 11"
189    version: (str) A version name, e.g. "13.2.2"
190  """
191  return subprocess.check_output(
192      ['xcrun', 'simctl', 'getenv',
193       get_simulator(platform, version), 'HOME']).rstrip()
194
195
196def is_device_with_udid_simulator(device_udid):
197  """Checks whether a device with udid is simulator or not.
198
199  Args:
200    device_udid: (str) UDID of a device.
201  """
202  simulator_list = get_simulator_list()['devices']
203  for _, simulators in simulator_list.items():
204    for device in simulators:
205      if device_udid == device['udid']:
206        return True
207  return False
208