Skip to content

Commit 63e642c

Browse files
author
Weiming Dai
committed
Robust solution for checking the device SDK in ios_test_runner
Robust solution for checking the device SDK in ios_test_runner. - Support passing optional argument --platform=[ios_device,ios_simulator]. - If args.platform is not given, use the output `xcrun simctl list devices` and `instruments -s devices` to detect the SDK of the device from given device id. For new iOS models, such as iPhone Xs, Xs MAX, the uuid is no longer 40 digit. To be more robust, ios_test_runner won't use uuid format to determine the SDK of the device. To be consistent with Google bazel, we add a new argument --platform.
1 parent 0bb08cc commit 63e642c

File tree

5 files changed

+96
-48
lines changed

5 files changed

+96
-48
lines changed

xctestrunner/shared/ios_constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ def enum(**enums):
2020

2121

2222
SDK = enum(IPHONEOS='iphoneos', IPHONESIMULATOR='iphonesimulator')
23+
# It is consistent with bazel's apple platform:
24+
# https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/rules/apple/ApplePlatform.java
25+
PLATFORM = enum(IOS_DEVICE='ios_device', IOS_SIMULATOR='ios_simulator')
2326
OS = enum(IOS='iOS', WATCHOS='watchOS', TVOS='tvOS')
2427
TestType = enum(XCUITEST='xcuitest', XCTEST='xctest', LOGIC_TEST='logic_test')
2528
SimState = enum(CREATING='Creating', SHUTDOWN='Shutdown', BOOTED='Booted',
2629
UNKNOWN='Unknown')
2730

2831
SUPPORTED_SDKS = [SDK.IPHONESIMULATOR, SDK.IPHONEOS]
32+
SUPPORTED_PLATFORMS = [PLATFORM.IOS_SIMULATOR, PLATFORM.IOS_DEVICE]
2933
SUPPORTED_TEST_TYPES = [TestType.XCUITEST, TestType.XCTEST, TestType.LOGIC_TEST]
3034
SUPPORTED_SIM_OSS = [OS.IOS]
3135

xctestrunner/simulator_control/simulator_util.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def Shutdown(self):
124124
'Can not shut down the simulator in state CREATING.')
125125
logging.info('Shutting down simulator %s.', self.simulator_id)
126126
try:
127-
_RunSimctlCommand(['xcrun', 'simctl', 'shutdown', self.simulator_id])
127+
RunSimctlCommand(['xcrun', 'simctl', 'shutdown', self.simulator_id])
128128
except ios_errors.SimError as e:
129129
if 'Unable to shutdown device in current state: Shutdown' in str(e):
130130
logging.info('Simulator %s has already shut down.', self.simulator_id)
@@ -143,13 +143,16 @@ def Delete(self):
143143
Raises:
144144
ios_errors.SimError: The simulator's state is not SHUTDOWN.
145145
"""
146-
sim_state = self.GetSimulatorState()
147-
if sim_state != ios_constants.SimState.SHUTDOWN:
148-
raise ios_errors.SimError(
149-
'Can only delete the simulator with state SHUTDOWN. The current '
150-
'state of simulator %s is %s.' % (self._simulator_id, sim_state))
146+
# In Xcode 9+, simctl can delete Booted simulator. In prior of Xcode 9,
147+
# we have to shutdown the simulator first before deleting it.
148+
if xcode_info_util.GetXcodeVersionNumber() < 900:
149+
sim_state = self.GetSimulatorState()
150+
if sim_state != ios_constants.SimState.SHUTDOWN:
151+
raise ios_errors.SimError(
152+
'Can only delete the simulator with state SHUTDOWN. The current '
153+
'state of simulator %s is %s.' % (self._simulator_id, sim_state))
151154
try:
152-
_RunSimctlCommand(['xcrun', 'simctl', 'delete', self.simulator_id])
155+
RunSimctlCommand(['xcrun', 'simctl', 'delete', self.simulator_id])
153156
except ios_errors.SimError as e:
154157
raise ios_errors.SimError(
155158
'Failed to delete simulator %s: %s' % (self.simulator_id, str(e)))
@@ -187,14 +190,14 @@ def GetAppDocumentsPath(self, app_bundle_id):
187190
"""Gets the path of the app's Documents directory."""
188191
if xcode_info_util.GetXcodeVersionNumber() >= 830:
189192
try:
190-
app_data_container = _RunSimctlCommand(
193+
app_data_container = RunSimctlCommand(
191194
['xcrun', 'simctl', 'get_app_container', self._simulator_id,
192195
app_bundle_id, 'data'])
193196
return os.path.join(app_data_container, 'Documents')
194197
except ios_errors.SimError as e:
195198
raise ios_errors.SimError(
196-
'Failed to get data container of the app %s in simulator %s: %s',
197-
app_bundle_id, self._simulator_id, str(e))
199+
'Failed to get data container of the app %s in simulator %s: %s'%
200+
(app_bundle_id, self._simulator_id, str(e)))
198201

199202
apps_dir = os.path.join(
200203
self.simulator_root_dir, 'data/Containers/Data/Application')
@@ -208,13 +211,13 @@ def GetAppDocumentsPath(self, app_bundle_id):
208211
if current_app_bundle_id == app_bundle_id:
209212
return os.path.join(apps_dir, sub_dir_name, 'Documents')
210213
raise ios_errors.SimError(
211-
'Failed to get Documents directory of the app %s in simulator %s: %s',
212-
app_bundle_id, self._simulator_id)
214+
'Failed to get Documents directory of the app %s in simulator %s' %
215+
(app_bundle_id, self._simulator_id))
213216

214217
def IsAppInstalled(self, app_bundle_id):
215218
"""Checks if the simulator has installed the app with given bundle id."""
216219
try:
217-
_RunSimctlCommand(
220+
RunSimctlCommand(
218221
['xcrun', 'simctl', 'get_app_container', self._simulator_id,
219222
app_bundle_id])
220223
return True
@@ -325,7 +328,7 @@ def CreateNewSimulator(device_type=None, os_version=None, name=None):
325328
name, os_type, os_version, device_type)
326329
for i in range(0, _SIM_OPERATION_MAX_ATTEMPTS):
327330
try:
328-
new_simulator_id = _RunSimctlCommand(
331+
new_simulator_id = RunSimctlCommand(
329332
['xcrun', 'simctl', 'create', name, device_type, runtime_id])
330333
except ios_errors.SimError as e:
331334
raise ios_errors.SimError(
@@ -383,7 +386,7 @@ def GetSupportedSimDeviceTypes(os_type=None):
383386
#
384387
# See more examples in testdata/simctl_list_devicetypes.json
385388
sim_types_infos_json = json.loads(
386-
_RunSimctlCommand(('xcrun', 'simctl', 'list', 'devicetypes', '-j')))
389+
RunSimctlCommand(('xcrun', 'simctl', 'list', 'devicetypes', '-j')))
387390
sim_types = []
388391
for sim_types_info in sim_types_infos_json['devicetypes']:
389392
sim_type = sim_types_info['name']
@@ -438,7 +441,9 @@ def GetSupportedSimOsVersions(os_type=ios_constants.OS.IOS):
438441
# {
439442
# "runtimes" : [
440443
# {
441-
# "bundlePath" : "\/Applications\/Xcode10.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime",
444+
# "bundlePath" : "\/Applications\/Xcode10.app\/Contents\/Developer\
445+
# /Platforms\/iPhoneOS.platform\/Developer\/Library\
446+
# /CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime",
442447
# "availabilityError" : "",
443448
# "buildversion" : "16A366",
444449
# "availability" : "(available)",
@@ -451,7 +456,7 @@ def GetSupportedSimOsVersions(os_type=ios_constants.OS.IOS):
451456
# See more examples in testdata/simctl_list_runtimes.json
452457
xcode_version_num = xcode_info_util.GetXcodeVersionNumber()
453458
sim_runtime_infos_json = json.loads(
454-
_RunSimctlCommand(('xcrun', 'simctl', 'list', 'runtimes', '-j')))
459+
RunSimctlCommand(('xcrun', 'simctl', 'list', 'runtimes', '-j')))
455460
sim_versions = []
456461
for sim_runtime_info in sim_runtime_infos_json['runtimes']:
457462
# Normally, the json does not contain unavailable runtimes. To be safe,
@@ -501,7 +506,7 @@ def GetLastSupportedSimOsVersion(os_type=ios_constants.OS.IOS,
501506
supported_os_versions = GetSupportedSimOsVersions(os_type)
502507
if not supported_os_versions:
503508
raise ios_errors.SimError(
504-
'Can not find supported OS version of %s.', os_type)
509+
'Can not find supported OS version of %s.' % os_type)
505510
if not device_type:
506511
return supported_os_versions[-1]
507512

@@ -648,7 +653,7 @@ def IsCoreSimulatorCrash(sim_sys_log):
648653
return pattern.search(sim_sys_log) is not None
649654

650655

651-
def _RunSimctlCommand(command):
656+
def RunSimctlCommand(command):
652657
"""Runs simctl command."""
653658
for i in range(_SIMCTL_MAX_ATTEMPTS):
654659
process = subprocess.Popen(

xctestrunner/test_runner/ios_test_runner.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import argparse
2323
import json
2424
import logging
25+
import subprocess
2526
import sys
2627

2728
from xctestrunner.shared import ios_constants
@@ -105,9 +106,9 @@ def _AddTestSubParser(subparsers):
105106
"""Adds sub parser for sub command `test`."""
106107
def _Test(args):
107108
"""The function of sub command `test`."""
109+
sdk = _PlatformToSdk(args.platform) if args.platform else _GetSdk(args.id)
108110
with xctest_session.XctestSession(
109-
sdk=xctest_session.GetSdk(args.id),
110-
work_dir=args.work_dir, output_dir=args.output_dir) as session:
111+
sdk=sdk, work_dir=args.work_dir, output_dir=args.output_dir) as session:
111112
session.Prepare(
112113
app_under_test=args.app_under_test_path,
113114
test_bundle=args.test_bundle_path,
@@ -126,6 +127,12 @@ def _Test(args):
126127
'--id',
127128
required=True,
128129
help='The device id. The device can be iOS real device or simulator.')
130+
optional_arguments = test_parser.add_argument_group('Optional arguments')
131+
optional_arguments.add_argument(
132+
'--platform',
133+
help='The platform of the device. The value can be ios_device or '
134+
'ios_simulator.'
135+
)
129136
test_parser.set_defaults(func=_Test)
130137

131138

@@ -144,7 +151,11 @@ def _RunSimulatorTest(args):
144151
signing_options=_GetJson(args.signing_options_json_path))
145152
session.SetLaunchOptions(_GetJson(args.launch_options_json_path))
146153

147-
simulator_util.QuitSimulatorApp()
154+
# In prior of Xcode 9, `xcodebuild test` will launch the Simulator.app
155+
# process. If there is Simulator.app before running test, it will cause
156+
# error later.
157+
if xcode_info_util.GetXcodeVersionNumber() < 900:
158+
simulator_util.QuitSimulatorApp()
148159
max_attempts = 3
149160
reboot_sim = False
150161
for i in range(max_attempts):
@@ -175,18 +186,22 @@ def _RunSimulatorTest(args):
175186
continue
176187
return exit_code
177188
finally:
178-
# 1. Before Xcode 9, `xcodebuild test` will launch the Simulator.app
179-
# process. Quit the Simulator.app to avoid side effect.
189+
# 1. In prior of Xcode 9, `xcodebuild test` will launch the
190+
# Simulator.app process. Quit the Simulator.app to avoid side effect.
180191
# 2. Quit Simulator.app can also shutdown the simulator. To make sure
181192
# the Simulator state to be SHUTDOWN, still call shutdown command
182193
# later.
183194
if xcode_info_util.GetXcodeVersionNumber() < 900:
184195
simulator_util.QuitSimulatorApp()
185196
simulator_obj = simulator_util.Simulator(simulator_id)
186-
# Can only delete the "SHUTDOWN" state simulator.
187-
simulator_obj.Shutdown()
188-
# Deletes the new simulator to avoid side effect.
189-
if not reboot_sim:
197+
if reboot_sim:
198+
simulator_obj.Shutdown()
199+
else:
200+
# In Xcode 9+, simctl can delete the Booted simulator.
201+
# In prior of Xcode 9, we have to shutdown the simulator first
202+
# before deleting it.
203+
if xcode_info_util.GetXcodeVersionNumber() < 900:
204+
simulator_obj.Shutdown()
190205
simulator_obj.Delete()
191206

192207
def _SimulatorTest(args):
@@ -245,6 +260,38 @@ def _GetJson(json_path):
245260
return None
246261

247262

263+
def _PlatformToSdk(platform):
264+
"""Gets the SDK of the given platform."""
265+
if platform == ios_constants.PLATFORM.IOS_DEVICE:
266+
return ios_constants.SDK.IPHONEOS
267+
if platform == ios_constants.PLATFORM.IOS_SIMULATOR:
268+
return ios_constants.SDK.IPHONESIMULATOR
269+
raise ios_errors.IllegalArgumentError(
270+
'The platform %s is not supported. The supported values are %s.' %
271+
(platform, ios_constants.SUPPORTED_PLATFORMS))
272+
273+
274+
def _GetSdk(device_id):
275+
"""Gets the sdk of the target device with the given device_id."""
276+
# The command `instruments -s devices` is much slower than
277+
# `xcrun simctl list devices`. So use `xcrun simctl list devices` to check
278+
# IPHONESIMULATOR SDK first.
279+
simlist_devices_output = simulator_util.RunSimctlCommand(
280+
['xcrun', 'simctl', 'list', 'devices'])
281+
if device_id in simlist_devices_output:
282+
return ios_constants.SDK.IPHONESIMULATOR
283+
284+
known_devices_output = subprocess.check_output(
285+
['instruments', '-s', 'devices'])
286+
for line in known_devices_output.split('\n'):
287+
if device_id in line and '(Simulator)' not in line:
288+
return ios_constants.SDK.IPHONEOS
289+
290+
raise ios_errors.IllegalArgumentError(
291+
'The device with id %s can not be found. The known devices are %s.' %
292+
(device_id, known_devices_output))
293+
294+
248295
def main(argv):
249296
args = _BuildParser().parse_args(argv[1:])
250297
if args.verbose:

xctestrunner/test_runner/xctest_session.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,6 @@ def Close(self):
264264
shutil.rmtree(self._output_dir)
265265

266266

267-
def GetSdk(device_id):
268-
"""Gets the sdk of the target device with the given device_id."""
269-
if '-' in device_id:
270-
return ios_constants.SDK.IPHONESIMULATOR
271-
else:
272-
return ios_constants.SDK.IPHONEOS
273-
274-
275267
def _PrepareBundles(working_dir, app_under_test_path, test_bundle_path):
276268
"""Prepares the bundles in work directory.
277269

xctestrunner/test_runner/xctestrun.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,10 @@ def __init__(self, app_under_test_dir, test_bundle_dir,
271271
self._sdk = sdk
272272
self._test_type = test_type
273273
if self._sdk == ios_constants.SDK.IPHONEOS:
274+
self._on_device = True
274275
self._signing_options = signing_options
275276
else:
277+
self._on_device = False
276278
if signing_options:
277279
logging.info(
278280
'The signing options only works on sdk iphoneos, but current sdk '
@@ -373,7 +375,7 @@ def _ValidateArguments(self):
373375
'The test type %s is not supported. Supported test types are %s.'
374376
% (self._test_type, ios_constants.SUPPORTED_TEST_TYPES))
375377
if (self._test_type == ios_constants.TestType.LOGIC_TEST and
376-
self._sdk != ios_constants.SDK.IPHONESIMULATOR):
378+
self._on_device):
377379
raise ios_errors.IllegalArgumentError(
378380
'Only support running logic test on sdk iphonesimulator. '
379381
'Current sdk is %s' % self._sdk)
@@ -406,7 +408,7 @@ def _GenerateTestRootForXcuitest(self):
406408

407409
self._PrepareUitestInRunerApp(uitest_runner_app)
408410

409-
if self._sdk == ios_constants.SDK.IPHONEOS:
411+
if self._on_device:
410412
runner_app_embedded_provision = os.path.join(
411413
uitest_runner_app, 'embedded.mobileprovision')
412414
use_customized_provision = False
@@ -448,23 +450,22 @@ def _GenerateTestRootForXcuitest(self):
448450
plist_util.Plist(entitlements_plist_path).SetPlistField(
449451
None, entitlements_dict)
450452

451-
app_under_test_signing_identity = bundle_util.GetCodesignIdentity(
452-
self._app_under_test_dir)
453+
test_bundle_signing_identity = bundle_util.GetCodesignIdentity(
454+
self._test_bundle_dir)
453455
bundle_util.CodesignBundle(
454-
xctest_framework, identity=app_under_test_signing_identity)
456+
xctest_framework, identity=test_bundle_signing_identity)
455457
if xcode_info_util.GetXcodeVersionNumber() >= 900:
456458
bundle_util.CodesignBundle(
457-
xct_automation_framework, identity=app_under_test_signing_identity)
459+
xct_automation_framework, identity=test_bundle_signing_identity)
458460
bundle_util.CodesignBundle(
459461
uitest_runner_app,
460462
entitlements_plist_path=entitlements_plist_path,
461-
identity=app_under_test_signing_identity)
463+
identity=test_bundle_signing_identity)
462464

463465
bundle_util.CodesignBundle(self._test_bundle_dir)
464466
bundle_util.CodesignBundle(self._app_under_test_dir)
465467

466-
platform_name = ('iPhoneOS' if self._sdk == ios_constants.SDK.IPHONEOS else
467-
'iPhoneSimulator')
468+
platform_name = 'iPhoneOS' if self._on_device else 'iPhoneSimulator'
468469
test_envs = {
469470
'DYLD_FRAMEWORK_PATH':
470471
'__TESTROOT__:__PLATFORMS__/%s.platform/Developer/'
@@ -581,7 +582,7 @@ def _GenerateTestRootForXctest(self):
581582
'Developer/usr/lib/libXCTestBundleInject.dylib'),
582583
insert_libs_framework)
583584

584-
if self._sdk == ios_constants.SDK.IPHONEOS:
585+
if self._on_device:
585586
app_under_test_signing_identity = bundle_util.GetCodesignIdentity(
586587
self._app_under_test_dir)
587588
bundle_util.CodesignBundle(
@@ -593,8 +594,7 @@ def _GenerateTestRootForXctest(self):
593594

594595
app_under_test_name = os.path.splitext(
595596
os.path.basename(self._app_under_test_dir))[0]
596-
platform_name = ('iPhoneOS' if self._sdk == ios_constants.SDK.IPHONEOS else
597-
'iPhoneSimulator')
597+
platform_name = 'iPhoneOS' if self._on_device else 'iPhoneSimulator'
598598
if xcode_info_util.GetXcodeVersionNumber() < 1000:
599599
dyld_insert_libs = ('__PLATFORMS__/%s.platform/Developer/Library/'
600600
'PrivateFrameworks/IDEBundleInjection.framework/'

0 commit comments

Comments
 (0)