def handle_params(self): ''' 加载并设置参数,用例中可重写加载方法方式; 并显示的调用以适配不支持参数传递的方式 正常模式:不设置参数 设置数据驱动参数:只设置数据驱动参数 设置全部参数:如果传入参数,以传入参数为准,否则设置数据驱动参数 ''' # 通过参数控制是否进行参数传递 param_mode = settings.get("QTAF_PARAM_MODE", False) if not param_mode: param_mode = settings.get("QC_PARAM_MODE", False) if not param_mode: # 正常模式 return self.add_params() # 添加参数定义 self.assert_types() # 参数校验 temp_data = self.params if param_mode == "DATADRIVE": if isinstance(self.casedata, dict): temp_data.update(self.casedata) elif param_mode == "PARAM" or param_mode is True: if isinstance(self.casedata, dict): temp_data.update(self.casedata) if not temp_data or not isinstance(temp_data, dict): return for key, value in temp_data.items(): if not isinstance(key, six.string_types): continue setattr(self, str(key), value)
def _rewrite_assert(self, cls): if not settings.get("QTAF_REWRITE_ASSERT", True) or cls == TestCase: return rewriter = AssertionRewriter() for key in dir(cls): item = getattr(cls, key) if isinstance(item, (types.MethodType, types.FunctionType)): rewriter.rewrite(item)
def _log_assert_failed(self, message, back_count=2): """记录断言失败的信息 """ stack = get_last_frame_stack(back_count) msg = "检查点不通过\n%s%s\n" % (smart_text(stack), smart_text(message)) self.__testresult.log_record(EnumLogLevel.ASSERT, msg) if not settings.get("QTAF_ASSERT_CONTINUE", True): raise RuntimeError("testcase assert failed:%s" % message)
def _thread_run(self): '''测试用例线程过程 ''' # 函数时发生了死锁,故注释掉。观察一段时间,看修改是否会影响测试。 try: try: self._check_testcase(self._testcase) except RuntimeError as e: self._testresult.error(e.args[0]) return while True: with self._lock: if len(self._subtasks) == 0 or self._stop_run: break it = self._subtasks.popleft() if isinstance(it, str): try: if it in ['init_test', 'initTest']: getattr(self._testcase, it)(self._testresult) else: task_result = getattr(self._testcase, it)() if task_result and isinstance( task_result, six.string_types): valid_task_result = False for name in TestResultType.__dict__: value = TestResultType.__dict__[name] if task_result == value: valid_task_result = True break if not valid_task_result: self._testresult.warning( '指定的自定义状态不支持,用例将继续执行') else: self._testresult.customize_result( task_result) while self._subtasks[0] not in [ 'post_test', 'postTest' ]: self._subtasks.popleft() except: self._testresult.exception('%s执行失败' % it) if settings.get( "QTAF_FAILED_SKIP_RUNTEST", False) and it in ['pre_test', 'preTest']: while self._subtasks[0] not in [ 'post_test', 'postTest' ]: self._subtasks.popleft() else: it() except: self._error = traceback.format_exc()
def _get_fbsimctl_agent_cmd_for_simulator(self, xcode_version): agent_dir = self._build_agent_for_simulator(xcode_version) xctest_path = os.path.join( agent_dir, 'XCTestAgent-Runner.app/PlugIns/XCTestAgent.xctest') attached_app = settings.get('QT4I_SIM_ATTACHED_APP', 'com.apple.reminders') return ' '.join([ dt.DT().fbsimctl, self.udid, 'launch_xctest', xctest_path, attached_app, '--port %s -- listen' % self._server_port ])
def __record_assert_failed(self, message, actual, expect): '''记录Assert失败信息 :param message: 提示信息 :type message: string :param actual: 实际值 :type actual: string :param expect: 期望值 :type expect: string ''' # 得到上一个函数调用帧所在的文件路径,行号,函数名 stack = get_last_frame_stack(3) msg = "检查点不通过\n%s%s\n期望值:%s%s\n实际值:%s%s" % ( smart_text(stack), smart_text(message), expect.__class__, expect, actual.__class__, actual) self.__testresult.log_record(EnumLogLevel.ASSERT, msg) if not settings.get("QTAF_ASSERT_CONTINUE", True): raise RuntimeError("testcase assert failed:%s" % message)
def __init__(self, addr, port, udid=None, agent_port=DEFAULT_AGENT_PORT, driver_type=settings.get('IOS_DRIVER', 'xctest'), endpoint_clss=''): '''初始化 :param addr: 设备主机ip :type addr: str :param port: 设备主机端口号 :type port: int :param udid: 设备UDID :type udid: str or None :param agent_port: agent端口号 :type agent_port: int :param driver_type: driver类型,xctest or instruments :type driver_type: str :param endpoint_clss: 注册远程调用类集,多个以";"间隔 :type endpoint_clss: str ''' unzip_path = pkg_resources.resource_filename( "qt4i", "driver") # @UndefinedVariable self._script = os.path.join(unzip_path, 'driverserver.py') self._addr = addr self._port = port self._udid = udid self._agent_port = agent_port self._driver_type = driver_type self._endpoint_clss = endpoint_clss self._python = "python3" if PY3 else "python" if self._udid: self.pidfile = '/tmp/driverserver_%s.pid' % self._udid else: self.pidfile = '/tmp/driverserver.pid' if hasattr(settings, 'QT4i_ENDPOINT_CLSS'): # 兼容小写配置项 self._endpoint_clss = ','.join( [self._endpoint_clss, settings.QT4i_ENDPOINT_CLSS]) elif hasattr(settings, 'QT4I_ENDPOINT_CLSS'): self._endpoint_clss = ','.join( [self._endpoint_clss, settings.QT4I_ENDPOINT_CLSS])
class XCUITestAgent(object): '''XCUITest Agent ''' _process = None # Agent进程 _relay_thread = None # 端口映射线程 _agent_cmd = '' CommandTimeout = settings.get('QT4I_START_APP_TIMEOUT', 40) PythonPath = Task("which python").execute() LOG_FILTERED_METHODS = [ Command.SCREENSHOT, Command.QTA_STOP_AGENT, Command.GET_PAGE_SOURCE, Command.GET_ELEMENT_TREE, Command.QTA_ELEMENT_TREE, ] def __init__(self, device_id, server_ip=DEFAULT_IP, server_port=DEFAULT_PORT, keep_alive=False, retry=3, timeout=60): self.log_name = "xctest_%s" % device_id self.log = logger.get_logger(self.log_name) self.version = '0.0.0' version_file = os.path.join(os.path.expanduser('~'), 'XCTestAgent', 'version.txt') if os.path.exists(version_file): with open(version_file, 'r') as f: self.version = f.read() # XCTestAgent工程默认路径:~/XCTestAent/XCTestAgent.xcodeproj self.XCTestAgentPath = os.path.join(os.path.expanduser('~'), "XCTestAgent", "XCTestAgent.xcodeproj") if not os.path.exists(self.XCTestAgentPath): msg = 'XCTestAgent does not exist. Please use command line: "python manage.py qt4i.setup" to setup XCTestAgent.xcodeproj' self.log.error(msg) raise Exception(msg) self.udid = device_id self._server_ip = server_ip self._server_port = server_port self._server_url = 'http://%s:%d' % (server_ip, server_port) self.type = EnumDevice.Simulator if dt.DT().is_simulator( device_id) else EnumDevice.Device self._is_remote = True self.session_id = None self.crash_flag = False self.capabilities = {} self.mjpeg_port = 9100 + self._server_port - 8100 self.stub_remote_port = 18123 self.stub_port = 18123 + self._server_port - 8100 self.stub_server_url = 'http://%s:%d' % (DEFAULT_IP, self.stub_port) self.stub_client = RemoteConnection(self.stub_server_url, keep_alive=False, logger_name=self.log_name) self.stub_client.set_timeout(0.01) self.error_handler = ErrorHandler() self._command_executor = RemoteConnection(self._server_url, keep_alive=keep_alive, logger_name=self.log_name) self._is_relay_quit = threading.Event() self.start(retry, timeout) def _tcp_relay(self): '''将设备8100端口映射到本地8100端口,虚拟机不需要映射 ''' if self.type == EnumDevice.Simulator: return if not self._relay_thread and self._server_ip == DEFAULT_IP: self._relay_error = None self._relay_thread = threading.Thread(target=self._forward_ports) self._relay_thread.daemon = True self.log.info("Start TCPRelay Thread") self._relay_thread.start() time.sleep(1) if self._relay_error: raise Exception(self._relay_error) if not self._relay_thread.is_alive(): raise RuntimeError('_relay_thread failed to start') def _forward_ports(self): '''iOS真机设备的端口转发 ''' try: agent_port_pair = '%d:%d' % (8100, self._server_port) stub_port_pair = '%d:%d' % (self.stub_remote_port, self.stub_port) mjpeg_port_pair = '%d:%d' % (9100, self.mjpeg_port) pair_ports = [ agent_port_pair, stub_port_pair, mjpeg_port_pair ] # 端口对的数组,每对端口中前一个代表远程端口,后一个代表本地端口,例如:["8100:8100", "8200:8200"] self._tcp_servers = [] serverclass = ThreadedTCPServer for pair_port in pair_ports: rport, lport = pair_port.split(":") rport = int(rport) lport = int(lport) self.log.info("Forwarding local port %d to remote port %d" % (lport, rport)) server = serverclass(("localhost", lport), TCPRelay) server.rport = rport server.bufsize = 128 server.udid = self.udid self._tcp_servers.append(server) self._quit_relay_thread = False # 退出端口转发线程的标识位 self._is_relay_quit.clear() while not self._quit_relay_thread: try: rl, wl, xl = select.select(self._tcp_servers, [], []) #@UnusedVariable for server in rl: server.handle_request() except: self.log.exception('Forward ports') self._quit_relay_thread = True self.log.info("Forward ports: successfully quit select") for server in self._tcp_servers: server.server_close() self._is_relay_quit.set() self.log.info("Forward ports: successfully close socket") except Exception: self._relay_error = traceback.format_exc() self.log.exception('forward ports') def _build_agent_for_simulator(self, xcode_version): agent_dir = os.path.join(AGENT_STORE_PATH, xcode_version) agent_file = os.path.join(agent_dir, 'XCTestAgent-Runner.app') if not os.path.exists(agent_file): if not os.path.exists(agent_dir): os.makedirs(agent_dir) xctest_root = pkg_resources.resource_filename( "qt4i", "driver/xctest/xctestproj") #@UndefinedVariable xctestproj = os.path.join(xctest_root, 'XCTestAgent', 'XCTestAgent.xcodeproj') build_for_test_cmd = ' '.join([ 'xcodebuild', 'build-for-testing', '-project', '"%s"' % xctestproj, '-scheme', 'XCTestAgent', '-destination', '"platform=%s,id=%s"' % (self.type, self.udid), 'CONFIGURATION_BUILD_DIR=%s' % agent_dir ]) subprocess.call(build_for_test_cmd, shell=True) return agent_dir def _get_xcodebuild_agent_cmd_for_simulator(self, xcode_version): template_root = pkg_resources.resource_filename( "qt4i", "driver/xctest/bin") #@UndefinedVariable xctestfile_template = os.path.join( template_root, "iphonesimulator-%s.xctestrun" % xcode_version) agent_dir = self._build_agent_for_simulator(xcode_version) xctestfile = os.path.join(agent_dir, "%s.xctestrun" % self.udid) if not os.path.isfile(xctestfile): shutil.copyfile(xctestfile_template, xctestfile) cmd = "/usr/libexec/PlistBuddy -c 'Set :XCTestAgent:EnvironmentVariables:USE_PORT %s' %s" % ( self._server_port, xctestfile) subprocess.call(cmd, shell=True) derived_data_path = '/tmp/xctlog/%s' % self.udid if not os.path.exists(derived_data_path): os.makedirs(derived_data_path) return ' '.join([ 'xcodebuild', 'test-without-building', '-xctestrun', xctestfile, '-destination', '"platform=%s,id=%s"' % (self.type, self.udid), '-derivedDataPath', derived_data_path ]) def _get_fbsimctl_agent_cmd_for_simulator(self, xcode_version): agent_dir = self._build_agent_for_simulator(xcode_version) xctest_path = os.path.join( agent_dir, 'XCTestAgent-Runner.app/PlugIns/XCTestAgent.xctest') attached_app = settings.get('QT4I_SIM_ATTACHED_APP', 'com.apple.reminders') return ' '.join([ dt.DT().fbsimctl, self.udid, 'launch_xctest', xctest_path, attached_app, '--port %s -- listen' % self._server_port ]) def _stdout_line_callback(self, line): '''标准输出回调函数 ''' self.log.debug(line.strip(' ')) def _stderr_line_callback(self, line): '''标准错误回调函数 ''' self.log.error(line.strip(' ')) def start(self, retry=1, timeout=CommandTimeout): '''启动Agent,类初始化即调用 :param retry: 启动尝试次数 :type retry: int :param timeout: 单次启动超时 (秒) :type timeout: int ''' if self.type == EnumDevice.Simulator: dt.DT().reboot(self.udid) xcode_version = dt.DT.get_xcode_version().split(".")[0] if int(xcode_version) >= 9: self._agent_cmd = self._get_xcodebuild_agent_cmd_for_simulator( xcode_version) else: self._agent_cmd = self._get_fbsimctl_agent_cmd_for_simulator( xcode_version) else: self._agent_cmd = ' '.join([ 'xcodebuild', '-project %s' % self.XCTestAgentPath, '-scheme %s' % 'XCTestAgent', '-destination "platform=%s,id=%s"' % (self.type, self.udid), 'test' ]) # 清理遗留的xcodebuild进程 print self._agent_cmd Process().kill_process_by_name(self._agent_cmd.replace('"', '')) Process().kill_process_by_port(self._server_port) start_time = time.time() # 启动端口转发线程 self._tcp_relay() for _ in range(retry): self.log.info("Start XCTestAgent: %s" % self._agent_cmd) self.log.info("XCTestAgent Version: %s" % self.version) self._process = ThreadTask( command=self._agent_cmd, stdout_line_callback=self._stdout_line_callback, stderr_line_callback=self._stderr_line_callback) time0 = time.time() while time.time() - time0 < timeout: if self.is_working(): exec_time = (time.time() - start_time) * 1000 self.log.info('[ %s ] consumed [ %dms ]' % ('Start XCTestAgent', exec_time)) return time.sleep(3) _dt = dt.DT() self.log.info("Uninstall com.apple.test.XCTestAgent-Runner") result = _dt.uninstall("com.apple.test.XCTestAgent-Runner", self.udid) if not result: self.log.error(_dt.uninstall_error) self.stop() error_log_name = 'xctest_%s' % self.udid agent_error_log = logger.get_agent_error_log(error_log_name, start_time) raise AgentStartError("Failed to start XCTestAgent.\nDetails:%s" % agent_error_log) def stop(self, is_timeout=False): '''关闭Agent ''' if not is_timeout: # 停止手机上Agent进程 try: self._command_executor.set_timeout(2) self.execute(Command.QTA_STOP_AGENT) except: pass # 停止PC上端口转发线程 if self._relay_thread: self._quit_relay_thread = True try: self._execute(Command.HEALTH) # 发送请求,退出select阻塞 except: pass self._is_relay_quit.wait() # 等待端口转发线程退出 self._relay_thread = None self._command_executor.reset_timeout() # 等待PC上xcodebuild进程停止 time.sleep(10) self._process = None self.session_id = None self.log.info("Stop XCTestAgent successfully") def is_working(self): '''检查当前Agent是否在工作,如果Agent无法退到后台,重启手机 :returns: boolean ''' try: response = self._execute(Command.HEALTH) return response['value'] == 'XCTestAgent is ready' except XCTestAgentDeadException: self.log.exception('XCTestAgentDead') # Agent无法退到后台,重启手机 try: self.log.info('Reboot device %s' % self.udid) dt.DT().reboot(self.udid) for _ in xrange(20): device = dt.DT().get_device_by_udid(self.udid) if device is not None: break else: raise RuntimeError("after reboot, device disconnected!") except Exception: self.log.exception('device reboot error') except XCTestAgentTimeoutException: if self._relay_error: self.log.error("TcpRelay error:%s" % self._relay_error) self.stop(True) return False def has_session(self): '''检查是否存在会话 :returns: boolean ''' return self.session_id is not None def start_session(self, desired_capabilities, timeout=CommandTimeout): '''创建新会话 :param desired_capabilities: - bundleId - APP的bundle_id,例如:com.tencent.qq.dailybuild.test - app - APP路径,用于运行前安装APP. :type desired_capabilities: dict :param timeout: 检测启动app超时时间,启动超时则重启agent,单位:秒 :type timeout: int :returns: 返回结果,dict ''' while not self.is_working(): self.start() while 1: time0 = time.time() response = self._execute( Command.SESSION, { 'desiredCapabilities': desired_capabilities, }) exec_time = int((time.time() - time0) * 1000) # 单位:毫秒 if exec_time < (timeout * 1000): break self.log.error('Restart XCTestAgent: [ %s ] consumed [ %dms ]' % ('start app', exec_time)) self.stop() self.start() self.session_id = response['sessionId'] self.capabilities = response['value']['capabilities'] return response def _execute(self, command, params={}): '''执行指令 ''' response = self._command_executor.execute(command, params) if response: if not command in self.LOG_FILTERED_METHODS: self.log.debug(response) # 检查返回结果 try: self.error_handler.check_response(response) except ApplicationCrashedException: self.crash_flag = True raise return response # 如果服务器没有发送响应,认为指令执行成功 return {'success': 0, 'value': None, 'sessionId': self.session_id} def capture_screen(self): try: return self.stub_client.execute(Command.SCREENSHOT, {}) except XCTestAgentTimeoutException: return None def execute(self, command, params={}, timeout=CommandTimeout): '''执行指令 :param command: 指令名称 :type command: str :param params: 指令参数集 :type params: dict 例如 {bundleId: com.tencent.qq.dailybuild.test} :param timeout: 指令执行超时等待时常 :type timeout: int :returns: 返回结果,dict ''' time0 = time.time() if not self.has_session(): response = self._execute(Command.STATUS, {}) self.session_id = response['sessionId'] if self.has_session(): if not params: params = {'sessionId': self.session_id} elif 'sessionId' not in params: params['sessionId'] = self.session_id if command == Command.START: if params is None: raise WebDriverException("Desired Capabilities can't be None") if not isinstance(params, dict): raise WebDriverException( "Desired Capabilities must be a dictionary") response = self.start_session(params, timeout) elif command == Command.QUIT: if params.has_key('sessionId'): response = self._execute(command, params) else: response = {} self.session_id = None elif command == Command.STATUS: response = self._execute(command, params) if self.session_id is None: self.session_id = response['sessionId'] else: response = self._execute(command, params) exec_time = int((time.time() - time0) * 1000) self.log.info('[ %s ] consumed [ %dms ]' % (command, exec_time)) return response def get_crash_flag(self): '''获取app是否crash的标识 :returns : boolean -- True if app crashed, False if not ''' return self.crash_flag def get_driver_log(self): '''获取driver日志路径 ''' driver_log_path = None for h in self.log.handlers: attr_name = 'baseFilename' if hasattr(h, attr_name): driver_log_path = getattr(h, attr_name) break return driver_log_path
def test_get(self): '''get settings ''' self.assertEqual(settings.DEBUG, False) self.assertEqual(settings.get('DEBUG'), False) self.assertEqual(settings.get('NOT_EXIST', False), False)
from qt4i.driver.tools import logger from qt4i.driver.xctest.webdriverclient.exceptions import NoSuchElementException from qt4i.driver.xctest.webdriverclient.exceptions import XCTestAgentTimeoutException from qt4i.driver.util.uimap import UIA_XCT_MAPS from qt4i.driver.xctest.webdriverclient.errorhandler import ErrorCode from qt4i.driver.util.modalmap import DeviceProperty from testbase.conf import settings from tuia.qpathparser import QPathParser from pymobiledevice import lockdown DEFAULT_IP = '127.0.0.1' DEFAULT_PORT = 8100 TMP_LOAD_FILE = '/tmp/tmpFile' TMP_DIR_PATH = settings.get('QT4I_TMP_DIR_PATH', '/tmp') def convert_to_qpath(element): '''转换成qpath ''' if 'type' in element: element['classname'] = element['type'] element.pop('type') if 'isEnabled' in element: element['enabled'] = (element['isEnabled'] == '1') element.pop('isEnabled') if 'isVisible' in element: element['visible'] = (element['isVisible'] == '1') element.pop('isVisible') if 'rect' in element: element['rect']= {
def __init__(self, attrs={}, devicemanager=None): '''Device构造函数 :param attrs: 设备UDID字符串|设备属性字典 keys: udid - 设备UDID host - 设备主机ip is_simulator - 是否为模拟器 :type attrs: str|dict :param devicemanager: 设备管理类 :type devicemanager: DeviceManager ''' cond = {} if isinstance(attrs, six.string_types): cond['udid'] = attrs elif isinstance(attrs, dict): cond = attrs.copy() else: raise Exception('Device attributes type error: %s' % type(attrs)) ct = None if devicemanager: self._device_resource = devicemanager.acquire_device(cond) else: ct = context.current_testcase() if ct is None: self._test_resources = TestResourceManager( LocalResourceManagerBackend()).create_session() else: self._test_resources = ct.test_resources self._device_resource = self._test_resources.acquire_resource( "ios", condition=cond) if self._device_resource is None: raise Exception('无可用的真机和模拟器: %s' % str(cond)) props = self._device_resource if 'properties' in self._device_resource: # online mode devprops = self._device_resource['properties'] props = { p['name'].encode(Encoding): p['value'].encode(Encoding) for p in devprops } props['id'] = self._device_resource['id'] if 'csst_uri' not in props or props['csst_uri'] == 'None': props['csst_uri'] = None self._device_resource = DeviceResource(props['host'], int( props['port']), props['udid'], props['is_simulator'], props['name'], props['version'], props['csst_uri'], props['id']) self._base_url = self._device_resource.driver_url self._ws_uri = self._device_resource.ws_uri self._host = RPCClientProxy('/'.join([self._base_url, 'host/']), self._ws_uri, allow_none=True, encoding=Encoding) self._device_udid = self._device_resource.udid self._device_name = self._device_resource.name self._device_ios = self._device_resource.version if isinstance(self._device_resource.is_simulator, bool): self._device_simulator = self._device_resource.is_simulator else: self._device_simulator = self._device_resource.is_simulator == str( True) if self._device_simulator: self._host.start_simulator(self._device_udid) self._app_started = False Device.Devices.append(self) url = '/'.join([self._base_url, 'device', '%s/' % self._device_udid]) self._driver = RPCClientProxy(url, self._ws_uri, allow_none=True, encoding=Encoding) self._keyboard = Keyboard(self) logger.info('[%s] Device - Connect - %s - %s (%s)' % (datetime.datetime.fromtimestamp( time.time()), self.name, self.udid, self.ios_version)) # 申请设备成功后,对弹窗进行处理 rule = settings.get('QT4I_ALERT_DISMISS', DEFAULT_ALERT_RULE) if rule: try: self._dismiss_alert(rule) except: logger.exception('dismiss alert %s' % rule)
from testbase import logger from qt4i.util import EnumDirect, Rectangle from qt4i.driver.rpc import RPCClientProxy from qt4i.driver.util import Process QT4i_LOGS_PATH = os.path.abspath('_attachments') if not os.path.exists(QT4i_LOGS_PATH): os.makedirs(QT4i_LOGS_PATH) Encoding = 'UTF-8' DEFAULT_ADDR = '127.0.0.1' DEFAULT_PORT = 12306 DEFAULT_AGENT_PORT = 8100 DEFAULT_ALERT_RULE = settings.get( 'QT4I_ALERT_RULES', [{ 'button_text': '^确定$|^好$|^允许$|^OK$|^Allow$' }]) class DeviceServer(object): '''设备服务器 ''' def __init__(self, addr, port, udid=None, agent_port=DEFAULT_AGENT_PORT, driver_type=settings.get('IOS_DRIVER', 'xctest'), endpoint_clss=''): '''初始化
import time import traceback import uuid import xmlrpclib import hashlib from testbase.conf import settings from testbase.util import LazyInit from qt4i.exceptions import ControlAmbiguousError from qt4i.exceptions import ControlNotFoundError from qt4i.app import App from qt4i.device import QT4i_LOGS_PATH from qt4i.qpath import QPath from qt4i.util import Rectangle, EnumDirect, Timeout INS_IOS_DRIVER = True if settings.get('IOS_DRIVER', 'xctest') == 'instruments' else False QTA_AI_SWITCH = settings.get('QT4I_AI_SWITCH', False) class ControlContainer(object): '''控件集合接口 ''' def __init__(self): self._locators = {} # 对象定义 def __getitem__(self, key): '''操作符"[]"重载 :param key: 控件名 :type key: str :rtype: object
# OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # '''logging日志模块的封装 ''' import os import logging import datetime import time import re from logging.handlers import BaseRotatingHandler from testbase.conf import settings TMP_DIR_PATH = settings.get('QT4I_TMP_DIR_PATH', '/tmp') LOG_MAX_BTYES = 1024 * 1024 * 10 #单份日志最大容量(byte) LOG_BACKUP_COUNT = settings.get('QT4I_LOG_BACKUP_COUNT', 10) #setting获取count时默认值 LOG_DELAY_DAY = 2 #日志保留天数0.0015625 TIME_FORMAT = '%Y-%m-%d %H:%M:%S' USER_KEY_WORDS = settings.get('QT4I_AGENT_ERROR_KEY_WORDS', []) DEFAULT_KEY_WORDS = [ 'requires a development team', 'invalid code signature', 'expired', 'Connection peer refused channel request' ] class RotatingFileHandler(BaseRotatingHandler): '''文件滚动日志 '''
class RemoteConnection(object): """A connection with the Remote WebDriver server. Communicates with the server using the WebDriver wire protocol: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol""" _default_timeout = settings.get('QT4I_XCTAGENT_CMD_TIMEOUT', 90) if _default_timeout == -1: _default_timeout = socket._GLOBAL_DEFAULT_TIMEOUT _timeout = _default_timeout # _timeout = socket._GLOBAL_DEFAULT_TIMEOUT @classmethod def get_timeout(cls): """ :returns: Timeout value in seconds for all http requests made to the Remote Connection """ return None if cls._timeout == socket._GLOBAL_DEFAULT_TIMEOUT else cls._timeout @classmethod def set_global_timeout(cls, timeout): """Override the default timeout :param timeout: timeout value for http requests in seconds :type timeout: int """ cls._timeout = timeout @classmethod def reset_global_timeout(cls): """Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT """ # cls._timeout = socket._GLOBAL_DEFAULT_TIMEOUT cls._timeout = cls._default_timeout def set_timeout(self, timeout): self._timeout = timeout def reset_timeout(self): self._timeout = self._default_timeout def __init__(self, remote_server_addr, keep_alive=False, resolve_ip=True, logger_name='RemoteConnection'): # Attempt to resolve the hostname and get an IP address. self.logger = logging.get_logger(logger_name) self.keep_alive = keep_alive parsed_url = parse.urlparse(remote_server_addr) addr = "" if parsed_url.hostname and resolve_ip: try: netloc = socket.gethostbyname(parsed_url.hostname) addr = netloc if parsed_url.port: netloc += ':%d' % parsed_url.port if parsed_url.username: auth = parsed_url.username if parsed_url.password: auth += ':%s' % parsed_url.password netloc = '%s@%s' % (auth, netloc) remote_server_addr = parse.urlunparse( (parsed_url.scheme, netloc, parsed_url.path, parsed_url.params, parsed_url.query, parsed_url.fragment)) except socket.gaierror: self.logger.info('Could not get IP address for host: %s' % parsed_url.hostname) self._url = remote_server_addr if keep_alive: self._conn = httplib.HTTPConnection( str(addr), str(parsed_url.port), timeout=self._timeout) self._commands = { Command.HEALTH: ('GET', '/health'), Command.DEACTIVATE_APP: ('POST', '/session/$sessionId/wda/deactivateApp'), Command.STATUS: ('GET', '/status'), Command.SESSION: ('POST', '/session'), Command.GET_ALL_SESSIONS: ('GET', '/sessions'), Command.QUIT: ('DELETE', '/session/$sessionId'), Command.GET: ('POST', '/session/$sessionId/url'), Command.EXECUTE_SCRIPT: ('POST', '/session/$sessionId/execute'), Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'), Command.GET_TITLE: ('GET', '/session/$sessionId/title'), Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'), Command.SCREENSHOT: ('GET', '/screenshot'), Command.ELEMENT_SCREENSHOT: ('GET', '/session/$sessionId/screenshot/$id'), Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'), Command.FIND_ELEMENTS: ('POST', '/session/$sessionId/elements'), Command.GET_ACTIVE_ELEMENT: ('POST', '/session/$sessionId/element/active'), Command.FIND_CHILD_ELEMENT: ('POST', '/session/$sessionId/element/$id/element'), Command.FIND_CHILD_ELEMENTS: ('POST', '/session/$sessionId/element/$id/elements'), Command.CLICK_ELEMENT: ('POST', '/session/$sessionId/element/$id/click'), Command.CLEAR_ELEMENT: ('POST', '/session/$sessionId/element/$id/clear'), Command.SUBMIT_ELEMENT: ('POST', '/session/$sessionId/element/$id/submit'), Command.GET_ELEMENT_TREE: ('GET', '/session/$sessionId/tree'), Command.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'), Command.SEND_KEYS_TO_ELEMENT: ('POST', '/session/$sessionId/element/$id/value'), Command.SEND_KEYS_TO_ACTIVE_ELEMENT: ('POST', '/session/$sessionId/keys'), Command.GET_ELEMENT_VALUE: ('GET', '/session/$sessionId/element/$id/value'), Command.GET_ELEMENT_TAG_NAME: ('GET', '/session/$sessionId/element/$id/name'), Command.IS_ELEMENT_SELECTED: ('GET', '/session/$sessionId/element/$id/selected'), Command.SET_ELEMENT_SELECTED: ('POST', '/session/$sessionId/element/$id/selected'), Command.IS_ELEMENT_ENABLED: ('GET', '/session/$sessionId/element/$id/enabled'), Command.IS_ELEMENT_DISPLAYED: ('GET', '/session/$sessionId/element/$id/displayed'), Command.GET_ELEMENT_LOCATION: ('GET', '/session/$sessionId/element/$id/location'), Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW: ('GET', '/session/$sessionId/element/$id/location_in_view'), Command.GET_ELEMENT_SIZE: ('GET', '/session/$sessionId/element/$id/size'), Command.GET_ELEMENT_RECT: ('GET', '/session/$sessionId/element/$id/rect'), Command.GET_ELEMENT_ATTRIBUTE: ('GET', '/session/$sessionId/element/$id/attribute/$name'), Command.ELEMENT_EQUALS: ('GET', '/session/$sessionId/element/$id/equals/$other'), Command.SWITCH_TO_FRAME: ('POST', '/session/$sessionId/frame'), Command.SWITCH_TO_PARENT_FRAME: ('POST', '/session/$sessionId/frame/parent'), Command.SWITCH_TO_WINDOW: ('POST', '/session/$sessionId/window'), Command.CLOSE: ('DELETE', '/session/$sessionId/window'), Command.IMPLICIT_WAIT: ('POST', '/session/$sessionId/timeouts/implicit_wait'), Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'), Command.SET_SCRIPT_TIMEOUT: ('POST', '/session/$sessionId/timeouts/async_script'), Command.SET_TIMEOUTS: ('POST', '/session/$sessionId/timeouts'), Command.DISMISS_ALERT: ('POST', '/session/$sessionId/dismiss_alert'), Command.ACCEPT_ALERT: ('POST', '/session/$sessionId/accept_alert'), Command.SET_ALERT_VALUE: ('POST', '/session/$sessionId/alert_text'), Command.GET_ALERT_TEXT: ('GET', '/session/$sessionId/alert_text'), Command.SET_ALERT_CREDENTIALS: ('POST', '/session/$sessionId/alert/credentials'), Command.CLICK: ('POST', '/session/$sessionId/click'), Command.DOUBLE_CLICK: ('POST', '/session/$sessionId/doubleclick'), Command.MOUSE_DOWN: ('POST', '/session/$sessionId/buttondown'), Command.MOUSE_UP: ('POST', '/session/$sessionId/buttonup'), Command.MOVE_TO: ('POST', '/session/$sessionId/moveto'), Command.GET_WINDOW_SIZE: ('GET', '/session/$sessionId/window/size'), Command.SET_WINDOW_SIZE: ('POST', '/session/$sessionId/window/$windowHandle/size'), Command.GET_WINDOW_POSITION: ('GET', '/session/$sessionId/window/$windowHandle/position'), Command.SET_WINDOW_POSITION: ('POST', '/session/$sessionId/window/$windowHandle/position'), Command.MAXIMIZE_WINDOW: ('POST', '/session/$sessionId/window/$windowHandle/maximize'), Command.SET_SCREEN_ORIENTATION: ('POST', '/session/$sessionId/orientation'), Command.GET_SCREEN_ORIENTATION: ('GET', '/session/$sessionId/orientation'), Command.SINGLE_TAP: ('POST', '/session/$sessionId/touch/click'), Command.TOUCH_DOWN: ('POST', '/session/$sessionId/touch/down'), Command.TOUCH_UP: ('POST', '/session/$sessionId/touch/up'), Command.TOUCH_MOVE: ('POST', '/session/$sessionId/touch/move'), Command.TOUCH_SCROLL: ('POST', '/session/$sessionId/touch/scroll'), Command.DOUBLE_TAP: ('POST', '/session/$sessionId/touch/doubleclick'), Command.LONG_PRESS: ('POST', '/session/$sessionId/touch/longclick'), Command.FLICK: ('POST', '/session/$sessionId/touch/flick'), Command.EXECUTE_SQL: ('POST', '/session/$sessionId/execute_sql'), Command.GET_LOCATION: ('GET', '/session/$sessionId/location'), Command.SET_LOCATION: ('POST', '/session/$sessionId/location'), Command.GET_APP_CACHE: ('GET', '/session/$sessionId/application_cache'), Command.GET_APP_CACHE_STATUS: ('GET', '/session/$sessionId/application_cache/status'), Command.CLEAR_APP_CACHE: ('DELETE', '/session/$sessionId/application_cache/clear'), Command.GET_NETWORK_CONNECTION: ('GET', '/session/$sessionId/network_connection'), Command.SET_NETWORK_CONNECTION: ('POST', '/session/$sessionId/network_connection'), Command.GET_LOCAL_STORAGE_ITEM: ('GET', '/session/$sessionId/local_storage/key/$key'), Command.REMOVE_LOCAL_STORAGE_ITEM: ('DELETE', '/session/$sessionId/local_storage/key/$key'), Command.GET_LOCAL_STORAGE_KEYS: ('GET', '/session/$sessionId/local_storage'), Command.SET_LOCAL_STORAGE_ITEM: ('POST', '/session/$sessionId/local_storage'), Command.CLEAR_LOCAL_STORAGE: ('DELETE', '/session/$sessionId/local_storage'), Command.GET_LOCAL_STORAGE_SIZE: ('GET', '/session/$sessionId/local_storage/size'), Command.GET_SESSION_STORAGE_ITEM: ('GET', '/session/$sessionId/session_storage/key/$key'), Command.REMOVE_SESSION_STORAGE_ITEM: ('DELETE', '/session/$sessionId/session_storage/key/$key'), Command.GET_SESSION_STORAGE_KEYS: ('GET', '/session/$sessionId/session_storage'), Command.SET_SESSION_STORAGE_ITEM: ('POST', '/session/$sessionId/session_storage'), Command.CLEAR_SESSION_STORAGE: ('DELETE', '/session/$sessionId/session_storage'), Command.GET_SESSION_STORAGE_SIZE: ('GET', '/session/$sessionId/session_storage/size'), Command.CURRENT_CONTEXT_HANDLE: ('GET', '/session/$sessionId/context'), Command.CONTEXT_HANDLES: ('GET', '/session/$sessionId/contexts'), Command.SWITCH_TO_CONTEXT: ('POST', '/session/$sessionId/context'), Command.QTA_FIND_ELEMENTS: ('POST', '/session/$sessionId/qta/element/$id/elements'), Command.QTA_DEVICE_CLICK: ('POST', '/session/$sessionId/qta/click'), Command.QTA_ELEMENT_CLICK: ('POST', '/session/$sessionId/qta/element/$id/click'), Command.QTA_DEVICE_DOUBLE_CLICK: ('POST', '/session/$sessionId/qta/doubleclick'), Command.QTA_ELEMENT_DOUBLE_CLICK: ('POST', '/session/$sessionId/wda/element/$id/doubleTap'), Command.QTA_DEVICE_LONG_CLICK: ('POST', '/session/$sessionId/qta/longclick'), Command.QTA_ELEMENT_LONG_CLICK: ('POST', '/session/$sessionId/wda/element/$id/touchAndHold'), Command.QTA_DEVICE_SENDKEYS: ('POST', '/session/$sessionId/qta/sendkeys'), Command.QTA_ELEMENT_SENDKEYS: ('POST', '/session/$sessionId/qta/element/$id/sendkeys'), Command.QTA_DEVICE_DRAG: ('POST', '/session/$sessionId/qta/drag'), Command.QTA_ELEMENT_DRAG: ('POST', '/session/$sessionId/qta/element/$id/drag'), Command.QTA_GET_PARENT_ELEMENT: ('POST', '/session/$sessionId/qta/element/$id/parent'), Command.QTA_GET_CHILDREN_ELEMENTS: ('POST', '/session/$sessionId/qta/element/$id/children'), Command.QTA_SCROLL_TO_VISIBLE: ('POST', '/session/$sessionId/qta/element/$id/scroll'), Command.QTA_DRAG_TO_VALUE: ('POST', '/session/$sessionId/qta/element/$id/slider'), Command.QTA_GET_ELEMENT_ATTRS: ('POST', '/session/$sessionId/qta/element/$id/attrs'), Command.QTA_ALERT_RULES_UPDATE: ('POST', '/qta/alertrules/update'), Command.QTA_ELEMENT_SETVALUE: ('POST', '/session/$sessionId/qta/element/$id/value'), Command.QTA_ELEMENT_TREE: ('POST', '/session/$sessionId/qta/element/$id/tree'), Command.QTA_STOP_AGENT: ('POST', '/qta/stop'), Command.QTA_GET_FOREGROUND_APP_NAME: ('GET', '/qta/appName'), Command.QTA_GET_FOREGROUND_APP_PID: ('GET', '/qta/appPid'), Command.QTA_GET_ELEMENT_WIN_INFO: ('GET', '/session/$sessionId/qta/element/$id/wininfo'), Command.QTA_ALERT_DISMISS: ('POST', '/qta/alert/dismiss'), Command.QTA_DEVICE_LOCK: ('POST', '/qta/device/lock'), Command.QTA_DEVICE_UNLOCK: ('POST', '/qta/device/unlock'), Command.QTA_SANDBOX_LIST: ('POST', '/qta/sandbox/list'), Command.QTA_SANDBOX_REMOVE: ('POST', '/qta/sandbox/remove'), Command.QTA_ALBUM_UPLOAD: ('POST', '/qta/album/upload'), Command.QTA_STUB_CALL: ('POST', '/qta/stub'), Command.QTA_DEVICE_VOLUME: ('POST', '/qta/device/volume'), Command.QTA_DEVICE_SIRI: ('POST', '/qta/device/siri'), Command.QTA_DEVICE_SCREEN_DIRECTION: ('POST', '/qta/device/screenDirection'), Command.QTA_DEVICE_DETAIL_INFO: ('POST', '/qta/device/detailInfo'), Command.QTA_WHEEL_SELECT: ('POST', '/session/$sessionId/qta/element/$id/wheel/select'), } def execute(self, command, params): """Send a command to the remote server. Any path subtitutions required for the URL mapped to the command should be included in the command parameters. :param command: A string specifying the command to execute. :type command: str :param params: A dictionary of named parameters to send with the command as its JSON payload. :type params: dict :returns: dict """ command_info = self._commands[command] assert command_info is not None, 'Unrecognised command %s' % command data = json.dumps(params) path = string.Template(command_info[1]).substitute(params) url = '%s%s' % (self._url, path) return self._request(command_info[0], url, body=data) def _request(self, method, url, body=None): """Send an HTTP request to the remote server. :param method: A string for the HTTP method to send the request with. :type method: str :param url: A string for the URL to send the request to. :type url: str :param body: A string for request body. Ignored unless method is POST or PUT. :type body: str :returns: A dictionary with the server's parsed JSON response. """ self.logger.debug('%s %s %s' % (method, url, body)) for _ in range(3): try: parsed_url = parse.urlparse(url) if self.keep_alive: headers = {"Connection": 'keep-alive', method: parsed_url.path, "User-Agent": "Python http auth", "Content-type": "application/json;charset=\"UTF-8\"", "Accept": "application/json"} if parsed_url.username: auth = base64.standard_b64encode(('%s:%s' % (parsed_url.username, parsed_url.password)).encode('ascii')).decode('ascii').replace('\n', '') headers["Authorization"] = "Basic %s" % auth if body and method != 'POST' and method != 'PUT': body = None try: self._conn.request(method, parsed_url.path, body, headers) resp = self._conn.getresponse() except (httplib.HTTPException, socket.error): self._conn.close() raise statuscode = resp.status else: password_manager = None if parsed_url.username: netloc = parsed_url.hostname if parsed_url.port: netloc += ":%s" % parsed_url.port cleaned_url = parse.urlunparse((parsed_url.scheme, netloc, parsed_url.path, parsed_url.params, parsed_url.query, parsed_url.fragment)) password_manager = url_request.HTTPPasswordMgrWithDefaultRealm() password_manager.add_password(None, "%s://%s" % (parsed_url.scheme, netloc), parsed_url.username, parsed_url.password) request = Request(cleaned_url, data=body.encode('utf-8'), method=method) else: request = Request(url, data=body.encode('utf-8'), method=method) request.add_header('Accept', 'application/json') request.add_header('Content-Type', 'application/json;charset=UTF-8') if password_manager: opener = url_request.build_opener(url_request.HTTPRedirectHandler(), HttpErrorHandler(), url_request.HTTPBasicAuthHandler(password_manager), url_request.ProxyHandler({})) else: opener = url_request.build_opener(url_request.HTTPRedirectHandler(), HttpErrorHandler(), url_request.ProxyHandler({})) resp = opener.open(request, timeout=self._timeout) statuscode = resp.code if not hasattr(resp, 'getheader'): if hasattr(resp.headers, 'getheader'): resp.getheader = lambda x: resp.headers.getheader(x) elif hasattr(resp.headers, 'get'): resp.getheader = lambda x: resp.headers.get(x) data = resp.read() try: if 300 <= statuscode < 304: return self._request('GET', resp.getheader('location')) body = data.decode('utf-8').replace('\x00', '').strip() if 399 < statuscode < 500: return {'status': statuscode, 'value': body} content_type = [] if resp.getheader('Content-Type') is not None: content_type = resp.getheader('Content-Type').split(';') if not any([x.startswith('image/png') for x in content_type]): try: data = json.loads(body.strip()) except ValueError: if 199 < statuscode < 300: status = ErrorCode.SUCCESS else: status = ErrorCode.UNKNOWN_ERROR return {'status': status, 'value': body.strip()} assert type(data) is dict, ( 'Invalid server response body: %s' % body) # Some of the drivers incorrectly return a response # with no 'value' field when they should return null. if 'value' not in data: data['value'] = None return data else: data = {'status': 0, 'value': body.strip()} return data finally: self.logger.debug("Finished Request") resp.close() except socket.timeout: self.logger.exception('Remote Connection timeout') raise XCTestAgentTimeoutException('XCTestAgent response is timed out') except Exception, e: self.logger.error('Remote Connection:%s' % str(e))