Skip to content

Commit aa821f3

Browse files
committed
Update test runner with internal change
- Fix the minSupportedVersion issue for simulator creation. - Allow running arm64 test bundle on arm64e iOS device.
1 parent d2b9ed9 commit aa821f3

File tree

9 files changed

+131
-53
lines changed

9 files changed

+131
-53
lines changed

xctestrunner/shared/bundle_util.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ def EnableUIFileSharing(bundle_path, resigning=True):
215215
CodesignBundle(bundle_path)
216216

217217

218+
def GetFileArchTypes(file_path):
219+
"""Gets the architecture types of the file."""
220+
output = subprocess.check_output(['/usr/bin/lipo', file_path, '-archs'])
221+
return output.split(' ')
222+
223+
224+
def RemoveArchType(file_path, arch_type):
225+
"""Remove the given architecture types for the file."""
226+
subprocess.check_call(
227+
['/usr/bin/lipo', file_path, '-remove', arch_type, '-output', file_path])
228+
229+
218230
def _ExtractBundleFile(target_dir, bundle_extension):
219231
"""Extract single bundle file with given extension.
220232

xctestrunner/shared/ios_constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ def enum(**enums):
1919
return type('Enum', (), enums)
2020

2121

22+
ARCH = enum(
23+
ARMV7='armv7',
24+
ARMV7S='armv7s',
25+
ARM64='arm64',
26+
ARM64E='arm64e',
27+
I386='i386',
28+
X86_64='x86_64')
2229
SDK = enum(IPHONEOS='iphoneos', IPHONESIMULATOR='iphonesimulator')
2330
# It is consistent with bazel's apple platform:
2431
# https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/rules/apple/ApplePlatform.java

xctestrunner/shared/xcode_info_util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def GetXcodeDeveloperPath():
3434
# path to that fallback directory.
3535
# See https://github.com/bazelbuild/rules_apple/issues/684 for context.
3636
def GetSwift5FallbackLibsDir():
37+
"""Gets the directory for Swift5 fallback libraries."""
3738
relativePath = "Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0"
3839
swiftLibsDir = os.path.join(GetXcodeDeveloperPath(), relativePath)
3940
swiftLibPlatformDir = os.path.join(swiftLibsDir, ios_constants.SDK.IPHONESIMULATOR)

xctestrunner/simulator_control/simtype_profile.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ def profile_plist_obj(self):
4747
profile.plist.
4848
"""
4949
if not self._profile_plist_obj:
50-
if xcode_info_util.GetXcodeVersionNumber() >= 900:
50+
xcode_version = xcode_info_util.GetXcodeVersionNumber()
51+
if xcode_version >= 900:
5152
platform_path = xcode_info_util.GetSdkPlatformPath(
5253
ios_constants.SDK.IPHONEOS)
5354
else:
5455
platform_path = xcode_info_util.GetSdkPlatformPath(
5556
ios_constants.SDK.IPHONESIMULATOR)
56-
if xcode_info_util.GetXcodeVersionNumber() >= 1100:
57+
if xcode_version >= 1100:
5758
sim_profiles_dir = os.path.join(
5859
platform_path, 'Library/Developer/CoreSimulator/Profiles')
5960
else:
@@ -71,34 +72,39 @@ def min_os_version(self):
7172
"""Gets the min supported OS version.
7273
7374
Returns:
74-
string, the min supported OS version.
75+
float, the min supported OS version.
7576
"""
7677
if not self._min_os_version:
77-
min_os_version = self.profile_plist_obj.GetPlistField('minRuntimeVersion')
78-
# Cut build version. E.g., cut 9.3.3 to 9.3.
79-
if min_os_version.count('.') > 1:
80-
min_os_version = min_os_version[:min_os_version.rfind('.')]
81-
self._min_os_version = min_os_version
78+
min_os_version_str = self.profile_plist_obj.GetPlistField(
79+
'minRuntimeVersion')
80+
self._min_os_version = _extra_os_version(min_os_version_str)
8281
return self._min_os_version
8382

8483
@property
8584
def max_os_version(self):
8685
"""Gets the max supported OS version.
8786
8887
Returns:
89-
string, the max supported OS version.
88+
float, the max supported OS version or None if it is not found.
9089
"""
9190
if not self._max_os_version:
9291
# If the profile.plist does not have maxRuntimeVersion field, it means
9392
# it supports the max OS version of current iphonesimulator platform.
9493
try:
95-
max_os_version = self.profile_plist_obj.GetPlistField(
94+
max_os_version_str = self.profile_plist_obj.GetPlistField(
9695
'maxRuntimeVersion')
9796
except ios_errors.PlistError:
98-
max_os_version = xcode_info_util.GetSdkVersion(
99-
ios_constants.SDK.IPHONESIMULATOR)
100-
# Cut build version. E.g., cut 9.3.3 to 9.3.
101-
if max_os_version.count('.') > 1:
102-
max_os_version = max_os_version[:max_os_version.rfind('.')]
103-
self._max_os_version = max_os_version
97+
return None
98+
self._max_os_version = _extra_os_version(max_os_version_str)
10499
return self._max_os_version
100+
101+
102+
def _extra_os_version(os_version_str):
103+
"""Extracts os version float value from a given string."""
104+
# Cut build version. E.g., cut 9.3.3 to 9.3.
105+
if os_version_str.count('.') > 1:
106+
os_version_str = os_version_str[:os_version_str.rfind('.')]
107+
# We need to round the os version string in the simulator profile. E.g.,
108+
# the maxRuntimeVersion of iPhone 5 is 10.255.255 and we could create iOS 10.3
109+
# for iPhone 5.
110+
return round(float(os_version_str), 1)

xctestrunner/simulator_control/simulator_util.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,14 @@ def Shutdown(self):
134134
self.WaitUntilStateShutdown()
135135
logging.info('Shut down simulator %s.', self.simulator_id)
136136

137-
def Delete(self):
138-
"""Deletes the simulator asynchronously.
137+
def Delete(self, asynchronously=True):
138+
"""Deletes the simulator.
139139
140140
The simulator state should be SHUTDOWN when deleting it. Otherwise, it will
141141
raise exception.
142142
143+
Args:
144+
asynchronously: whether deleting the simulator asynchronously.
143145
Raises:
144146
ios_errors.SimError: The simulator's state is not SHUTDOWN.
145147
"""
@@ -151,11 +153,21 @@ def Delete(self):
151153
raise ios_errors.SimError(
152154
'Can only delete the simulator with state SHUTDOWN. The current '
153155
'state of simulator %s is %s.' % (self._simulator_id, sim_state))
154-
logging.info('Deleting simulator %s asynchronously.', self.simulator_id)
155-
subprocess.Popen(['xcrun', 'simctl', 'delete', self.simulator_id],
156-
stdout=subprocess.PIPE,
157-
stderr=subprocess.PIPE,
158-
preexec_fn=os.setpgrp)
156+
command = ['xcrun', 'simctl', 'delete', self.simulator_id]
157+
if asynchronously:
158+
logging.info('Deleting simulator %s asynchronously.', self.simulator_id)
159+
subprocess.Popen(
160+
command,
161+
stdout=subprocess.PIPE,
162+
stderr=subprocess.PIPE,
163+
preexec_fn=os.setpgrp)
164+
else:
165+
try:
166+
RunSimctlCommand(command)
167+
logging.info('Deleted simulator %s.', self.simulator_id)
168+
except ios_errors.SimError as e:
169+
raise ios_errors.SimError('Failed to delete simulator %s: %s' %
170+
(self.simulator_id, str(e)))
159171
# The delete command won't delete the simulator log directory.
160172
if os.path.exists(self.simulator_log_root_dir):
161173
shutil.rmtree(self.simulator_log_root_dir, ignore_errors=True)
@@ -413,9 +425,8 @@ def GetLastSupportedIphoneSimType(os_version):
413425
os_version_float = float(os_version)
414426
for sim_type in supported_sim_types:
415427
if sim_type.startswith('iPhone'):
416-
min_os_version_float = float(
417-
simtype_profile.SimTypeProfile(sim_type).min_os_version)
418-
if os_version_float >= min_os_version_float:
428+
min_os_version = simtype_profile.SimTypeProfile(sim_type).min_os_version
429+
if os_version_float >= min_os_version:
419430
return sim_type
420431
raise ios_errors.SimError('Can not find supported iPhone simulator type.')
421432

@@ -520,17 +531,19 @@ def GetLastSupportedSimOsVersion(os_type=ios_constants.OS.IOS,
520531
if not device_type:
521532
return supported_os_versions[-1]
522533

523-
simtype_max_os_version_float = float(
524-
simtype_profile.SimTypeProfile(device_type).max_os_version)
534+
max_os_version = simtype_profile.SimTypeProfile(device_type).max_os_version
535+
# The supported os versions will be from latest to older after reverse().
525536
supported_os_versions.reverse()
537+
if not max_os_version:
538+
return supported_os_versions[0]
539+
526540
for os_version in supported_os_versions:
527-
if float(os_version) <= simtype_max_os_version_float:
541+
if float(os_version) <= max_os_version:
528542
return os_version
529-
if not supported_os_versions:
530-
raise ios_errors.IllegalArgumentError(
531-
'The supported OS version %s can not match simulator type %s. Because '
532-
'its max OS version is %s' %
533-
(supported_os_versions, device_type, simtype_max_os_version_float))
543+
raise ios_errors.IllegalArgumentError(
544+
'The supported OS version %s can not match simulator type %s. Because '
545+
'its max OS version is %s' %
546+
(supported_os_versions, device_type, max_os_version))
534547

535548

536549
def GetOsType(device_type):
@@ -598,16 +611,17 @@ def _ValidateSimulatorTypeWithOsVersion(device_type, os_version):
598611
"""
599612
os_version_float = float(os_version)
600613
sim_profile = simtype_profile.SimTypeProfile(device_type)
601-
min_os_version_float = float(sim_profile.min_os_version)
602-
if min_os_version_float > os_version_float:
614+
min_os_version = sim_profile.min_os_version
615+
if min_os_version > os_version_float:
603616
raise ios_errors.IllegalArgumentError(
604-
'The min OS version of %s is %s. But current OS version is %s' %
605-
(device_type, min_os_version_float, os_version))
606-
max_os_version_float = float(sim_profile.max_os_version)
607-
if max_os_version_float < os_version_float:
608-
raise ios_errors.IllegalArgumentError(
609-
'The max OS version of %s is %s. But current OS version is %s' %
610-
(device_type, max_os_version_float, os_version))
617+
'The min OS version of %s is %f. But current OS version is %s' %
618+
(device_type, min_os_version, os_version))
619+
max_os_version = sim_profile.max_os_version
620+
if max_os_version:
621+
if max_os_version < os_version_float:
622+
raise ios_errors.IllegalArgumentError(
623+
'The max OS version of %s is %f. But current OS version is %s' %
624+
(device_type, max_os_version, os_version))
611625

612626

613627
def QuitSimulatorApp():

xctestrunner/test_runner/ios_test_runner.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ def _AddTestSubParser(subparsers):
107107
def _Test(args):
108108
"""The function of sub command `test`."""
109109
sdk = _PlatformToSdk(args.platform) if args.platform else _GetSdk(args.id)
110+
device_arch = _GetDeviceArch(args.id, sdk)
110111
with xctest_session.XctestSession(
111-
sdk=sdk, work_dir=args.work_dir, output_dir=args.output_dir) as session:
112+
sdk=sdk,
113+
device_arch=device_arch,
114+
work_dir=args.work_dir,
115+
output_dir=args.output_dir) as session:
112116
session.Prepare(
113117
app_under_test=args.app_under_test_path,
114118
test_bundle=args.test_bundle_path,
@@ -142,6 +146,7 @@ def _RunSimulatorTest(args):
142146
"""The function of running test with new simulator."""
143147
with xctest_session.XctestSession(
144148
sdk=ios_constants.SDK.IPHONESIMULATOR,
149+
device_arch=ios_constants.ARCH.X86_64,
145150
work_dir=args.work_dir, output_dir=args.output_dir) as session:
146151
session.Prepare(
147152
app_under_test=args.app_under_test_path,
@@ -293,6 +298,17 @@ def _GetSdk(device_id):
293298
(device_id, known_devices_output))
294299

295300

301+
def _GetDeviceArch(device_id, sdk):
302+
"""Gets the device architecture."""
303+
# It is a temporary soluton to get device architecture. Checking i386 and
304+
# armv7/armv7s is not supported.
305+
if sdk == ios_constants.SDK.IPHONESIMULATOR:
306+
return ios_constants.ARCH.X86_64
307+
if '-' in device_id:
308+
return ios_constants.ARCH.ARM64E
309+
return ios_constants.ARCH.ARM64
310+
311+
296312
def main(argv):
297313
args = _BuildParser().parse_args(argv[1:])
298314
if args.verbose:

xctestrunner/test_runner/xcodebuild_test_executor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
_TOO_MANY_INSTANCES_ALREADY_RUNNING = ('Too many instances of this service are '
5050
'already running.')
5151
_LOST_CONNECTION_ERROR = 'Lost connection to testmanagerd'
52+
_LOST_CONNECTION_TO_DTSERVICEHUB_ERROR = 'Lost connection to DTServiceHub'
5253

5354

5455
class CheckXcodebuildStuckThread(threading.Thread):
@@ -211,9 +212,14 @@ def Execute(self, return_output=True):
211212
output_str = output.getvalue()
212213
if self._sdk == ios_constants.SDK.IPHONEOS:
213214
if ((re.search(_DEVICE_TYPE_WAS_NULL_PATTERN, output_str) or
214-
_LOST_CONNECTION_ERROR in output_str) and i < max_attempts - 1):
215+
_LOST_CONNECTION_ERROR in output_str or
216+
_LOST_CONNECTION_TO_DTSERVICEHUB_ERROR in output_str) and
217+
i < max_attempts - 1):
215218
logging.warning(
216-
'Failed to launch test on the device. Will relaunch again.')
219+
'Failed to launch test on the device. Will relaunch again '
220+
'after 5s.'
221+
)
222+
time.sleep(5)
217223
continue
218224
if _TOO_MANY_INSTANCES_ALREADY_RUNNING in output_str:
219225
return (runner_exit_codes.EXITCODE.NEED_REBOOT_DEVICE,

xctestrunner/test_runner/xctest_session.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
class XctestSession(object):
3535
"""The class that runs XCTEST based tests."""
3636

37-
def __init__(self, sdk, work_dir=None, output_dir=None):
37+
def __init__(self, sdk, device_arch, work_dir=None, output_dir=None):
3838
"""Initializes the XctestSession object.
3939
4040
If work_dir is not provdied, will create a temp direcotry to be work_dir and
@@ -43,6 +43,7 @@ def __init__(self, sdk, work_dir=None, output_dir=None):
4343
4444
Args:
4545
sdk: ios_constants.SDK. The sdk of the target device.
46+
device_arch: ios_constants.ARCH. The architecture of the target device.
4647
work_dir: string, the working directory contains runfiles.
4748
output_dir: string, The directory where derived data will go, including:
4849
1) the detailed test session log which includes test output and the
@@ -51,6 +52,7 @@ def __init__(self, sdk, work_dir=None, output_dir=None):
5152
specified, the directory will not be deleted after test ends.'
5253
"""
5354
self._sdk = sdk
55+
self._device_arch = device_arch
5456
self._work_dir = work_dir
5557
self._delete_work_dir = True
5658
self._output_dir = output_dir
@@ -146,8 +148,8 @@ def Prepare(self, app_under_test=None, test_bundle=None,
146148
test_type != ios_constants.TestType.LOGIC_TEST and
147149
xcode_info_util.GetXcodeVersionNumber() >= 800):
148150
xctestrun_factory = xctestrun.XctestRunFactory(
149-
app_under_test_dir, test_bundle_dir, self._sdk, test_type,
150-
signing_options, self._work_dir)
151+
app_under_test_dir, test_bundle_dir, self._sdk, self._device_arch,
152+
test_type, signing_options, self._work_dir)
151153
self._xctestrun_obj = xctestrun_factory.GenerateXctestrun()
152154
elif test_type == ios_constants.TestType.XCUITEST:
153155
raise ios_errors.IllegalArgumentError(

xctestrunner/test_runner/xctestrun.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ class XctestRunFactory(object):
251251

252252
def __init__(self, app_under_test_dir, test_bundle_dir,
253253
sdk=ios_constants.SDK.IPHONESIMULATOR,
254+
device_arch=ios_constants.ARCH.X86_64,
254255
test_type=ios_constants.TestType.XCUITEST,
255256
signing_options=None, work_dir=None):
256257
"""Initializes the XctestRun object.
@@ -263,6 +264,7 @@ def __init__(self, app_under_test_dir, test_bundle_dir,
263264
test_bundle_dir: string, path of the test bundle.
264265
sdk: string, SDKRoot of the test. See supported SDKs in module
265266
xctestrunner.shared.ios_constants.
267+
device_arch: ios_constants.ARCH. The architecture of the target device.
266268
test_type: string, test type of the test bundle. See supported test types
267269
in module xctestrunner.shared.ios_constants.
268270
signing_options: dict, the signing app options. See
@@ -276,6 +278,7 @@ def __init__(self, app_under_test_dir, test_bundle_dir,
276278
self._test_bundle_dir = test_bundle_dir
277279
self._test_name = os.path.splitext(os.path.basename(test_bundle_dir))[0]
278280
self._sdk = sdk
281+
self._device_arch = device_arch
279282
self._test_type = test_type
280283
if self._sdk == ios_constants.SDK.IPHONEOS:
281284
self._on_device = True
@@ -468,7 +471,7 @@ def _GenerateTestRootForXcuitest(self):
468471
bundle_util.CodesignBundle(self._app_under_test_dir)
469472

470473
platform_name = 'iPhoneOS' if self._on_device else 'iPhoneSimulator'
471-
developer_path = '__PLATFORMS__/%s.platform/Developer/' % platform_name
474+
developer_path = '__PLATFORMS__/%s.platform/Developer' % platform_name
472475
test_envs = {
473476
'DYLD_FRAMEWORK_PATH': '__TESTROOT__:{developer}/Library/Frameworks:'
474477
'{developer}/Library/PrivateFrameworks'.format(
@@ -516,9 +519,20 @@ def _GetUitestRunnerAppFromXcode(self, platform_library_path):
516519
uitest_runner_app = os.path.join(self._test_root_dir,
517520
uitest_runner_app_name + '.app')
518521
shutil.copytree(xctrunner_app, uitest_runner_app)
522+
uitest_runner_exec = os.path.join(uitest_runner_app, uitest_runner_app_name)
519523
shutil.move(
520-
os.path.join(uitest_runner_app, 'XCTRunner'),
521-
os.path.join(uitest_runner_app, uitest_runner_app_name))
524+
os.path.join(uitest_runner_app, 'XCTRunner'), uitest_runner_exec)
525+
# XCTRunner is multi-archs. When launching XCTRunner on arm64e device, it
526+
# will be launched as arm64e process by default. If the test bundle is arm64
527+
# bundle, the XCTRunner which hosts the test bundle will failed to be
528+
# launched. So removing the arm64e arch from XCTRunner can resolve this
529+
# case.
530+
if self._device_arch == ios_constants.ARCH.ARM64E:
531+
test_executable = os.path.join(self._test_bundle_dir, test_bundle_name)
532+
test_archs = bundle_util.GetFileArchTypes(test_executable)
533+
if ios_constants.ARCH.ARM64E not in test_archs:
534+
bundle_util.RemoveArchType(uitest_runner_exec,
535+
ios_constants.ARCH.ARM64E)
522536

523537
runner_app_info_plist_path = os.path.join(uitest_runner_app, 'Info.plist')
524538
info_plist = plist_util.Plist(runner_app_info_plist_path)
@@ -606,7 +620,7 @@ def _GenerateTestRootForXctest(self):
606620
app_under_test_name = os.path.splitext(
607621
os.path.basename(self._app_under_test_dir))[0]
608622
platform_name = 'iPhoneOS' if self._on_device else 'iPhoneSimulator'
609-
developer_path = '__PLATFORMS__/%s.platform/Developer/' % platform_name
623+
developer_path = '__PLATFORMS__/%s.platform/Developer' % platform_name
610624
if xcode_info_util.GetXcodeVersionNumber() < 1000:
611625
dyld_insert_libs = ('%s/Library/PrivateFrameworks/'
612626
'IDEBundleInjection.framework/IDEBundleInjection' %

0 commit comments

Comments
 (0)