Example #1
0
class AndroidEngine:
    """
    安卓引擎,自动化启动appium服务,自动连接设备,真机可自动切换WiFi连接,获取driver
    1、实例化前请确保模拟器已开启,真机已连接电脑,并且打开调试模式
    2、暂时只支持单机连接启动,若本机同时连接多个设备,则默认连接已连接设备列表第一个设备
    """
    def __init__(self):
        self._log = Logger('安卓引擎').get_logger()
        # 读取配置
        self._reader = OperateConfig(constant.config_pro_app)
        self._if_wifi = self._reader.get_bool('server', 'if_wifi')
        self._android_mode = self._reader.get_str('server', 'android_mode')

    def get_driver(self):
        """
        根据配置获取driver
        :return: driver对象
        """
        self._start_server()
        devices = self._get_device_names()
        version = self._get_android_version(devices[0])
        app_path = publicFunctions.get_apk_path()
        ports = eval(self._reader.get_str('connected', 'server_ports'))
        if self._android_mode == 'simulator':
            desired_caps = DesiredCaps.caps_android_simulator
            desired_caps['platformVersion'] = version
            desired_caps['deviceName'] = devices[0]
            desired_caps['app'] = app_path
            driver = self._get_driver(desired_caps, ports[0])
            return driver
        elif self._android_mode == 'machine':
            desired_caps = DesiredCaps.caps_android_machine
            desired_caps['platformVersion'] = version
            desired_caps['deviceName'] = devices[0]
            desired_caps['app'] = app_path
            driver = self._get_driver(desired_caps, ports[0])
            return driver
        else:
            self._log.error('启动模式配置有误,请确认:{}'.format(self._android_mode))
            self._kill_server()
            sys.exit()

    def quit_driver(self, driver):
        """
        退出驱动程序,断开模拟器连接,杀掉node进程
        :param driver: driver对象
        :return: None
        """
        if self._android_mode == 'simulator':
            self._disconnect_simulators()
        driver.quit()
        self._kill_server()
        sleep(3)
        self._log.info('已退出驱动')

    def _start_server(self):
        """
        使用命令行自动化启动appium server
        :return: driver对象
        """
        if self._android_mode == 'simulator':
            self._connect_simulators()
        devices = self._get_device_names()
        if self._if_wifi is True and self._android_mode == 'machine':
            self._switch_to_wifi()
            devices = [device for device in self._get_device_names() if ':' in device]
        commands = self._create_appium_commands(devices)
        for cmd in commands:
            cmd = r"start {}".format(cmd)
            os.system(cmd)
            sleep(3)
            self._log.info('appium server已启动:{}'.format(cmd))

    def _get_driver(self, desired_caps: dict, port: str):
        """
        获取driver
        :param desired_caps: 连接参数
        :param port: 服务端口
        :return: driver对象
        """
        try:
            driver = webdriver.Remote(command_executor='http://127.0.0.1:{}/wd/hub'.format(port),
                                      desired_capabilities=desired_caps)
            sleep(1)
            self._log.info('appium server已连接')
            return driver
        except WebDriverException as e:
            self._log.error('appium server连接失败:{}'.format(e))
            sys.exit()

    def _connect_simulators(self):
        """
        对于模拟器,在启动后可以调用此方法实现自动连接电脑
        :return: None
        """
        simulators = self._reader.get_str('server', 'simulator').split(';')
        for simulator in simulators:
            cmd = 'adb connect {}'.format(simulator)
            os.system(cmd)
            self._log.debug('模拟器({})已连接'.format(simulator))

    def _disconnect_simulators(self):
        """
        断开全部已连接模拟器设备
        :return: None
        """
        devices = self._reader.get_str('server', 'simulator').split(';')
        for device in devices:
            cmd = 'adb disconnect {}'.format(device)
            os.system(cmd)
            self._log.debug('设备({})已断开'.format(device))

    def _switch_to_wifi(self):
        """
        对于真机,若需要使用WiFi连接模式,在手机用USB线连接到电脑打开调试模式后,调用此方法可切换至WIFI连接
        :return: None
        """
        devices = self._get_device_names()
        simulators = self._reader.get_str('server', 'simulator').split(';')
        machines = list(set(devices) - set(simulators))
        ports = self._create_useful_ports(5555, machines)
        for machine, port in zip(machines, ports):
            if str(port) in '|'.join(self._get_device_names()):
                cmd_1 = 'adb -s {} shell ip -f inet addr show wlan0'.format(machine)
                result_1 = self._execute_command(cmd_1)
                ip = re.search(r"inet\s(\d+\.\d+\.\d+\.\d+)", result_1).group(1)
                cmd_2 = 'adb -s {} tcpip {}'.format(machine, port)
                os.system(cmd_2)
                cmd_3 = 'adb connect {}:{}'.format(ip, port)
                result_2 = self._execute_command(cmd_3)
                if 'connected' in result_2:
                    self._log.debug('设备({})成功切换至WIFI连接:{}'.format(machine, result_2.strip()))
                    self._log.warning('请拔掉设备({})USB线!!'.format(machine))
                else:
                    self._log.error('设备({})切换至WIFI连接失败:{}'.format(machine, result_2.strip()))

    def _get_device_names(self):
        """
        获取已连接安卓设备名
        :return: 安卓设备名列表
        """
        cmd = 'adb devices'
        result = self._execute_command(cmd)
        devices = re.findall(r"(.*[^\s])\s*device", result)
        devices.pop(0)
        if devices:
            self._log.debug('获取到已连接设备列表:{}'.format(devices))
            return devices
        else:
            self._log.error('未检测到安卓设备。')
            sys.exit()

    def _get_android_version(self, device: str):
        """
        获取已连接安卓设备版本号
        :param device: 设备名
        :return: 版本号
        """
        cmd = f'adb -s {device} shell getprop ro.build.version.release'
        result = self._execute_command(cmd)
        self._log.debug('获取到设备版本号:{}'.format(result))
        return result.strip()

    def _get_package_and_activity(self, apk_path=publicFunctions.get_apk_path()):
        """
        通过'aapt'命令自动获取appPackage和appActivity
        :param apk_path: apk路径
        :return: appPackage和appActivity
        """
        sdk_path = self._get_sdk_path()
        adb_disk = sdk_path.split(':')[0]
        build_tools_path = os.path.join(sdk_path, 'build-tools')
        aapt_path = os.path.join(build_tools_path, os.listdir(build_tools_path)[0])
        cmd = f'{adb_disk}:&cd {aapt_path}&aapt dump badging {apk_path}'
        result = self._execute_command(cmd)
        package = re.search(r"package: name='([\w\\.]+)'", result).group(1)
        activity = re.search(r"launch.*activity: name='([\w\\.]+)'", result).group(1)
        return package, activity

    def _get_sdk_path(self):
        """
        从PATH环境变量中提取Android SDK路径
        :return: Android SDK路径
        """
        path_env = os.environ['PATH']
        sdk_search = re.search(r'(.+?)\\platform-tools', path_env)
        if sdk_search:
            sdk_path = sdk_search.group(1).split(';')[-1]
            if '%' in sdk_path:
                sdk_path = os.environ[sdk_path.strip('%')]
            return sdk_path
        else:
            self._log.error('Android SDK环境变量未配置!!')
            exit()

    @staticmethod
    def _execute_command(cmd: str):
        """
        执行cmd命令
        :param cmd: cmd命令
        :return: 命令行输出
        """
        with os.popen(cmd) as f:
            result = f.read()
        return result

    def _kill_server(self):
        """
        用于每次执行完毕,杀掉进程
        :return: None
        """
        cmd1 = 'tasklist | find "node.exe"'
        if self._execute_command(cmd1):
            cmd2 = 'taskkill -F -PID node.exe'
            self._execute_command(cmd2)
            self._log.info('杀掉appium server进程')

    def _create_appium_commands(self, devices_list: list):
        """
        创建Appium命令行模式启动命令
        :param devices_list: 设备名列表
        :return: cmd命令列表
        """
        p_port_list = self._create_useful_ports(4723, devices_list)
        bp_port_list = self._create_useful_ports(4900, devices_list)
        self._reader.write_data('connected', 'server_ports', str(p_port_list))
        cmd_list = ['appium -a 127.0.0.1 -p {} -bp {}'.format(
            p_port_list[i], bp_port_list[i]
            ) for i in range(len(devices_list))
        ]
        self._log.debug('已生成启动命令:{}'.format(cmd_list))
        return cmd_list

    def _create_useful_ports(self, start_port: int, devices_list: list):
        """
        根据获取的已连接设备创建指定数量的可用端口
        :param start_port: 起始端口
        :param devices_list: 从命令行自动获取的设备列表
        :return: 可用端口列表
        """
        port_list = []
        cmd = 'netstat -ano | findstr {}'.format(start_port)
        while len(port_list) != len(devices_list):
            if not self._execute_command(cmd):
                port_list.append(start_port)
            start_port += 1
        self._log.debug('已生成可用端口:{}'.format(port_list))
        return port_list
class BrowserEngine:
    """
    浏览器引擎,主函数:open_browser()
    """
    def __init__(self):
        # 引用日志类
        self._log = Logger('浏览器引擎').get_logger()
        # 获取配置
        self._reader = OperateConfig(constant.config_pro_web)

    def open_browser(self):
        """
        根据配置决定采用何种模式打开浏览器,获取driver
        :return: driver对象
        """
        headless = self._reader.get_bool('browser', 'headless')
        if headless:
            driver = self._open_browser_with_headless()
        else:
            driver = self._open_browser_without_headless()
        return driver

    def quit_browser(self, driver):
        """
        退出浏览器
        :param driver: driver对象
        :return: None
        """
        driver.quit()
        # 退出浏览器后留缓冲时间,避免立马执行下个case重启浏览器引起进程冲突
        sleep(1.5)
        self._log.info('关闭{}浏览器'.format(driver.name))

    def _open_browser_without_headless(self):
        """
        有界面模式打开浏览器
        :return: driver对象
        """
        driver = None
        browser = self._reader.get_str('browser', 'browser').lower()
        try:
            if browser == 'chrome':
                driver = webdriver.Chrome(constant.chrome_path)
            elif browser == 'firefox':
                driver = webdriver.Firefox(
                    executable_path=constant.firefox_path,
                    service_log_path=devnull)  # 重定向Firefox日志文件至空文件
            elif browser == 'ie':
                driver = webdriver.Ie(constant.ie_path)
            elif browser == 'edge':
                driver = webdriver.Edge(constant.edge_path)
            else:
                self._log.error(f'暂不支持{browser}浏览器!请自行添加!')
                exit()
            try:
                version = driver.capabilities['browserVersion']
            except KeyError:
                version = driver.capabilities['version']
            self._log.info(f'{browser}启动成功,版本号:{version}')
            sleep(1)
            return driver
        except WebDriverException as e:
            self._log.error('{}启动失败:{}'.format(browser, e))
            exit()

    def _open_browser_with_headless(self):
        """
        无头模式打开谷歌或火狐
        :return: driver对象
        """
        driver = None
        browser = self._reader.get_str('browser', 'browser').lower()
        try:
            if browser == 'chrome':
                chrome_options = ChromeOptions()
                chrome_options.add_argument('--headless')
                chrome_options.add_argument('--disable-gpu')
                driver = webdriver.Chrome(options=chrome_options,
                                          executable_path=constant.chrome_path)
            elif browser == 'firefox':
                firefox_options = FirefoxOptions()
                firefox_options.add_argument('--headless')
                firefox_options.add_argument('--disable-gpu')
                driver = webdriver.Firefox(
                    options=firefox_options,
                    executable_path=constant.firefox_path,
                    service_log_path=devnull)
            else:
                self._log.error(f'{browser}配置有误,或{browser}不支持无头模式,请确认!!')
                exit()
            try:
                version = driver.capabilities['browserVersion']
            except KeyError:
                version = driver.capabilities['version']
            self._log.info(f'{browser}启动成功,版本号:{version}')
            sleep(1)
            return driver
        except WebDriverException as e:
            self._log.error('{}无头模式启动失败:{}'.format(browser, e))
            exit()

    def remote_control_browser(self):
        """
        根据配置,获取远程driver
        注:分布式执行用例时,需要先在服务端启动selenium server
        :return: driver对象
        """
        server_address = self._reader.get_str('remote', 'server_address')
        remote_browser = self._reader.get_str('remote',
                                              'remote_browser').upper()
        if hasattr(DesiredCapabilities, remote_browser):
            browser = getattr(DesiredCapabilities, remote_browser)
            try:
                driver = Remote(
                    command_executor='http://{}/wd/hub'.format(server_address),
                    desired_capabilities=browser)
                try:
                    version = driver.capabilities['browserVersion']
                except KeyError:
                    version = driver.capabilities['version']
                self._log.info(f'{browser}启动成功,版本号:{version}')
                return driver
            except WebDriverException as e:
                self._log.error('远程{}启动失败,请确认:{}'.format(remote_browser, e))
                exit()
        else:
            self._log.error(f'{remote_browser}配置有误!')
            exit()