Skip to content

Commit

Permalink
Update test runner with internal change
Browse files Browse the repository at this point in the history
- Fix the minSupportedVersion issue for simulator creation.
- Allow running arm64 test bundle on arm64e iOS device.
  • Loading branch information
albertdai committed Feb 10, 2020
1 parent d2b9ed9 commit aa821f3
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 53 deletions.
12 changes: 12 additions & 0 deletions xctestrunner/shared/bundle_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,18 @@ def EnableUIFileSharing(bundle_path, resigning=True):
CodesignBundle(bundle_path)


def GetFileArchTypes(file_path):
"""Gets the architecture types of the file."""
output = subprocess.check_output(['/usr/bin/lipo', file_path, '-archs'])
return output.split(' ')


def RemoveArchType(file_path, arch_type):
"""Remove the given architecture types for the file."""
subprocess.check_call(
['/usr/bin/lipo', file_path, '-remove', arch_type, '-output', file_path])


def _ExtractBundleFile(target_dir, bundle_extension):
"""Extract single bundle file with given extension.
Expand Down
7 changes: 7 additions & 0 deletions xctestrunner/shared/ios_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ def enum(**enums):
return type('Enum', (), enums)


ARCH = enum(
ARMV7='armv7',
ARMV7S='armv7s',
ARM64='arm64',
ARM64E='arm64e',
I386='i386',
X86_64='x86_64')
SDK = enum(IPHONEOS='iphoneos', IPHONESIMULATOR='iphonesimulator')
# It is consistent with bazel's apple platform:
# https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/rules/apple/ApplePlatform.java
Expand Down
1 change: 1 addition & 0 deletions xctestrunner/shared/xcode_info_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def GetXcodeDeveloperPath():
# path to that fallback directory.
# See https://github.com/bazelbuild/rules_apple/issues/684 for context.
def GetSwift5FallbackLibsDir():
"""Gets the directory for Swift5 fallback libraries."""
relativePath = "Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0"
swiftLibsDir = os.path.join(GetXcodeDeveloperPath(), relativePath)
swiftLibPlatformDir = os.path.join(swiftLibsDir, ios_constants.SDK.IPHONESIMULATOR)
Expand Down
38 changes: 22 additions & 16 deletions xctestrunner/simulator_control/simtype_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ def profile_plist_obj(self):
profile.plist.
"""
if not self._profile_plist_obj:
if xcode_info_util.GetXcodeVersionNumber() >= 900:
xcode_version = xcode_info_util.GetXcodeVersionNumber()
if xcode_version >= 900:
platform_path = xcode_info_util.GetSdkPlatformPath(
ios_constants.SDK.IPHONEOS)
else:
platform_path = xcode_info_util.GetSdkPlatformPath(
ios_constants.SDK.IPHONESIMULATOR)
if xcode_info_util.GetXcodeVersionNumber() >= 1100:
if xcode_version >= 1100:
sim_profiles_dir = os.path.join(
platform_path, 'Library/Developer/CoreSimulator/Profiles')
else:
Expand All @@ -71,34 +72,39 @@ def min_os_version(self):
"""Gets the min supported OS version.
Returns:
string, the min supported OS version.
float, the min supported OS version.
"""
if not self._min_os_version:
min_os_version = self.profile_plist_obj.GetPlistField('minRuntimeVersion')
# Cut build version. E.g., cut 9.3.3 to 9.3.
if min_os_version.count('.') > 1:
min_os_version = min_os_version[:min_os_version.rfind('.')]
self._min_os_version = min_os_version
min_os_version_str = self.profile_plist_obj.GetPlistField(
'minRuntimeVersion')
self._min_os_version = _extra_os_version(min_os_version_str)
return self._min_os_version

@property
def max_os_version(self):
"""Gets the max supported OS version.
Returns:
string, the max supported OS version.
float, the max supported OS version or None if it is not found.
"""
if not self._max_os_version:
# If the profile.plist does not have maxRuntimeVersion field, it means
# it supports the max OS version of current iphonesimulator platform.
try:
max_os_version = self.profile_plist_obj.GetPlistField(
max_os_version_str = self.profile_plist_obj.GetPlistField(
'maxRuntimeVersion')
except ios_errors.PlistError:
max_os_version = xcode_info_util.GetSdkVersion(
ios_constants.SDK.IPHONESIMULATOR)
# Cut build version. E.g., cut 9.3.3 to 9.3.
if max_os_version.count('.') > 1:
max_os_version = max_os_version[:max_os_version.rfind('.')]
self._max_os_version = max_os_version
return None
self._max_os_version = _extra_os_version(max_os_version_str)
return self._max_os_version


def _extra_os_version(os_version_str):
"""Extracts os version float value from a given string."""
# Cut build version. E.g., cut 9.3.3 to 9.3.
if os_version_str.count('.') > 1:
os_version_str = os_version_str[:os_version_str.rfind('.')]
# We need to round the os version string in the simulator profile. E.g.,
# the maxRuntimeVersion of iPhone 5 is 10.255.255 and we could create iOS 10.3
# for iPhone 5.
return round(float(os_version_str), 1)
68 changes: 41 additions & 27 deletions xctestrunner/simulator_control/simulator_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,14 @@ def Shutdown(self):
self.WaitUntilStateShutdown()
logging.info('Shut down simulator %s.', self.simulator_id)

def Delete(self):
"""Deletes the simulator asynchronously.
def Delete(self, asynchronously=True):
"""Deletes the simulator.
The simulator state should be SHUTDOWN when deleting it. Otherwise, it will
raise exception.
Args:
asynchronously: whether deleting the simulator asynchronously.
Raises:
ios_errors.SimError: The simulator's state is not SHUTDOWN.
"""
Expand All @@ -151,11 +153,21 @@ def Delete(self):
raise ios_errors.SimError(
'Can only delete the simulator with state SHUTDOWN. The current '
'state of simulator %s is %s.' % (self._simulator_id, sim_state))
logging.info('Deleting simulator %s asynchronously.', self.simulator_id)
subprocess.Popen(['xcrun', 'simctl', 'delete', self.simulator_id],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=os.setpgrp)
command = ['xcrun', 'simctl', 'delete', self.simulator_id]
if asynchronously:
logging.info('Deleting simulator %s asynchronously.', self.simulator_id)
subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=os.setpgrp)
else:
try:
RunSimctlCommand(command)
logging.info('Deleted simulator %s.', self.simulator_id)
except ios_errors.SimError as e:
raise ios_errors.SimError('Failed to delete simulator %s: %s' %
(self.simulator_id, str(e)))
# The delete command won't delete the simulator log directory.
if os.path.exists(self.simulator_log_root_dir):
shutil.rmtree(self.simulator_log_root_dir, ignore_errors=True)
Expand Down Expand Up @@ -413,9 +425,8 @@ def GetLastSupportedIphoneSimType(os_version):
os_version_float = float(os_version)
for sim_type in supported_sim_types:
if sim_type.startswith('iPhone'):
min_os_version_float = float(
simtype_profile.SimTypeProfile(sim_type).min_os_version)
if os_version_float >= min_os_version_float:
min_os_version = simtype_profile.SimTypeProfile(sim_type).min_os_version
if os_version_float >= min_os_version:
return sim_type
raise ios_errors.SimError('Can not find supported iPhone simulator type.')

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

simtype_max_os_version_float = float(
simtype_profile.SimTypeProfile(device_type).max_os_version)
max_os_version = simtype_profile.SimTypeProfile(device_type).max_os_version
# The supported os versions will be from latest to older after reverse().
supported_os_versions.reverse()
if not max_os_version:
return supported_os_versions[0]

for os_version in supported_os_versions:
if float(os_version) <= simtype_max_os_version_float:
if float(os_version) <= max_os_version:
return os_version
if not supported_os_versions:
raise ios_errors.IllegalArgumentError(
'The supported OS version %s can not match simulator type %s. Because '
'its max OS version is %s' %
(supported_os_versions, device_type, simtype_max_os_version_float))
raise ios_errors.IllegalArgumentError(
'The supported OS version %s can not match simulator type %s. Because '
'its max OS version is %s' %
(supported_os_versions, device_type, max_os_version))


def GetOsType(device_type):
Expand Down Expand Up @@ -598,16 +611,17 @@ def _ValidateSimulatorTypeWithOsVersion(device_type, os_version):
"""
os_version_float = float(os_version)
sim_profile = simtype_profile.SimTypeProfile(device_type)
min_os_version_float = float(sim_profile.min_os_version)
if min_os_version_float > os_version_float:
min_os_version = sim_profile.min_os_version
if min_os_version > os_version_float:
raise ios_errors.IllegalArgumentError(
'The min OS version of %s is %s. But current OS version is %s' %
(device_type, min_os_version_float, os_version))
max_os_version_float = float(sim_profile.max_os_version)
if max_os_version_float < os_version_float:
raise ios_errors.IllegalArgumentError(
'The max OS version of %s is %s. But current OS version is %s' %
(device_type, max_os_version_float, os_version))
'The min OS version of %s is %f. But current OS version is %s' %
(device_type, min_os_version, os_version))
max_os_version = sim_profile.max_os_version
if max_os_version:
if max_os_version < os_version_float:
raise ios_errors.IllegalArgumentError(
'The max OS version of %s is %f. But current OS version is %s' %
(device_type, max_os_version, os_version))


def QuitSimulatorApp():
Expand Down
18 changes: 17 additions & 1 deletion xctestrunner/test_runner/ios_test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,12 @@ def _AddTestSubParser(subparsers):
def _Test(args):
"""The function of sub command `test`."""
sdk = _PlatformToSdk(args.platform) if args.platform else _GetSdk(args.id)
device_arch = _GetDeviceArch(args.id, sdk)
with xctest_session.XctestSession(
sdk=sdk, work_dir=args.work_dir, output_dir=args.output_dir) as session:
sdk=sdk,
device_arch=device_arch,
work_dir=args.work_dir,
output_dir=args.output_dir) as session:
session.Prepare(
app_under_test=args.app_under_test_path,
test_bundle=args.test_bundle_path,
Expand Down Expand Up @@ -142,6 +146,7 @@ def _RunSimulatorTest(args):
"""The function of running test with new simulator."""
with xctest_session.XctestSession(
sdk=ios_constants.SDK.IPHONESIMULATOR,
device_arch=ios_constants.ARCH.X86_64,
work_dir=args.work_dir, output_dir=args.output_dir) as session:
session.Prepare(
app_under_test=args.app_under_test_path,
Expand Down Expand Up @@ -293,6 +298,17 @@ def _GetSdk(device_id):
(device_id, known_devices_output))


def _GetDeviceArch(device_id, sdk):
"""Gets the device architecture."""
# It is a temporary soluton to get device architecture. Checking i386 and
# armv7/armv7s is not supported.
if sdk == ios_constants.SDK.IPHONESIMULATOR:
return ios_constants.ARCH.X86_64
if '-' in device_id:
return ios_constants.ARCH.ARM64E
return ios_constants.ARCH.ARM64


def main(argv):
args = _BuildParser().parse_args(argv[1:])
if args.verbose:
Expand Down
10 changes: 8 additions & 2 deletions xctestrunner/test_runner/xcodebuild_test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
_TOO_MANY_INSTANCES_ALREADY_RUNNING = ('Too many instances of this service are '
'already running.')
_LOST_CONNECTION_ERROR = 'Lost connection to testmanagerd'
_LOST_CONNECTION_TO_DTSERVICEHUB_ERROR = 'Lost connection to DTServiceHub'


class CheckXcodebuildStuckThread(threading.Thread):
Expand Down Expand Up @@ -211,9 +212,14 @@ def Execute(self, return_output=True):
output_str = output.getvalue()
if self._sdk == ios_constants.SDK.IPHONEOS:
if ((re.search(_DEVICE_TYPE_WAS_NULL_PATTERN, output_str) or
_LOST_CONNECTION_ERROR in output_str) and i < max_attempts - 1):
_LOST_CONNECTION_ERROR in output_str or
_LOST_CONNECTION_TO_DTSERVICEHUB_ERROR in output_str) and
i < max_attempts - 1):
logging.warning(
'Failed to launch test on the device. Will relaunch again.')
'Failed to launch test on the device. Will relaunch again '
'after 5s.'
)
time.sleep(5)
continue
if _TOO_MANY_INSTANCES_ALREADY_RUNNING in output_str:
return (runner_exit_codes.EXITCODE.NEED_REBOOT_DEVICE,
Expand Down
8 changes: 5 additions & 3 deletions xctestrunner/test_runner/xctest_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
class XctestSession(object):
"""The class that runs XCTEST based tests."""

def __init__(self, sdk, work_dir=None, output_dir=None):
def __init__(self, sdk, device_arch, work_dir=None, output_dir=None):
"""Initializes the XctestSession object.
If work_dir is not provdied, will create a temp direcotry to be work_dir and
Expand All @@ -43,6 +43,7 @@ def __init__(self, sdk, work_dir=None, output_dir=None):
Args:
sdk: ios_constants.SDK. The sdk of the target device.
device_arch: ios_constants.ARCH. The architecture of the target device.
work_dir: string, the working directory contains runfiles.
output_dir: string, The directory where derived data will go, including:
1) the detailed test session log which includes test output and the
Expand All @@ -51,6 +52,7 @@ def __init__(self, sdk, work_dir=None, output_dir=None):
specified, the directory will not be deleted after test ends.'
"""
self._sdk = sdk
self._device_arch = device_arch
self._work_dir = work_dir
self._delete_work_dir = True
self._output_dir = output_dir
Expand Down Expand Up @@ -146,8 +148,8 @@ def Prepare(self, app_under_test=None, test_bundle=None,
test_type != ios_constants.TestType.LOGIC_TEST and
xcode_info_util.GetXcodeVersionNumber() >= 800):
xctestrun_factory = xctestrun.XctestRunFactory(
app_under_test_dir, test_bundle_dir, self._sdk, test_type,
signing_options, self._work_dir)
app_under_test_dir, test_bundle_dir, self._sdk, self._device_arch,
test_type, signing_options, self._work_dir)
self._xctestrun_obj = xctestrun_factory.GenerateXctestrun()
elif test_type == ios_constants.TestType.XCUITEST:
raise ios_errors.IllegalArgumentError(
Expand Down
22 changes: 18 additions & 4 deletions xctestrunner/test_runner/xctestrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class XctestRunFactory(object):

def __init__(self, app_under_test_dir, test_bundle_dir,
sdk=ios_constants.SDK.IPHONESIMULATOR,
device_arch=ios_constants.ARCH.X86_64,
test_type=ios_constants.TestType.XCUITEST,
signing_options=None, work_dir=None):
"""Initializes the XctestRun object.
Expand All @@ -263,6 +264,7 @@ def __init__(self, app_under_test_dir, test_bundle_dir,
test_bundle_dir: string, path of the test bundle.
sdk: string, SDKRoot of the test. See supported SDKs in module
xctestrunner.shared.ios_constants.
device_arch: ios_constants.ARCH. The architecture of the target device.
test_type: string, test type of the test bundle. See supported test types
in module xctestrunner.shared.ios_constants.
signing_options: dict, the signing app options. See
Expand All @@ -276,6 +278,7 @@ def __init__(self, app_under_test_dir, test_bundle_dir,
self._test_bundle_dir = test_bundle_dir
self._test_name = os.path.splitext(os.path.basename(test_bundle_dir))[0]
self._sdk = sdk
self._device_arch = device_arch
self._test_type = test_type
if self._sdk == ios_constants.SDK.IPHONEOS:
self._on_device = True
Expand Down Expand Up @@ -468,7 +471,7 @@ def _GenerateTestRootForXcuitest(self):
bundle_util.CodesignBundle(self._app_under_test_dir)

platform_name = 'iPhoneOS' if self._on_device else 'iPhoneSimulator'
developer_path = '__PLATFORMS__/%s.platform/Developer/' % platform_name
developer_path = '__PLATFORMS__/%s.platform/Developer' % platform_name
test_envs = {
'DYLD_FRAMEWORK_PATH': '__TESTROOT__:{developer}/Library/Frameworks:'
'{developer}/Library/PrivateFrameworks'.format(
Expand Down Expand Up @@ -516,9 +519,20 @@ def _GetUitestRunnerAppFromXcode(self, platform_library_path):
uitest_runner_app = os.path.join(self._test_root_dir,
uitest_runner_app_name + '.app')
shutil.copytree(xctrunner_app, uitest_runner_app)
uitest_runner_exec = os.path.join(uitest_runner_app, uitest_runner_app_name)
shutil.move(
os.path.join(uitest_runner_app, 'XCTRunner'),
os.path.join(uitest_runner_app, uitest_runner_app_name))
os.path.join(uitest_runner_app, 'XCTRunner'), uitest_runner_exec)
# XCTRunner is multi-archs. When launching XCTRunner on arm64e device, it
# will be launched as arm64e process by default. If the test bundle is arm64
# bundle, the XCTRunner which hosts the test bundle will failed to be
# launched. So removing the arm64e arch from XCTRunner can resolve this
# case.
if self._device_arch == ios_constants.ARCH.ARM64E:
test_executable = os.path.join(self._test_bundle_dir, test_bundle_name)
test_archs = bundle_util.GetFileArchTypes(test_executable)
if ios_constants.ARCH.ARM64E not in test_archs:
bundle_util.RemoveArchType(uitest_runner_exec,
ios_constants.ARCH.ARM64E)

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

0 comments on commit aa821f3

Please sign in to comment.