forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 1
/
iossim_util.py
621 lines (506 loc) · 20.3 KB
/
iossim_util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# 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.
import json
import logging
import os
import subprocess
import time
import typing
import constants
import test_runner
import test_runner_errors
import mac_util
from collections import OrderedDict
LOGGER = logging.getLogger(__name__)
MAX_WAIT_TIME_TO_DELETE_RUNTIME = 45 # 45 seconds
SIMULATOR_DEFAULT_PATH = os.path.expanduser(
'~/Library/Developer/CoreSimulator/Devices')
# TODO(crbug.com/1441931): remove Legacy Download once iOS 15.5 is deprecated
IOS_SIM_RUNTIME_BUILTIN_STATE = ['Legacy Download', 'Bundled with Xcode']
def _compose_simulator_name(platform, version):
"""Composes the name of simulator of platform and version strings."""
return '%s %s test simulator' % (platform, version)
def get_simulator_list(path=SIMULATOR_DEFAULT_PATH):
"""Gets list of available simulator as a dictionary.
Args:
path: (str) Path to be passed to '--set' option.
"""
return json.loads(
subprocess.check_output(['xcrun', 'simctl', '--set', path, 'list',
'-j']).decode('utf-8'))
def get_simulator(platform, version):
"""Gets a simulator or creates a new one if not exist by platform and version.
Args:
platform: (str) A platform name, e.g. "iPhone 11 Pro"
version: (str) A version name, e.g. "13.4"
Returns:
A udid of a simulator device.
"""
udids = get_simulator_udids_by_platform_and_version(platform, version)
if udids:
return udids[0]
return create_device_by_platform_and_version(platform, version)
def get_simulator_device_type_by_platform(simulators, platform):
"""Gets device type identifier for platform.
Args:
simulators: (dict) A list of available simulators.
platform: (str) A platform name, e.g. "iPhone 11 Pro"
Returns:
Simulator device type identifier string of the platform.
e.g. 'com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro'
Raises:
test_runner.SimulatorNotFoundError when the platform can't be found.
"""
for devicetype in simulators['devicetypes']:
if devicetype['name'] == platform:
return devicetype['identifier']
raise test_runner.SimulatorNotFoundError(
'Not found device "%s" in devicetypes %s' %
(platform, simulators['devicetypes']))
def get_simulator_runtime_by_version(simulators, version):
"""Gets runtime based on iOS version.
Args:
simulators: (dict) A list of available simulators.
version: (str) A version name, e.g. "13.4"
Returns:
Simulator runtime identifier string of the version.
e.g. 'com.apple.CoreSimulator.SimRuntime.iOS-13-4'
Raises:
test_runner.SimulatorNotFoundError when the version can't be found.
"""
for runtime in simulators['runtimes']:
# The output might use version with a patch number (e.g. 17.0.1)
# but the passed in version does not have a patch number (e.g. 17.0)
# Therefore, we should use startswith for substring match.
if runtime['version'].startswith(version) and 'iOS' in runtime['name']:
return runtime['identifier']
raise test_runner.SimulatorNotFoundError('Not found "%s" SDK in runtimes %s' %
(version, simulators['runtimes']))
def get_simulator_runtime_by_device_udid(simulator_udid):
"""Gets simulator runtime based on simulator UDID.
Args:
simulator_udid: (str) UDID of a simulator.
"""
simulator_list = get_simulator_list()['devices']
for runtime, simulators in simulator_list.items():
for device in simulators:
if simulator_udid == device['udid']:
return runtime
raise test_runner.SimulatorNotFoundError(
'Not found simulator with "%s" UDID in devices %s' % (simulator_udid,
simulator_list))
def get_simulator_udids_by_platform_and_version(platform, version):
"""Gets list of simulators UDID based on platform name and iOS version.
Args:
platform: (str) A platform name, e.g. "iPhone 11"
version: (str) A version name, e.g. "13.2.2"
"""
simulators = get_simulator_list()
devices = simulators['devices']
sdk_id = get_simulator_runtime_by_version(simulators, version)
results = []
for device in devices.get(sdk_id, []):
if device['name'] == _compose_simulator_name(platform, version):
results.append(device['udid'])
return results
def create_device_by_platform_and_version(platform, version):
"""Creates a simulator and returns UDID of it.
Args:
platform: (str) A platform name, e.g. "iPhone 11"
version: (str) A version name, e.g. "13.2.2"
"""
name = _compose_simulator_name(platform, version)
LOGGER.info('Creating simulator %s', name)
simulators = get_simulator_list()
device_type = get_simulator_device_type_by_platform(simulators, platform)
runtime = get_simulator_runtime_by_version(simulators, version)
try:
udid = subprocess.check_output(
['xcrun', 'simctl', 'create', name, device_type,
runtime]).decode('utf-8').rstrip()
LOGGER.info('Created simulator in first attempt with UDID: %s', udid)
# Sometimes above command fails to create a simulator. Verify it and retry
# once if first attempt failed.
if not is_device_with_udid_simulator(udid):
# Try to delete once to avoid duplicate in case of race condition.
delete_simulator_by_udid(udid)
udid = subprocess.check_output(
['xcrun', 'simctl', 'create', name, device_type,
runtime]).decode('utf-8').rstrip()
LOGGER.info('Created simulator in second attempt with UDID: %s', udid)
return udid
except subprocess.CalledProcessError as e:
LOGGER.error('Error when creating simulator "%s": %s' % (name, e.output))
raise e
def delete_simulator_by_udid(udid):
"""Deletes simulator by its udid.
Args:
udid: (str) UDID of simulator.
"""
LOGGER.info('Deleting simulator %s', udid)
try:
subprocess.check_output(['xcrun', 'simctl', 'delete', udid],
stderr=subprocess.STDOUT).decode('utf-8')
except subprocess.CalledProcessError as e:
# Logging error instead of throwing so we don't cause failures in case
# this was indeed failing to clean up.
message = 'Failed to delete simulator %s with error %s' % (udid, e.output)
LOGGER.error(message)
def wipe_simulator_by_udid(udid):
"""Wipes simulators by its udid.
Args:
udid: (str) UDID of simulator.
"""
for _, devices in get_simulator_list()['devices'].items():
for device in devices:
if device['udid'] != udid:
continue
try:
LOGGER.info('Shutdown simulator %s ', device)
if device['state'] != 'Shutdown':
subprocess.check_call(['xcrun', 'simctl', 'shutdown', device['udid']])
except subprocess.CalledProcessError as ex:
LOGGER.error('Shutdown failed %s ', ex)
subprocess.check_call(['xcrun', 'simctl', 'erase', device['udid']])
def get_home_directory(platform, version):
"""Gets directory where simulators are stored.
Args:
platform: (str) A platform name, e.g. "iPhone 11"
version: (str) A version name, e.g. "13.2.2"
"""
return subprocess.check_output(
['xcrun', 'simctl', 'getenv',
get_simulator(platform, version), 'HOME']).decode('utf-8').rstrip()
def boot_simulator_if_not_booted(sim_udid):
"""Boots the simulator of given udid.
Args:
sim_udid: (str) UDID of the simulator.
Raises:
test_runner.SimulatorNotFoundError if the sim_udid is not found on machine.
"""
simulator_list = get_simulator_list()
for _, devices in simulator_list['devices'].items():
for device in devices:
if device['udid'] != sim_udid:
continue
if device['state'] == 'Booted':
return
subprocess.check_output(['xcrun', 'simctl', 'boot',
sim_udid]).decode('utf-8')
return
raise test_runner.SimulatorNotFoundError(
'Not found simulator with "%s" UDID in devices %s' %
(sim_udid, simulator_list['devices']))
def get_app_data_directory(app_bundle_id, sim_udid):
"""Returns app data directory for a given app on a given simulator.
Args:
app_bundle_id: (str) Bundle id of application.
sim_udid: (str) UDID of the simulator.
"""
return subprocess.check_output(
['xcrun', 'simctl', 'get_app_container', sim_udid, app_bundle_id,
'data']).decode('utf-8').rstrip()
def is_device_with_udid_simulator(device_udid):
"""Checks whether a device with udid is simulator or not.
Args:
device_udid: (str) UDID of a device.
"""
simulator_list = get_simulator_list()['devices']
for _, simulators in simulator_list.items():
for device in simulators:
if device_udid == device['udid']:
return True
return False
def copy_trusted_certificate(cert_path, udid):
"""Copies a cert into a simulator.
This allows the simulator to install the input cert.
Args:
cert_path: (str) A path for the cert
udid: (str) UDID of a simulator.
"""
# TODO(crbug.com/1351820): Update wpr runner to use this function.
if not os.path.exists(cert_path):
LOGGER.error('Failed to find the cert path %s', cert_path)
return
LOGGER.info('Copying cert into %s', udid)
# Try to boot first, if the simulator is already booted, continue.
try:
subprocess.check_call(['xcrun', 'simctl', 'boot', udid])
except subprocess.CalledProcessError as e:
if 'booted' not in str(e):
# Logging error instead of throwing, so we don't cause failures in case
# this was indeed failing to copy the cert.
message = 'Failed to boot simulator before installing cert. ' \
'Error: %s' % e.output
LOGGER.error(message)
return
try:
subprocess.check_call(
['xcrun', 'simctl', 'keychain', udid, 'add-root-cert', cert_path])
subprocess.check_call(['xcrun', 'simctl', 'shutdown', udid])
except subprocess.CalledProcessError as e:
message = 'Failed to install cert. Error: %s' % e.output
LOGGER.error(message)
def get_simulator_runtime_list():
"""Gets list of available simulator runtimes as a dictionary."""
return json.loads(
subprocess.check_output(['xcrun', 'simctl', 'runtime', 'list',
'-j']).decode('utf-8'))
def get_simulator_runtime_match_list():
"""Gets list of chosen simulator runtime for each simulator sdk type"""
return json.loads(
subprocess.check_output(
['xcrun', 'simctl', 'runtime', 'match', 'list',
'-j']).decode('utf-8'))
def get_simulator_runtime_info_by_build(runtime_build):
"""Gets runtime object based on the runtime build.
Args:
runtime_build: (str) build id of the runtime, e.g. "20C52"
Returns:
a simulator runtime json object that contains all the info of an
iOS runtime
e.g.
{
"build" : "19F70",
"deletable" : true,
"identifier" : "FD9ED7F9-96A7-4621-B328-4C317893EC8A",
etc...
}
if no runtime for the corresponding build id is found, then
return None.
"""
runtimes = get_simulator_runtime_list()
for runtime in runtimes.values():
if runtime['build'].lower() == runtime_build.lower():
return runtime
return None
def get_simulator_runtime_info_by_id(identifier):
"""Gets runtime object based on the runtime id.
Args:
identifier: (str) id of the runtime, e.g. "7A46A063-35D7"
Returns:
a simulator runtime json object that contains all the info of an
iOS runtime
e.g.
{
"build" : "19F70",
"deletable" : true,
"identifier" : "7A46A063-35D7",
etc...
}
if no runtime for the corresponding id is found, then
return None.
"""
runtimes = get_simulator_runtime_list()
for runtime in runtimes.values():
if runtime['identifier'].lower() == identifier.lower():
return runtime
return None
def get_simulator_runtime_info(ios_version):
"""Gets runtime object based on iOS version.
Args:
version: (str) A version name, e.g. "13.4"
Returns:
a simulator runtime json object that contains all the info of an
iOS runtime
e.g.
{
"build" : "19F70",
"deletable" : true,
"identifier" : "FD9ED7F9-96A7-4621-B328-4C317893EC8A",
etc...
}
if no runtime for the corresponding iOS version is found, then
return None.
"""
runtimes = get_simulator_runtime_list()
for runtime in runtimes.values():
# The output might use version with a patch number (e.g. 17.0.1)
# but the passed in version does not have a patch number (e.g. 17.0)
# Therefore, we should use startswith for substring match.
if runtime['version'].startswith(ios_version):
return runtime
return None
def is_simulator_runtime_builtin(runtime):
if (runtime is None or runtime['kind'] not in IOS_SIM_RUNTIME_BUILTIN_STATE):
return False
return True
def override_default_iphonesim_runtime(runtime_id, ios_version):
"""Overrides the default simulator runtime build version.
The default simulator runtime build version that Xcode looks
for might not be the same as what we downloaded. Therefore,
this method gives the option for override the default with a
different runtime build version (ideally the one we downloaded from cipd.
Args:
runtime_id: (str) the runtime id that we desire to use.
The runtime build version will be extracted and override the
default one.
ios_version: the iOS version of the iphone sdk we want to
override. e.g. 17.0
"""
# find the runtime build number to override with
overriding_build = None
runtimes = get_simulator_runtime_list()
for runtime_key in runtimes:
if runtime_key in runtime_id:
overriding_build = runtimes[runtime_key].get('build')
break
if overriding_build is None:
LOGGER.debug(
'Unable to find the simulator runtime build number to override with...')
return
# find the runtime build number to be overridden
sdks = get_simulator_runtime_match_list()
iphone_sdk_key = 'iphoneos' + ios_version
sdk_build = sdks.get(iphone_sdk_key, {}).get("sdkBuild")
if sdk_build is None:
LOGGER.debug(
'Unable to find the simulator runtime build number to be overriden...')
return
cmd = [
'xcrun', 'simctl', 'runtime', 'match', 'set', iphone_sdk_key,
overriding_build, '--sdkBuild', sdk_build
]
LOGGER.debug('Overriding default runtime with command %s' % cmd)
subprocess.check_call(cmd)
def add_simulator_runtime(runtime_dmg_path):
cmd = ['xcrun', 'simctl', 'runtime', 'add', runtime_dmg_path]
LOGGER.debug('Adding runtime with command %s' % cmd)
return subprocess.check_output(cmd).decode('utf-8')
def delete_simulator_runtime(runtime_id, shoud_wait=False):
cmd = ['xcrun', 'simctl', 'runtime', 'delete', runtime_id]
LOGGER.debug('Deleting runtime with command %s' % cmd)
subprocess.check_output(cmd)
if shoud_wait:
# runtime takes a few seconds to delete
time_waited = 0
runtime_to_delete = get_simulator_runtime_info_by_id(runtime_id)
while (runtime_to_delete is not None):
LOGGER.debug('Waiting for runtime to be deleted. Current state is %s' %
runtime_to_delete['state'])
time.sleep(1)
time_waited += 1
if (time_waited > MAX_WAIT_TIME_TO_DELETE_RUNTIME):
raise test_runner_errors.SimRuntimeDeleteTimeoutError(ios_version)
runtime_to_delete = get_simulator_runtime_info_by_id(runtime_id)
LOGGER.debug('Runtime successfully deleted!')
def delete_simulator_runtime_after_days(days):
cmd = ['xcrun', 'simctl', 'runtime', 'delete', '--notUsedSinceDays', days]
LOGGER.debug('Deleting unused runtime with command %s' % cmd)
subprocess.run(cmd, check=False)
def delete_least_recently_used_simulator_runtimes(
max_to_keep=constants.MAX_RUNTIME_KETP_COUNT):
"""Delete least recently used simulator runtimes.
Delete simulator runtimes that are least recently used, based
on the lastUsedAt field. iOS15.5 and runtimes bundled within Xcode
are excluded.
Args:
max_to_keep: (int) max number of simulator runtimes to keep.
All other simulator runtimes will be deleted based on lastUsedAt field.
"""
runtimes = get_simulator_runtime_list()
sorted_runtime_values = sorted(
runtimes.values(), key=lambda x: x.get("lastUsedAt", ""), reverse=True)
sorted_runtimes = OrderedDict(
(item["identifier"], item) for item in sorted_runtime_values)
keep_count = 0
for runtime_id, value in sorted_runtimes.items():
if is_simulator_runtime_builtin(value):
LOGGER.debug('Built-in Runtime %s with iOS %s should not be deleted' %
(runtime_id, value['version']))
continue
if keep_count < max_to_keep:
LOGGER.debug('Runtime %s with iOS %s should be kept undeleted' %
(runtime_id, value['version']))
keep_count += 1
else:
delete_simulator_runtime(runtime_id, True)
def delete_simulator_runtime_and_wait(ios_version):
runtime_to_delete = get_simulator_runtime_info(ios_version)
if runtime_to_delete == None:
LOGGER.debug('Runtime %s does not exist in Xcode, no need to cleanup...' %
ios_version)
return
delete_simulator_runtime(runtime_to_delete['identifier'], True)
def disable_hardware_keyboard(udid: str) -> None:
"""Disables hardware keyboard input for the given simulator.
Exceptions are caught and logged but do not interrupt program flow. The result
is that if the util is unable to change the HW keyboard pref for any reason
the test will still run without changing the preference.
Args:
udid: (str) UDID of the simulator to disable hw keyboard for.
"""
path = os.path.expanduser(
'~/Library/Preferences/com.apple.iphonesimulator.plist')
try:
if not os.path.exists(path):
subprocess.check_call(['plutil', '-create', 'binary1', path])
plist, error = mac_util.plist_as_dict(path)
if error:
raise error
if 'DevicePreferences' not in plist:
subprocess.check_call(
['plutil', '-insert', 'DevicePreferences', '-dictionary', path])
plist['DevicePreferences'] = {}
if 'DevicePreferences' in plist and udid not in plist['DevicePreferences']:
subprocess.check_call([
'plutil', '-insert', 'DevicePreferences.{}'.format(udid),
'-dictionary', path
])
plist['DevicePreferences'][udid] = {}
subprocess.check_call([
'plutil', '-replace',
'DevicePreferences.{}.ConnectHardwareKeyboard'.format(udid), '-bool',
'NO', path
])
except subprocess.CalledProcessError as e:
message = 'Unable to disable hardware keyboard. Error: %s' % e.stderr
LOGGER.error(message)
except json.JSONDecodeError as e:
message = 'Unable to disable hardware keyboard. Error: %s' % e.msg
LOGGER.error(message)
def disable_simulator_keyboard_tutorial(udid):
"""Disables keyboard tutorial for the given simulator.
Keyboard tutorial can cause flakes to EG tests as they are not expected.
Exceptions are caught and logged but do not interrupt program flow.
Args:
udid: (str) UDID of the simulator.
"""
boot_simulator_if_not_booted(udid)
try:
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'DidShowContinuousPathIntroduction',
'1'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'KeyboardDidShowProductivityTutorial',
'1'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'DidShowGestureKeyboardIntroduction',
'1'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences',
'UIKeyboardDidShowInternationalInfoIntroduction', '1'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'KeyboardAutocorrection', '0'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'KeyboardPrediction', '0'
])
subprocess.check_call([
'xcrun', 'simctl', 'spawn', udid, 'defaults', 'write',
'com.apple.keyboard.preferences', 'KeyboardShowPredictionBar', '0'
])
except subprocess.CalledProcessError as e:
message = 'Unable to disable keyboard tutorial: %s' % e.stderr
LOGGER.error(message)