def GetAppDocumentsPath(self, app_bundle_id): """Gets the path of the app's Documents directory.""" if xcode_info_util.GetXcodeVersionNumber() >= 830: try: app_data_container = RunSimctlCommand([ 'xcrun', 'simctl', 'get_app_container', self._simulator_id, app_bundle_id, 'data' ]) return os.path.join(app_data_container, 'Documents') except ios_errors.SimError as e: raise ios_errors.SimError( 'Failed to get data container of the app %s in simulator %s: %s' % (app_bundle_id, self._simulator_id, str(e))) apps_dir = os.path.join(self.simulator_root_dir, 'data/Containers/Data/Application') for sub_dir_name in os.listdir(apps_dir): container_manager_plist = plist_util.Plist( os.path.join( apps_dir, sub_dir_name, '.com.apple.mobile_container_manager.metadata.plist')) current_app_bundle_id = container_manager_plist.GetPlistField( 'MCMMetadataIdentifier') if current_app_bundle_id == app_bundle_id: return os.path.join(apps_dir, sub_dir_name, 'Documents') raise ios_errors.SimError( 'Failed to get Documents directory of the app %s in simulator %s' % (app_bundle_id, self._simulator_id))
def Delete(self): """Deletes the simulator. The simulator state should be SHUTDOWN when deleting it. Otherwise, it will raise exception. Raises: ios_errors.SimError: The simulator's state is not SHUTDOWN. """ sim_state = self.GetSimulatorState() if sim_state != ios_constants.SimState.SHUTDOWN: 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)) try: _RunSimctlCommand(['xcrun', 'simctl', 'delete', 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) logging.info('Deleted simulator %s.', self.simulator_id) self._simulator_id = None
def GetLastSupportedIphoneSimType(os_version): """"Gets the last supported iPhone simulator type of the given OS version. Currently, the last supported iPhone simulator type is the last iPhone from the output of `xcrun simctl list devicetypes`. Args: os_version: string, OS version of the new simulator. The format is {major}.{minor}, such as 9.3, 10.2. Returns: a string, the last supported iPhone simulator type. Raises: ios_errors.SimError: when there is no supported iPhone simulator type. """ supported_sim_types = GetSupportedSimDeviceTypes(ios_constants.OS.IOS) supported_sim_types.reverse() 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: return sim_type raise ios_errors.SimError('Can not find supported iPhone simulator type.')
def FetchLogToFile(self, output_file_path, start_time=None, end_time=None): """Gets simulator log via running `log` tool on simulator. Args: output_file_path: string, the path of the stdout file. start_time: datetime, the start time of the simulatro log. end_time: datetime, the end time of the simulatro log. """ command = [ 'xcrun', 'simctl', 'spawn', self._simulator_id, 'log', 'show', '--style', 'syslog' ] if start_time: command.extend( ('--start', start_time.strftime('%Y-%m-%d %H:%M:%S'))) if end_time: command.extend(('--end', end_time.strftime('%Y-%m-%d %H:%M:%S'))) with open(output_file_path, 'w') as stdout_file: try: subprocess.Popen(command, stdout=stdout_file, stderr=subprocess.STDOUT) except ios_errors.SimError as e: raise ios_errors.SimError( 'Failed to get log on simulator %s: %s' % (self.simulator_id, str(e)))
def Delete(self): """Deletes the simulator asynchronously. The simulator state should be SHUTDOWN when deleting it. Otherwise, it will raise exception. Raises: ios_errors.SimError: The simulator's state is not SHUTDOWN. """ # In Xcode 9+, simctl can delete Booted simulator. In prior of Xcode 9, # we have to shutdown the simulator first before deleting it. if xcode_info_util.GetXcodeVersionNumber() < 900: sim_state = self.GetSimulatorState() if sim_state != ios_constants.SimState.SHUTDOWN: 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) # 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) self._simulator_id = None
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. """ 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) self._simulator_id = None
def Shutdown(self): """Shuts down the simulator.""" sim_state = self.GetSimulatorState() if sim_state == ios_constants.SimState.SHUTDOWN: logging.info('Simulator %s has already shut down.', self.simulator_id) return if sim_state == ios_constants.SimState.CREATING: raise ios_errors.SimError( 'Can not shut down the simulator in state CREATING.') logging.info('Shutting down simulator %s.', self.simulator_id) try: RunSimctlCommand(['xcrun', 'simctl', 'shutdown', self.simulator_id]) except ios_errors.SimError as e: if 'Unable to shutdown device in current state: Shutdown' in str(e): logging.info('Simulator %s has already shut down.', self.simulator_id) return raise ios_errors.SimError('Failed to shutdown simulator %s: %s' % (self.simulator_id, str(e))) self.WaitUntilStateShutdown() logging.info('Shut down simulator %s.', self.simulator_id)
def RunSimctlCommand(command): """Runs simctl command.""" for i in range(_SIMCTL_MAX_ATTEMPTS): process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') stdout, stderr = process.communicate() all_output = '\n'.join([stdout, stderr]) output = stdout.strip() if process.poll() != 0: if (i < (_SIMCTL_MAX_ATTEMPTS - 1) and ios_constants.CORESIMULATOR_INTERRUPTED_ERROR in all_output): continue raise ios_errors.SimError(output) return output
def WaitUntilStateBooted(self, timeout_sec=_SIMULATOR_BOOTED_TIMEOUT_SEC): """Waits until the simulator state becomes BOOTED. Args: timeout_sec: int, timeout of waiting simulator state for becoming BOOTED in seconds. Raises: ios_errors.SimError: when it is timeout to wait the simulator state becomes BOOTED. """ start_time = time.time() while start_time + timeout_sec >= time.time(): time.sleep(_SIM_CHECK_STATE_INTERVAL_SEC) if self.GetSimulatorState() == ios_constants.SimState.BOOTED: return raise ios_errors.SimError( 'Timeout to wait for simulator booted in %ss.' % timeout_sec)
def WaitUntilStateShutdown(self, timeout_sec=_SIMULATOR_SHUTDOWN_TIMEOUT_SEC): """Waits until the simulator state becomes SHUTDOWN. Args: timeout_sec: int, timeout of waiting simulator state for becoming SHUTDOWN in seconds. Raises: ios_errors.SimError: when it is timeout to wait the simulator state becomes SHUTDOWN. """ start_time = time.time() while start_time + timeout_sec >= time.time(): if self.GetSimulatorState() == ios_constants.SimState.SHUTDOWN: return time.sleep(_SIM_CHECK_STATE_INTERVAL_SEC) raise ios_errors.SimError('Timeout to wait for simulator shutdown in %ss.' % timeout_sec)
def GetLastSupportedSimOsVersion(os_type=ios_constants.OS.IOS, device_type=None): """Gets the last supported version of given arguments. If device_type is given, will return the last supported OS version of the device type. Otherwise, will return the last supported OS version of the OS type. Args: os_type: shared.ios_constants.OS, OS type of simulator, such as iOS, watchOS, tvOS. device_type: string, device type of the new simulator. The value corresponds to the output of `xcrun simctl list devicetypes`. E.g., iPhone 6, iPad Air, etc. Returns: a string, the last supported version. Raises: ios_errors.SimError: when there is no supported OS version of the given OS. ios_errors.IllegalArgumentError: when the supported OS version can not match the given simulator type. """ supported_os_versions = GetSupportedSimOsVersions(os_type) if not supported_os_versions: raise ios_errors.SimError('Can not find supported OS version of %s.' % os_type) if not device_type: return supported_os_versions[-1] 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) <= max_os_version: return os_version 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 Execute(self, return_output=True): """Executes the xcodebuild test command. Args: return_output: bool, whether save output in the execution result. Returns: a tuple of two fields: exit_code: A value of type runner_exit_codes.EXITCODE. output: the output of xcodebuild test command or None if return_output is False. """ run_env = dict(os.environ) run_env['NSUnbufferedIO'] = 'YES' max_attempts = 1 sim_log_path = None if self._sdk == ios_constants.SDK.IPHONESIMULATOR: max_attempts = _SIM_TEST_MAX_ATTEMPTS if self._device_id: sim_log_path = simulator_util.Simulator( self._device_id).simulator_system_log_path elif self._sdk == ios_constants.SDK.IPHONEOS: max_attempts = _DEVICE_TEST_MAX_ATTEMPTS test_started = False test_succeeded = False test_failed = False for i in range(max_attempts): process = subprocess.Popen(self._command, env=run_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) check_xcodebuild_stuck = CheckXcodebuildStuckThread( process, self._startup_timeout_sec) check_xcodebuild_stuck.start() output = io.BytesIO() for stdout_line in iter(process.stdout.readline, ''): if not test_started: # Terminates the CheckXcodebuildStuckThread when test has started # or XCTRunner.app has started. # But XCTRunner.app start does not mean test start. if ios_constants.TEST_STARTED_SIGNAL in stdout_line: test_started = True check_xcodebuild_stuck.Terminate() # Only terminate the check_xcodebuild_stuck thread when running on # iphonesimulator device. When running on iphoneos device, the # XCTRunner.app may not launch the test session sometimes # (error rate < 1%). if (self._test_type == ios_constants.TestType.XCUITEST and ios_constants.XCTRUNNER_STARTED_SIGNAL in stdout_line and self._sdk == ios_constants.SDK.IPHONESIMULATOR): check_xcodebuild_stuck.Terminate() else: if self._succeeded_signal and self._succeeded_signal in stdout_line: test_succeeded = True if self._failed_signal and self._failed_signal in stdout_line: test_failed = True sys.stdout.write(stdout_line) sys.stdout.flush() # If return_output is false, the output is only used for checking error # cause and deleting cached files (_DeleteTestCacheFileDirs method). if return_output or not test_started: output.write(stdout_line) try: if test_started: if test_succeeded: exit_code = runner_exit_codes.EXITCODE.SUCCEEDED elif test_failed: exit_code = runner_exit_codes.EXITCODE.FAILED else: exit_code = runner_exit_codes.EXITCODE.ERROR return exit_code, output.getvalue( ) if return_output else None check_xcodebuild_stuck.Terminate() if check_xcodebuild_stuck.is_xcodebuild_stuck: return self._GetResultForXcodebuildStuck( output, return_output) 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): logging.warning( 'Failed to launch test on the device. Will relaunch again.' ) continue if _TOO_MANY_INSTANCES_ALREADY_RUNNING in output_str: return (runner_exit_codes.EXITCODE.NEED_REBOOT_DEVICE, output_str if return_output else None) if self._sdk == ios_constants.SDK.IPHONESIMULATOR: if self._NeedRebootSim(output_str): return (runner_exit_codes.EXITCODE.NEED_REBOOT_DEVICE, output_str if return_output else None) if self._NeedRecreateSim(output_str): return (runner_exit_codes.EXITCODE.NEED_RECREATE_SIM, output_str if return_output else None) # The following error can be fixed by relaunching the test again. try: if sim_log_path and os.path.exists(sim_log_path): # Sleeps short time. Then the tail simulator log can get more log. time.sleep(0.5) tail_sim_log = _ReadFileTailInShell( sim_log_path, _TAIL_SIM_LOG_LINE) if (self._test_type == ios_constants.TestType.LOGIC_TEST and simulator_util. IsXctestFailedToLaunchOnSim(tail_sim_log) or self._test_type != ios_constants.TestType.LOGIC_TEST and simulator_util. IsAppFailedToLaunchOnSim(tail_sim_log) or simulator_util.IsCoreSimulatorCrash( tail_sim_log)): raise ios_errors.SimError('') if _PROCESS_EXISTED_OR_CRASHED_ERROR in output_str: raise ios_errors.SimError('') if ios_constants.CORESIMULATOR_INTERRUPTED_ERROR in output_str: # Sleep random[0,2] seconds to avoid race condition. It is a known # issue that CoreSimulatorService connection will be interrupted # if two simulators are booting at the same time. time.sleep(random.uniform(0, 2)) raise ios_errors.SimError('') if (self._app_bundle_id and not simulator_util.Simulator( self._device_id).IsAppInstalled( self._app_bundle_id)): raise ios_errors.SimError('') except ios_errors.SimError: if i < max_attempts - 1: logging.warning( 'Failed to launch test on simulator. Will relaunch again.' ) continue return (runner_exit_codes.EXITCODE.TEST_NOT_START, output_str if return_output else None) finally: _DeleteTestCacheFileDirs(output.getvalue(), self._sdk, self._test_type)
def simulator_id(self): if not self._simulator_id: raise ios_errors.SimError( 'The simulator has not been created or has been deleted.') return self._simulator_id
def CreateNewSimulator(device_type=None, os_version=None, name_prefix=None): """Creates a new simulator according to arguments. If neither device_type nor os_version is given, will use the latest iOS version and latest iPhone type. If os_version is given but device_type is not, will use latest iPhone type according to the OS version limitation. E.g., if the given os_version is 9.3, the latest simulator type is iPhone 6s Plus. Because the min OS version of iPhone 7 is 10.0. If device_type is given but os_version is not, will use the min value between max OS version of the simulator type and current latest OS version. E.g., if the given device_type is iPhone 5 and latest OS version is 10.3, will use 10.2. Because the max OS version of iPhone 5 is 10.2. Args: device_type: string, device type of the new simulator. The value corresponds to the output of `xcrun simctl list devicetypes`. E.g., iPhone 6, iPad Air, etc. os_version: string, OS version of the new simulator. The format is {major}.{minor}, such as 9.3, 10.2. name_prefix: string, name prefix of the new simulator. By default, it is "New". Returns: a tuple with four items: string, id of the new simulator. string, simulator device type of the new simulator. string, OS version of the new simulator. string, name of the new simulator. Raises: ios_errors.SimError: when failed to create new simulator. ios_errors.IllegalArgumentError: when the given argument is invalid. """ if not device_type: os_type = ios_constants.OS.IOS else: _ValidateSimulatorType(device_type) os_type = GetOsType(device_type) if not os_version: os_version = GetLastSupportedSimOsVersion(os_type, device_type=device_type) else: supported_sim_os_versions = GetSupportedSimOsVersions(os_type) if os_version not in supported_sim_os_versions: raise ios_errors.IllegalArgumentError( 'The simulator os version %s is not supported. Supported simulator ' 'os versions are %s.' % (os_version, supported_sim_os_versions)) if not device_type: device_type = GetLastSupportedIphoneSimType(os_version) else: _ValidateSimulatorTypeWithOsVersion(device_type, os_version) if not name_prefix: name_prefix = 'New' name = '%s-%s-%s' % (name_prefix, device_type, os_version) # Example # Runtime ID of iOS 10.2: com.apple.CoreSimulator.SimRuntime.iOS-10-2 runtime_id = _PREFIX_RUNTIME_ID + os_type + '-' + os_version.replace( '.', '-') logging.info('Creating a new simulator:\nName: %s\nOS: %s %s\nType: %s', name, os_type, os_version, device_type) for i in range(0, _SIM_OPERATION_MAX_ATTEMPTS): try: new_simulator_id = RunSimctlCommand( ['xcrun', 'simctl', 'create', name, device_type, runtime_id]) except ios_errors.SimError as e: raise ios_errors.SimError('Failed to create simulator: %s' % str(e)) new_simulator_obj = Simulator(new_simulator_id) # After creating a new simulator, its state is CREATING. When the # simulator's state becomes SHUTDOWN, the simulator is created. try: new_simulator_obj.WaitUntilStateShutdown( _SIMULATOR_CREATING_TO_SHUTDOWN_TIMEOUT_SEC) logging.info('Created new simulator %s.', new_simulator_id) return new_simulator_id, device_type, os_version, name except ios_errors.SimError as error: logging.debug('Failed to create simulator %s: %s.', new_simulator_id, error) logging.debug('Deleted half-created simulator %s.', new_simulator_id) new_simulator_obj.Delete() if i != _SIM_OPERATION_MAX_ATTEMPTS - 1: logging.debug('Will sleep %ss and retry again.', _SIM_ERROR_RETRY_INTERVAL_SEC) # If the simulator's state becomes SHUTDOWN, there may be something # wrong in CoreSimulatorService. Sleeps a short interval(2s) can help # reduce flakiness. time.sleep(_SIM_ERROR_RETRY_INTERVAL_SEC) raise ios_errors.SimError('Failed to create simulator in %d attempts.' % _SIM_OPERATION_MAX_ATTEMPTS)