├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── demo.gif ├── fasttest ├── __init__.py ├── common │ ├── __init__.py │ ├── check.py │ ├── decorator.py │ ├── dict.py │ ├── log.py │ └── variable_global.py ├── driver.py ├── drivers │ ├── __init__.py │ ├── appium │ │ ├── __init__.py │ │ └── driver_appium.py │ ├── driver.py │ ├── driver_base_app.py │ ├── driver_base_web.py │ └── macaca │ │ ├── __init__.py │ │ └── driver_macaca.py ├── fasttest_runner.py ├── keywords │ ├── __init__.py │ └── keywords.py ├── project.py ├── result │ ├── __init__.py │ ├── html_result.py │ ├── resource │ │ ├── css.css │ │ └── js.js │ ├── test_result.py │ └── test_runner.py ├── runner │ ├── __init__.py │ ├── action_analysis.py │ ├── action_executor_app.py │ ├── action_executor_base.py │ ├── action_executor_web.py │ ├── case_analysis.py │ ├── run_case.py │ └── test_case.py ├── utils │ ├── __init__.py │ ├── devices_utils.py │ ├── opcv_utils.py │ ├── server_utils_app.py │ ├── server_utils_web.py │ ├── testcast_utils.py │ └── yaml_utils.py └── version.py ├── requirements.txt └── setup.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '39 17 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript', 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.gitignore 3 | /.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include fasttest/result/resource/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `fasttest` 在`macaca`、`appium`、`selenium`的基础上做了一层关键字映射,在`yaml`文件上编写自动化用例,即使无代码基础的同学也已可以很快上手自动化测试 2 | 3 | ![](https://img.shields.io/badge/python-3.7-green) 4 | 5 | #### 我能做什么 6 | - 支持`IDEA`、`Pycharm`插件,在`yaml`文件上写用例可智能联想关键字 --> [FastYaml](https://plugins.jetbrains.com/plugin/16600-fastyaml) 7 | - 支持实时`debug`用例步骤,无需重复运行验证 8 | - 支持现有关键字组合、自定义关键字,拥有无限扩展性 9 | - 支持`PO`模式、支持`iOS`、`Android`两端共用一份用例 10 | - 支持`if`、`while`、`for`等语法用于构造复杂场景 11 | - 支持`CLI`命令,支持`Jenkins`持续集成 12 | - 支持多设备并行执行 13 | 14 | #### 演示↓↓↓ 15 | ![](https://github.com/Jodeee/fasttest/blob/master/demo.gif) 16 | 17 | 更多请点击 [fasttest](https://www.yuque.com/jodeee/vt6gkg/oue9xb) 18 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jodeee/fasttest/827292908f9ea8623bb4f58aeac4b12109ecb991/demo.gif -------------------------------------------------------------------------------- /fasttest/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.common import Var 4 | from fasttest.project import Project 5 | 6 | -------------------------------------------------------------------------------- /fasttest/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.common.dict import Dict, DictEncoder 4 | from fasttest.common.variable_global import Var 5 | from fasttest.common.log import log_info, log_error 6 | 7 | __all__ = ['log_info','log_error','Var', 'Dict', 'DictEncoder'] 8 | -------------------------------------------------------------------------------- /fasttest/common/check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import traceback 4 | from fasttest.common import log_error 5 | from selenium.common.exceptions import WebDriverException 6 | 7 | def check(func, *args, **kwds): 8 | def wrapper(*args, **kwds): 9 | index = 10 10 | result = None 11 | while index: 12 | try: 13 | if args or kwds: 14 | result = func(*args, **kwds) 15 | else: 16 | result = func() 17 | break 18 | except WebDriverException as e: 19 | log_error(e.msg, False) 20 | index -= 1 21 | if index == 0: 22 | raise e 23 | except Exception as e: 24 | raise e 25 | return result 26 | return wrapper -------------------------------------------------------------------------------- /fasttest/common/decorator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | try: 5 | import cv2 6 | except: 7 | pass 8 | import time 9 | from fasttest.common import * 10 | 11 | def mach_keywords(func, *args, **kwds): 12 | def wrapper(*args, **kwds): 13 | start_time = time.time() 14 | result = None 15 | try: 16 | if args or kwds: 17 | result = func(*args, **kwds) 18 | else: 19 | result = func() 20 | except Exception as e: 21 | Var.case_snapshot_index += 1 22 | Var.exception_flag = False 23 | snapshot_index = Var.case_snapshot_index 24 | imagename = "Step_{}.png".format(snapshot_index) 25 | file = os.path.join(Var.snapshot_dir, imagename) 26 | action_step = args[1] 27 | style = args[-1] 28 | try: 29 | Var.instance.save_screenshot(file) 30 | except: 31 | log_error(' screenshot failed!', False) 32 | 33 | stop_time = time.time() 34 | duration = str('%.2f' % (stop_time - start_time)) 35 | 36 | # call action中某一语句抛出异常,会导致call action状态也是false,需要处理 37 | status = False 38 | if Var.exception_flag: 39 | status = True 40 | 41 | Var.test_case_steps[snapshot_index] = { 42 | 'index': snapshot_index, 43 | 'status': status, 44 | 'duration': duration, 45 | 'snapshot': file, 46 | 'step': f'{style}- {action_step}', 47 | 'result': result if result is not None else '' 48 | } 49 | raise e 50 | 51 | return result 52 | return wrapper 53 | 54 | def executor_keywords(func, *args, **kwds): 55 | def wrapper(*args, **kwds): 56 | result = None 57 | exception_flag = False 58 | exception = None 59 | Var.ocrimg = None 60 | start_time = time.time() 61 | Var.case_snapshot_index += 1 62 | Var.exception_flag = False 63 | snapshot_index = Var.case_snapshot_index 64 | imagename = "Step_{}.png".format(snapshot_index) 65 | file = os.path.join(Var.snapshot_dir, imagename) 66 | action_step = args[-2].step 67 | style = args[-1] 68 | try: 69 | if args or kwds: 70 | result = func(*args, **kwds) 71 | else: 72 | result = func() 73 | except Exception as e: 74 | exception = e 75 | exception_flag = True 76 | finally: 77 | try: 78 | if Var.ocrimg is not None: 79 | # matchImage,绘制图片 80 | cv2.imwrite(file, Var.ocrimg) 81 | Var.ocrimg = None 82 | elif Var.save_screenshot: 83 | # 全局参数 84 | Var.instance.save_screenshot(file) 85 | elif not Var.exception_flag and exception_flag: 86 | # call出现异常 87 | Var.instance.save_screenshot(file) 88 | except: 89 | Var.ocrimg = None 90 | log_error(' screenshot failed!', False) 91 | 92 | # 步骤执行时间 93 | stop_time = time.time() 94 | duration = str('%.2f' % (stop_time - start_time)) 95 | 96 | # 步骤执行结果 97 | if result is not None: 98 | action_result = f'{result}'.replace("<", "{").replace(">", "}") 99 | else: 100 | action_result = '' 101 | 102 | # call action中某一语句抛出异常,会导致call action状态也是false,需要处理 103 | status = not exception_flag 104 | if Var.exception_flag: 105 | status = True 106 | 107 | Var.test_case_steps[snapshot_index] = { 108 | 'index': snapshot_index, 109 | 'status': status, 110 | 'duration': duration, 111 | 'snapshot': file, 112 | 'step': f'{style}- {action_step}', 113 | 'result': action_result 114 | } 115 | if exception_flag: 116 | raise exception 117 | return result 118 | return wrapper -------------------------------------------------------------------------------- /fasttest/common/dict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import collections 5 | try: 6 | from appium.webdriver import WebElement 7 | except: 8 | pass 9 | try: 10 | from macaca.webdriver import WebElement 11 | except: 12 | pass 13 | 14 | class Dict(collections.UserDict): 15 | def __missing__(self, key): 16 | return None 17 | 18 | def __contains__(self, item): 19 | return str(item) in self.data 20 | 21 | def __setitem__(self, key, value): 22 | if isinstance(value,dict): 23 | _item = Dict() 24 | for _key ,_value in value.items(): 25 | _item[_key] = _value 26 | self.data[str(key)] = _item 27 | else: 28 | self.data[str(key)] = value 29 | 30 | def __getattr__(self, item): 31 | if item in self: 32 | return self[str(item)] 33 | else: 34 | return None 35 | 36 | def __copy__(self): 37 | n_d = type(self)() 38 | n_d.__dict__.update(self.__dict__) 39 | return n_d 40 | 41 | class DictEncoder(json.JSONEncoder): 42 | 43 | def default(self, obj): 44 | if isinstance(obj, Dict): 45 | d = {} 46 | for k, v in obj.items(): 47 | d[k] = v 48 | return d 49 | elif isinstance(obj, WebElement): 50 | return str(obj) 51 | else: 52 | return json.JSONEncoder.default(self, obj) 53 | -------------------------------------------------------------------------------- /fasttest/common/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | import datetime 6 | import platform 7 | from colorama import init, Fore, Back, Style 8 | from fasttest.common import * 9 | 10 | logger = None 11 | if platform.system() != 'Windows': 12 | init(wrap=True) 13 | init(autoreset=True) 14 | 15 | def write(message): 16 | try: 17 | log_file_path = os.path.join(Var.report, "project.log") 18 | with open(log_file_path, 'a+', encoding='UTF-8') as f: 19 | f.write(f'{message}\n') 20 | except: 21 | pass 22 | 23 | def log_info(message,color=None): 24 | 25 | format_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S INFO :") 26 | if not isinstance(message, str): 27 | message = str(message) 28 | if color: 29 | print(format_str + color + message) 30 | else: 31 | print(format_str + message) 32 | write(format_str + message) 33 | 34 | def log_error(message, exit=True): 35 | 36 | format_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S ERROR :") 37 | print(format_str + Fore.RED + message) 38 | write(format_str + message) 39 | if exit: 40 | os._exit(0) 41 | 42 | 43 | -------------------------------------------------------------------------------- /fasttest/common/variable_global.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import threading 4 | class VariableGlobal(object): 5 | 6 | def __getattr__(self, item): 7 | try: 8 | name = threading.currentThread().getName() 9 | value = self.__getattribute__(name) 10 | return value[item] 11 | except: 12 | return None 13 | 14 | def __setattr__(self, key, value): 15 | name = threading.currentThread().getName() 16 | try: 17 | item = self.__getattribute__(name) 18 | except: 19 | item = {} 20 | item.update({key: value}) 21 | object.__setattr__(self, name, item) 22 | 23 | def __setitem__(self, key, value): 24 | name = threading.currentThread().getName() 25 | try: 26 | item = self.__getattribute__(name) 27 | except: 28 | item = {} 29 | item.update({key: value}) 30 | object.__setattr__(self, name, item) 31 | 32 | Var = VariableGlobal() -------------------------------------------------------------------------------- /fasttest/driver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from fasttest.common import log_info, log_error 5 | from fasttest.drivers.driver import wd -------------------------------------------------------------------------------- /fasttest/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /fasttest/drivers/appium/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.drivers.appium.driver_appium import AndroidDriver, iOSDriver 4 | 5 | 6 | __all__ = ['AndroidDriver','iOSDriver'] 7 | -------------------------------------------------------------------------------- /fasttest/drivers/appium/driver_appium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import time 5 | import traceback 6 | import subprocess 7 | from fasttest.common import * 8 | from appium.webdriver.common.touch_action import TouchAction 9 | 10 | class AndroidDriver(object): 11 | 12 | @staticmethod 13 | def adb_shell(cmd): 14 | ''' 15 | :param cmd: 16 | :return: 17 | ''' 18 | try: 19 | log_info(' adb: {}'.format(cmd)) 20 | if cmd.startswith('shell'): 21 | cmd = ["adb", "-s", Var.desired_caps.udid, "shell", "{}".format(cmd.lstrip('shell').strip())] 22 | pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, 23 | stdout=subprocess.PIPE) 24 | out = pipe.communicate() 25 | else: 26 | cmd = ["adb", "-s", Var.desired_caps.udid, "{}".format(cmd)] 27 | os.system(' '.join(cmd)) 28 | except: 29 | raise Exception(traceback.format_exc()) 30 | 31 | @staticmethod 32 | def install_app(app_path): 33 | ''' 34 | install app 35 | :param app_path: 36 | :return: 37 | ''' 38 | try: 39 | Var.instance.install_app(app_path) 40 | except Exception as e: 41 | raise e 42 | 43 | @staticmethod 44 | def uninstall_app(package_info): 45 | ''' 46 | uninstall app 47 | :param package_info: Android(package) or iOS(bundleId) 48 | :return: 49 | ''' 50 | try: 51 | Var.instance.remove_app(package_info) 52 | except Exception as e: 53 | raise e 54 | 55 | @staticmethod 56 | def launch_app(package_info): 57 | ''' 58 | launch app 59 | :param package_info: Android(package/activity) or iOS(bundleId) 60 | :return: 61 | ''' 62 | try: 63 | if not package_info: 64 | Var.instance.launch_app() 65 | else: 66 | AndroidDriver.adb_shell('shell am start -W {}'.format(package_info)) 67 | 68 | except Exception as e: 69 | raise e 70 | 71 | @staticmethod 72 | def close_app(package_info): 73 | ''' 74 | close app 75 | :param package_info: Android(package) or iOS(bundleId) 76 | :return: 77 | ''' 78 | try: 79 | if not package_info: 80 | Var.instance.close_app() 81 | else: 82 | AndroidDriver.adb_shell('shell am force-stop {}'.format(package_info)) 83 | except Exception as e: 84 | raise e 85 | 86 | @staticmethod 87 | def background_app(): 88 | ''' 89 | only appium 90 | :return: 91 | ''' 92 | try: 93 | Var.instance.background_app() 94 | except Exception as e: 95 | raise e 96 | 97 | @staticmethod 98 | def tap(x, y): 99 | ''' 100 | :param x: 101 | :param y: 102 | :return: 103 | ''' 104 | try: 105 | width = Var.instance.get_window_size()['width'] 106 | height = Var.instance.get_window_size()['height'] 107 | if x <= 1.0: 108 | x = x * width 109 | if y <= 1.0: 110 | y = y * height 111 | Var.instance.tap([(int(x), int(y))]) 112 | except Exception as e: 113 | raise e 114 | 115 | @staticmethod 116 | def double_tap(x, y): 117 | ''' 118 | :param x: 119 | :param y: 120 | :return: 121 | ''' 122 | try: 123 | width = Var.instance.get_window_size()['width'] 124 | height = Var.instance.get_window_size()['height'] 125 | if x <= 1.0: 126 | x = x * width 127 | if y <= 1.0: 128 | y = y * height 129 | TouchAction(Var.instance).press(x=int(x), y=int(y), pressure=0.25).release().perform().wait(110). \ 130 | press(x=int(x), y=int(y), pressure=0.25).release().perform() 131 | except Exception as e: 132 | raise e 133 | 134 | @staticmethod 135 | def press(x, y, duration=2): 136 | ''' 137 | :param x: 138 | :param y: 139 | :param duration: 140 | :return: 141 | ''' 142 | try: 143 | width = Var.instance.get_window_size()['width'] 144 | height = Var.instance.get_window_size()['height'] 145 | if x <= 1.0: 146 | x = x * width 147 | if y <= 1.0: 148 | y = y * height 149 | Var.instance.long_press(x=int(x), y=int(y), duration=duration) 150 | except Exception as e: 151 | raise e 152 | 153 | @staticmethod 154 | def press(element, duration=2): 155 | ''' 156 | :param element: 157 | :param duration: 158 | :return: 159 | ''' 160 | try: 161 | Var.instance.long_press(element=element, duration=duration) 162 | except Exception as e: 163 | raise e 164 | 165 | @staticmethod 166 | def swipe_up(duration=2): 167 | ''' 168 | :param duration: 169 | :return: 170 | ''' 171 | try: 172 | width = Var.instance.get_window_size()['width'] 173 | height = Var.instance.get_window_size()['height'] 174 | AndroidDriver.swipe(width / 2, height * 0.65, width / 2, height / 4, duration) 175 | except Exception as e: 176 | raise e 177 | 178 | @staticmethod 179 | def swipe_down(duration=2): 180 | ''' 181 | :param duration: 182 | :return: 183 | ''' 184 | try: 185 | width = Var.instance.get_window_size()['width'] 186 | height = Var.instance.get_window_size()['height'] 187 | AndroidDriver.swipe(width / 2, height / 4, width / 2, height * 3 / 4, duration) 188 | except Exception as e: 189 | raise e 190 | 191 | @staticmethod 192 | def swipe_left(duration=2): 193 | ''' 194 | :param duration: 195 | :return: 196 | ''' 197 | try: 198 | width = Var.instance.get_window_size()['width'] 199 | height = Var.instance.get_window_size()['height'] 200 | AndroidDriver.swipe(width * 3 / 4, height / 2, width / 4, height / 2, duration) 201 | except Exception as e: 202 | raise e 203 | 204 | @staticmethod 205 | def swipe_right(duration=2): 206 | ''' 207 | :param duration: 208 | :return: 209 | ''' 210 | try: 211 | width = Var.instance.get_window_size()['width'] 212 | height = Var.instance.get_window_size()['height'] 213 | AndroidDriver.swipe(width / 4, height / 2, width * 3 / 4, height / 2, duration) 214 | except Exception as e: 215 | raise e 216 | 217 | @staticmethod 218 | def swipe(from_x, from_y, to_x, to_y, duration=3): 219 | ''' 220 | :param from_x: 221 | :param from_y: 222 | :param to_x: 223 | :param to_y: 224 | :param duration: 225 | :return: 226 | ''' 227 | try: 228 | width = Var.instance.get_window_size()['width'] 229 | height = Var.instance.get_window_size()['height'] 230 | if from_x <= 1.0: 231 | from_x = from_x * width 232 | if from_y <= 1.0: 233 | from_y = from_y * height 234 | if to_x <= 1.0: 235 | to_x = to_x * width 236 | if to_y <= 1.0: 237 | to_y = to_y * height 238 | AndroidDriver.adb_shell( 239 | 'shell input swipe {} {} {} {} {}'.format(from_x, from_y, to_x, to_y, duration * 100)) 240 | except Exception as e: 241 | raise e 242 | 243 | @staticmethod 244 | def input(element, text, clear=True, hide_keyboard=True): 245 | ''' 246 | :param element: 247 | :param text: 248 | :param clear: 249 | :param hide_keyboard: 250 | :return: 251 | ''' 252 | try: 253 | # if clear: 254 | # AndroidDriver.clear() 255 | # if hide_keyboard: 256 | # AndroidDriver.hide_keyboard() 257 | element.send_keys(text) 258 | except Exception as e: 259 | raise e 260 | 261 | @staticmethod 262 | def get_text(element): 263 | ''' 264 | :param element: 265 | :return: 266 | ''' 267 | try: 268 | text = element.text 269 | return text 270 | except Exception as e: 271 | raise e 272 | 273 | @staticmethod 274 | def clear(): 275 | ''' 276 | :return: 277 | ''' 278 | try: 279 | Var.instance.clear() 280 | except: 281 | traceback.print_exc() 282 | 283 | @staticmethod 284 | def hide_keyboard(): 285 | ''' 286 | :return: 287 | ''' 288 | try: 289 | Var.instance.hide_keyboard() 290 | except: 291 | traceback.print_exc() 292 | 293 | @staticmethod 294 | def wait_for_elements_by_id(id, timeout=10, interval=1): 295 | ''' 296 | :param id: 297 | :return: 298 | ''' 299 | try: 300 | elements = Var.instance.find_elements_by_id(id) 301 | return elements 302 | except Exception as e: 303 | raise e 304 | 305 | @staticmethod 306 | def wait_for_elements_by_name(name, timeout=10, interval=1): 307 | ''' 308 | :param name: 309 | :return: 310 | ''' 311 | try: 312 | elements = Var.instance.find_elements_by_android_uiautomator('new UiSelector().text("{}")'.format(name)) 313 | return elements 314 | except Exception as e: 315 | raise e 316 | 317 | @staticmethod 318 | def wait_for_elements_by_xpath(xpath, timeout=10, interval=1): 319 | ''' 320 | :param xpath: 321 | :return: 322 | ''' 323 | try: 324 | elements = Var.instance.find_elements_by_xpath(xpath) 325 | return elements 326 | except Exception as e: 327 | raise e 328 | 329 | @staticmethod 330 | def wait_for_elements_by_classname(classname, timeout=10, interval=1): 331 | ''' 332 | :param classname: 333 | :return: 334 | ''' 335 | try: 336 | elements = Var.instance.find_elements_by_class_name(classname) 337 | return elements 338 | except Exception as e: 339 | raise e 340 | 341 | class iOSDriver(object): 342 | 343 | @staticmethod 344 | def install_app(app_path): 345 | ''' 346 | install app 347 | :param app_path: 348 | :return: 349 | ''' 350 | try: 351 | Var.instance.install_app(app_path) 352 | except Exception as e: 353 | raise e 354 | 355 | @staticmethod 356 | def uninstall_app(package_info): 357 | ''' 358 | uninstall app 359 | :param package_info: Android(package) or iOS(bundleId) 360 | :return: 361 | ''' 362 | try: 363 | Var.instance.remove_app(package_info) 364 | except Exception as e: 365 | raise e 366 | 367 | @staticmethod 368 | def launch_app(package_info): 369 | ''' 370 | launch app 371 | :param package_info: Android(package/activity) or iOS(bundleId) 372 | :return: 373 | ''' 374 | try: 375 | if not package_info: 376 | Var.instance.launch_app() 377 | else: 378 | pass # todo 待补充 379 | except Exception as e: 380 | raise e 381 | 382 | @staticmethod 383 | def close_app(package_info): 384 | ''' 385 | close app 386 | :param package_info: Android(package) or iOS(bundleId) 387 | :return: 388 | ''' 389 | try: 390 | if not package_info: 391 | Var.instance.close_app() 392 | else: 393 | pass # todo 待补充 394 | except Exception as e: 395 | raise e 396 | 397 | @staticmethod 398 | def background_app(): 399 | ''' 400 | only appium 401 | :return: 402 | ''' 403 | try: 404 | Var.instance.background_app() 405 | except Exception as e: 406 | raise e 407 | 408 | @staticmethod 409 | def tap(x, y): 410 | ''' 411 | :param x: 412 | :param y: 413 | :return: 414 | ''' 415 | try: 416 | width = Var.instance.get_window_size()['width'] 417 | height = Var.instance.get_window_size()['height'] 418 | if x <= 1.0: 419 | x = x * width 420 | if y <= 1.0: 421 | y = y * height 422 | Var.instance.tap([(int(x), int(y))]) 423 | except Exception as e: 424 | raise e 425 | 426 | @staticmethod 427 | def double_tap(x, y): 428 | ''' 429 | :param x: 430 | :param y: 431 | :return: 432 | ''' 433 | try: 434 | width = Var.instance.get_window_size()['width'] 435 | height = Var.instance.get_window_size()['height'] 436 | if x <= 1.0: 437 | x = x * width 438 | if y <= 1.0: 439 | y = y * height 440 | TouchAction(Var.instance).press(x=int(x), y=int(y), pressure=0.25).release().perform().wait(110). \ 441 | press(x=int(x), y=int(y), pressure=0.25).release().perform() 442 | except Exception as e: 443 | raise e 444 | 445 | @staticmethod 446 | def press(x, y, duration=2): 447 | ''' 448 | :param x: 449 | :param y: 450 | :param duration: 451 | :return: 452 | ''' 453 | try: 454 | width = Var.instance.get_window_size()['width'] 455 | height = Var.instance.get_window_size()['height'] 456 | if x <= 1.0: 457 | x = x * width 458 | if y <= 1.0: 459 | y = y * height 460 | Var.instance.long_press(x=int(x), y=int(y), duration=duration) 461 | except Exception as e: 462 | raise e 463 | 464 | @staticmethod 465 | def press(element, duration=2): 466 | ''' 467 | :param element: 468 | :param duration: 469 | :return: 470 | ''' 471 | try: 472 | Var.instance.long_press(element=element, duration=duration) 473 | except Exception as e: 474 | raise e 475 | 476 | @staticmethod 477 | def swipe_up(duration=2): 478 | ''' 479 | :param duration: 480 | :return: 481 | ''' 482 | try: 483 | width = Var.instance.get_window_size()['width'] 484 | height = Var.instance.get_window_size()['height'] 485 | iOSDriver.swipe(width / 2, height * 0.65, width / 2, height / 4, duration) 486 | except Exception as e: 487 | raise e 488 | 489 | @staticmethod 490 | def swipe_down(duration=2): 491 | ''' 492 | :param duration: 493 | :return: 494 | ''' 495 | try: 496 | width = Var.instance.get_window_size()['width'] 497 | height = Var.instance.get_window_size()['height'] 498 | iOSDriver.swipe(width / 2, height / 4, width / 2, height * 3 / 4, duration) 499 | except Exception as e: 500 | raise e 501 | 502 | @staticmethod 503 | def swipe_left(duration=2): 504 | ''' 505 | :param duration: 506 | :return: 507 | ''' 508 | try: 509 | width = Var.instance.get_window_size()['width'] 510 | height = Var.instance.get_window_size()['height'] 511 | iOSDriver.swipe(width * 3 / 4, height / 2, width / 4, height / 2, duration) 512 | except Exception as e: 513 | raise e 514 | 515 | @staticmethod 516 | def swipe_right(duration=2): 517 | ''' 518 | :param duration: 519 | :return: 520 | ''' 521 | try: 522 | width = Var.instance.get_window_size()['width'] 523 | height = Var.instance.get_window_size()['height'] 524 | iOSDriver.swipe(width / 4, height / 2, width * 3 / 4, height / 2, duration) 525 | except Exception as e: 526 | raise e 527 | 528 | @staticmethod 529 | def swipe(from_x, from_y, to_x, to_y, duration=2): 530 | ''' 531 | :param from_x: 532 | :param from_y: 533 | :param to_x: 534 | :param to_y: 535 | :param duration: 536 | :return: 537 | ''' 538 | try: 539 | width = Var.instance.get_window_size()['width'] 540 | height = Var.instance.get_window_size()['height'] 541 | if from_x <= 1.0: 542 | from_x = from_x * width 543 | if from_y <= 1.0: 544 | from_y = from_y * height 545 | if to_x <= 1.0: 546 | to_x = to_x * width 547 | if to_y <= 1.0: 548 | to_y = to_y * height 549 | Var.instance.swipe(int(from_x), int(from_y), int(to_x), int(to_y), int(duration * 100)) 550 | except Exception as e: 551 | raise e 552 | 553 | @staticmethod 554 | def input(element, text, clear=True, hide_keyboard=True): 555 | ''' 556 | :param element: 557 | :param text: 558 | :param clear: 559 | :param hide_keyboard: 560 | :return: 561 | ''' 562 | try: 563 | # if clear: 564 | # iOSDriver.clear() 565 | # if hide_keyboard: 566 | # iOSDriver.hide_keyboard() 567 | element.send_keys(text) 568 | except Exception as e: 569 | raise e 570 | 571 | @staticmethod 572 | def get_text(element): 573 | ''' 574 | :param element: 575 | :return: 576 | ''' 577 | try: 578 | text = element.text 579 | return text 580 | except Exception as e: 581 | raise e 582 | 583 | @staticmethod 584 | def clear(): 585 | ''' 586 | :return: 587 | ''' 588 | try: 589 | Var.instance.clear() 590 | except: 591 | traceback.print_exc() 592 | 593 | @staticmethod 594 | def hide_keyboard(): 595 | ''' 596 | :return: 597 | ''' 598 | try: 599 | Var.instance.hide_keyboard() 600 | except: 601 | traceback.print_exc() 602 | 603 | @staticmethod 604 | def wait_for_elements_by_id(id, timeout=10, interval=1): 605 | ''' 606 | :param id: 607 | :return: 608 | ''' 609 | try: 610 | elements = Var.instance.find_elements_by_id(id) 611 | return elements 612 | except Exception as e: 613 | raise e 614 | 615 | @staticmethod 616 | def wait_for_elements_by_name(name, timeout=10, interval=1): 617 | ''' 618 | :param name: 619 | :return: 620 | ''' 621 | try: 622 | elements = Var.instance.find_elements_by_accessibility_id(name) 623 | return elements 624 | except Exception as e: 625 | raise e 626 | 627 | @staticmethod 628 | def wait_for_elements_by_xpath(xpath, timeout=10, interval=1): 629 | ''' 630 | :param xpath: 631 | :return: 632 | ''' 633 | try: 634 | elements = Var.instance.find_elements_by_xpath(xpath) 635 | return elements 636 | except Exception as e: 637 | raise e 638 | 639 | @staticmethod 640 | def wait_for_elements_by_classname(classname, timeout=10, interval=1): 641 | ''' 642 | :param classname: 643 | :return: 644 | ''' 645 | try: 646 | elements = Var.instance.find_elements_by_class_name(classname) 647 | return elements 648 | except Exception as e: 649 | raise e 650 | 651 | 652 | -------------------------------------------------------------------------------- /fasttest/drivers/driver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.common import Var 4 | 5 | class WebDriver(object): 6 | 7 | def __init__(self): 8 | self.driver = None 9 | 10 | def __getattribute__(self, item): 11 | try: 12 | if item == 'driver': 13 | self.driver = Var.instanc 14 | return Var.instance 15 | else: 16 | return None 17 | except: 18 | return None 19 | 20 | wd = WebDriver() 21 | -------------------------------------------------------------------------------- /fasttest/drivers/driver_base_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | from fasttest.common import * 5 | 6 | class DriverBaseApp(object): 7 | 8 | 9 | @staticmethod 10 | def init(): 11 | try: 12 | global driver 13 | if Var.driver.lower() == 'appium': 14 | from fasttest.drivers.appium import AndroidDriver, iOSDriver 15 | else: 16 | from fasttest.drivers.macaca import AndroidDriver, iOSDriver 17 | 18 | if Var.desired_caps.platformName.lower() == "ios": 19 | driver = iOSDriver 20 | elif Var.desired_caps.platformName.lower() == "android": 21 | driver = AndroidDriver 22 | Var.driver_instance = driver 23 | except Exception as e: 24 | raise e 25 | 26 | @staticmethod 27 | def adb_shell(cmd): 28 | """onlu Android 29 | Args: 30 | command 31 | Usage: 32 | adbshell 'adb devices' 33 | Returns: 34 | None 35 | """ 36 | driver.adb_shell(cmd) 37 | 38 | @staticmethod 39 | def install_app(app_path): 40 | ''' 41 | install app 42 | :param app_path: 43 | :return: 44 | ''' 45 | driver.install_app(app_path) 46 | 47 | @staticmethod 48 | def uninstall_app(package_info): 49 | ''' 50 | uninstall app 51 | :param package_info: Android(package) or iOS(bundleId) 52 | :return: 53 | ''' 54 | driver.uninstall_app(package_info) 55 | 56 | @staticmethod 57 | def launch_app(package_info): 58 | ''' 59 | launch app 60 | :param package_info: Android(package/activity) or iOS(bundleId) 61 | :return: 62 | ''' 63 | driver.launch_app(package_info) 64 | 65 | @staticmethod 66 | def close_app(package_info): 67 | ''' 68 | close app 69 | :param package_info: Android(package) or iOS(bundleId) 70 | :return: 71 | ''' 72 | driver.close_app(package_info) 73 | 74 | @staticmethod 75 | def background_app(): 76 | ''' 77 | only appium 78 | :return: 79 | ''' 80 | driver.background_app() 81 | 82 | @staticmethod 83 | def tap(x, y): 84 | ''' 85 | :param x: 86 | :param y: 87 | :return: 88 | ''' 89 | driver.tap(x, y) 90 | 91 | @staticmethod 92 | def double_tap(x, y): 93 | ''' 94 | :param x: 95 | :param y: 96 | :return: 97 | ''' 98 | driver.double_tap(x, y) 99 | 100 | @staticmethod 101 | def press(x, y, duration=2): 102 | ''' 103 | :param x: 104 | :param y: 105 | :param duration: 106 | :return: 107 | ''' 108 | driver.press(x, y, duration) 109 | 110 | @staticmethod 111 | def press(element, duration=2): 112 | ''' 113 | :param element: 114 | :param duration: 115 | :return: 116 | ''' 117 | driver.press(element, duration) 118 | 119 | @staticmethod 120 | def swipe_up(duration=2): 121 | ''' 122 | :param duration: 123 | :return: 124 | ''' 125 | driver.swipe_up(duration) 126 | 127 | @staticmethod 128 | def swipe_down(duration=2): 129 | ''' 130 | :param duration: 131 | :return: 132 | ''' 133 | driver.swipe_down(duration) 134 | 135 | @staticmethod 136 | def swipe_left(duration=2): 137 | ''' 138 | :param duration: 139 | :return: 140 | ''' 141 | driver.swipe_left(duration) 142 | 143 | @staticmethod 144 | def swipe_right(duration=2): 145 | ''' 146 | :param duration: 147 | :return: 148 | ''' 149 | driver.swipe_right(duration) 150 | 151 | @staticmethod 152 | def swipe(from_x, from_y, to_x, to_y, duration=2): 153 | ''' 154 | :param from_x: 155 | :param from_y: 156 | :param to_x: 157 | :param to_y: 158 | :param duration: 159 | :return: 160 | ''' 161 | driver.swipe(from_x, from_y, to_x, to_y, duration) 162 | 163 | @staticmethod 164 | def move_to(x, y): 165 | ''' 166 | :param x: 167 | :param y: 168 | :return: 169 | ''' 170 | driver.move_to(x, y) 171 | 172 | @staticmethod 173 | def click(element): 174 | ''' 175 | :param element: 176 | :return: 177 | ''' 178 | element.click() 179 | 180 | @staticmethod 181 | def check(element): 182 | ''' 183 | :param element: 184 | :return: 185 | ''' 186 | if not element: 187 | return False 188 | return True 189 | 190 | @staticmethod 191 | def input(element, text='', clear=True): 192 | ''' 193 | :param element: 194 | :param text: 195 | :param clear: 196 | :return: 197 | ''' 198 | driver.input(element, text) 199 | 200 | @staticmethod 201 | def get_text(element, index=0): 202 | ''' 203 | :param element: 204 | :param index: 205 | :return: 206 | ''' 207 | text = driver.get_text(element) 208 | return text 209 | 210 | @staticmethod 211 | def find_elements_by_key(key, timeout=10, interval=1, index=0, not_processing=False): 212 | ''' 213 | :param key: 214 | :param timeout: 215 | :param interval: 216 | :param index: 217 | :param not_processing: 不处理数据 218 | :return: 219 | ''' 220 | if not interval: 221 | interval = 0.5 222 | dict = { 223 | 'element': key, 224 | 'timeout': timeout, 225 | 'interval': interval, 226 | 'index': index, 227 | 'not_processing': not_processing 228 | } 229 | if Var.desired_caps.platformName.lower() == 'android': 230 | if re.match(r'[a-zA-Z]+\.[a-zA-Z]+[\.\w]+:id/\S+', key): 231 | dict['element_type'] = 'id' 232 | elif re.match(r'android\.[a-zA-Z]+[\.(a-zA-Z)]+', key) or re.match(r'[a-zA-Z]+\.[a-zA-Z]+[\.(a-zA-Z)]+', key): 233 | dict['element_type'] = 'classname' 234 | elif re.match('//\*\[@\S+=\S+\]', key) or re.match('//[a-zA-Z]+\.[a-zA-Z]+[\.(a-zA-Z)]+\[\d+\]', key): 235 | dict['element_type'] = 'xpath' 236 | else: 237 | dict['element_type'] = 'name' 238 | else: 239 | if re.match(r'XCUIElementType', key): 240 | dict['element_type'] = 'classname' 241 | elif re.match(r'//XCUIElementType', key): 242 | dict['element_type'] = 'xpath' 243 | elif re.match(r'//\*\[@\S+=\S+\]', key): 244 | dict['element_type'] = 'xpath' 245 | else: 246 | dict['element_type'] = 'name' 247 | return DriverBaseApp.wait_for_elements_by_key(dict) 248 | 249 | @staticmethod 250 | def wait_for_elements_by_key(elements_info): 251 | ''' 252 | :param elements_info: 253 | :return: 254 | ''' 255 | 256 | element_type = elements_info['element_type'] 257 | element = elements_info['element'] 258 | timeout = elements_info['timeout'] 259 | interval = elements_info['interval'] 260 | index = elements_info['index'] 261 | not_processing = elements_info['not_processing'] 262 | log_info(" --> body: {'using': '%s', 'value': '%s', 'index': %s, 'timeout': %s}" % (element_type, element, index, timeout)) 263 | if element_type == 'name': 264 | elements = driver.wait_for_elements_by_name(name=element, timeout=timeout, interval=interval) 265 | elif element_type == 'id': 266 | elements = driver.wait_for_elements_by_id(id=element, timeout=timeout, interval=interval) 267 | elif element_type == 'xpath': 268 | elements = driver.wait_for_elements_by_xpath(xpath=element, timeout=timeout, interval=interval) 269 | elif element_type == 'classname': 270 | elements = driver.wait_for_elements_by_classname(classname=element, timeout=timeout, interval=interval) 271 | else: 272 | elements = None 273 | 274 | if elements: 275 | log_info(' <-- result:') 276 | for e in elements: 277 | log_info(' - {}'.format(e)) 278 | if len(elements) <= int(index): 279 | log_error('elements exists, but cannot find index({}) position'.format(index), False) 280 | raise Exception('list index out of range, index:{}'.format(index)) 281 | if not_processing: 282 | return elements 283 | return elements[index] 284 | return None 285 | -------------------------------------------------------------------------------- /fasttest/drivers/driver_base_web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import os 5 | import pdb 6 | import time 7 | import datetime 8 | from concurrent import futures 9 | from fasttest.utils import * 10 | from fasttest.common import * 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.common.keys import Keys 13 | from selenium.webdriver.support.wait import WebDriverWait 14 | from selenium.webdriver.support import expected_conditions as EC 15 | from selenium.webdriver.common.action_chains import ActionChains 16 | from selenium.common.exceptions import NoSuchElementException, NoSuchWindowException, InvalidSessionIdException, TimeoutException 17 | 18 | class DriverBaseWeb(object): 19 | 20 | 21 | @staticmethod 22 | def init(): 23 | try: 24 | global by 25 | by = Dict({ 26 | 'id': By.ID, 27 | 'name': By.NAME, 28 | 'xpath': By.XPATH, 29 | 'class': By.CLASS_NAME, 30 | 'tag_name': By.TAG_NAME, 31 | 'link_text': By.LINK_TEXT, 32 | 'css_selector': By.CSS_SELECTOR, 33 | 'partial_link_text': By.PARTIAL_LINK_TEXT, 34 | }) 35 | except Exception as e: 36 | raise e 37 | 38 | @staticmethod 39 | def open_url(url): 40 | ''' 41 | open url 42 | :param url: 43 | :return: 44 | ''' 45 | try: 46 | Var.instance.get(url) 47 | except NoSuchWindowException: 48 | handles = DriverBaseWeb.get_window_handles() 49 | # 如果浏览器未关闭打开新窗口触发该异常,提示用户切换窗口 50 | if handles: 51 | raise NoSuchWindowException('no such window, execute the switchToWindow method to switch the window') 52 | DriverBaseWeb.createSession() 53 | Var.instance.get(url) 54 | except InvalidSessionIdException: 55 | DriverBaseWeb.createSession() 56 | Var.instance.get(url) 57 | 58 | @staticmethod 59 | def close(): 60 | ''' 61 | close 62 | :param: 63 | :return: 64 | ''' 65 | Var.instance.close() 66 | 67 | @staticmethod 68 | def createSession(): 69 | server_web = ServerUtilsWeb(Var.desired_capabilities) 70 | Var.instance = server_web.start_server() 71 | DriverBaseWeb.init() 72 | 73 | @staticmethod 74 | def quit(): 75 | ''' 76 | quit 77 | :param: 78 | :return: 79 | ''' 80 | Var.instance.quit() 81 | 82 | @staticmethod 83 | def back(): 84 | ''' 85 | back 86 | :param 87 | :return: 88 | ''' 89 | Var.instance.back() 90 | 91 | @staticmethod 92 | def forward(): 93 | ''' 94 | forward 95 | :param 96 | :return: 97 | ''' 98 | Var.instance.forward() 99 | 100 | @staticmethod 101 | def refresh(): 102 | ''' 103 | refresh 104 | :param 105 | :return: 106 | ''' 107 | Var.instance.refresh() 108 | 109 | @staticmethod 110 | def maximize_window(): 111 | ''' 112 | maxWindow 113 | :param: 114 | :return: 115 | ''' 116 | Var.instance.maximize_window() 117 | 118 | @staticmethod 119 | def minimize_window(): 120 | ''' 121 | minWindow 122 | :param: 123 | :return: 124 | ''' 125 | Var.instance.minimize_window() 126 | 127 | @staticmethod 128 | def fullscreen_window(): 129 | ''' 130 | fullscreenWindow 131 | :param: 132 | :return: 133 | ''' 134 | Var.instance.fullscreen_window() 135 | 136 | @staticmethod 137 | def delete_all_cookies(): 138 | ''' 139 | deleteAllCookies 140 | :param: 141 | :return: 142 | ''' 143 | Var.instance.delete_all_cookies() 144 | 145 | @staticmethod 146 | def delete_cookie(name): 147 | ''' 148 | deleteCookie 149 | :param name 150 | :return: 151 | ''' 152 | Var.instance.delete_cookie(name) 153 | 154 | @staticmethod 155 | def add_cookie(cookie_dict): 156 | ''' 157 | addCookie 158 | :param cookie_dict 159 | :return: 160 | ''' 161 | Var.instance.add_cookie(cookie_dict) 162 | 163 | @staticmethod 164 | def submit(element): 165 | ''' 166 | submit 167 | :param: element 168 | :return: 169 | ''' 170 | element.submit() 171 | 172 | @staticmethod 173 | def clear(element): 174 | ''' 175 | element 176 | :param: 177 | :return: 178 | ''' 179 | element.clear() 180 | 181 | @staticmethod 182 | def click(element): 183 | ''' 184 | click 185 | :param: element 186 | :return: 187 | ''' 188 | element.click() 189 | 190 | 191 | @staticmethod 192 | def context_click(element): 193 | ''' 194 | contextClick 195 | :param: element 196 | :return: 197 | ''' 198 | ActionChains(Var.instance).context_click(element).perform() 199 | 200 | @staticmethod 201 | def double_click(element): 202 | ''' 203 | doubleClick 204 | :param: element 205 | :return: 206 | ''' 207 | ActionChains(Var.instance).double_click(element).perform() 208 | 209 | @staticmethod 210 | def click_and_hold(element): 211 | ''' 212 | holdClick 213 | :param: element 214 | :return: 215 | ''' 216 | ActionChains(Var.instance).click_and_hold(element).perform() 217 | 218 | @staticmethod 219 | def drag_and_drop(element, target): 220 | ''' 221 | dragDrop 222 | :param element:鼠标按下的源元素 223 | :param target:鼠标释放的目标元素 224 | :return: 225 | ''' 226 | ActionChains(Var.instance).drag_and_drop(element, target).perform() 227 | 228 | @staticmethod 229 | def drag_and_drop_by_offse(element, xoffset, yoffset): 230 | ''' 231 | dragDropByOffset 232 | :param element: 233 | :param xoffset: 234 | :param yoffset: 235 | :return: 236 | ''' 237 | ActionChains(Var.instance).drag_and_drop_by_offset(element, xoffset, yoffset).perform() 238 | 239 | @staticmethod 240 | def move_by_offset(xoffset, yoffset): 241 | ''' 242 | moveByOffset 243 | :param xoffset: 244 | :param yoffset: 245 | :return: 246 | ''' 247 | ActionChains(Var.instance).move_by_offset(xoffset, yoffset).perform() 248 | 249 | @staticmethod 250 | def move_to_element(element): 251 | ''' 252 | moveToElement 253 | :param element 254 | :return: 255 | ''' 256 | ActionChains(Var.instance).move_to_element(element).perform() 257 | 258 | @staticmethod 259 | def move_to_element_with_offset(element, xoffset, yoffset): 260 | ''' 261 | moveToElementWithOffset 262 | :param element 263 | :param xoffset: 264 | :param yoffset: 265 | :return: 266 | ''' 267 | ActionChains(Var.instance).move_to_element_with_offset(element, xoffset, yoffset).perform() 268 | 269 | @staticmethod 270 | def key_down_and_key_up(value): 271 | ''' 272 | keyDownAndkeyUp 273 | :param element 274 | :param value: 275 | :return: 276 | ''' 277 | try: 278 | action = 'ActionChains(Var.instance)' 279 | for k, v in value.items(): 280 | if k.lower() == 'keydown': 281 | for k_down in v: 282 | action = '{}.key_down({})'.format(action, k_down) 283 | elif k.lower() == 'sendkeys': 284 | action = '{}.send_keys("{}")'.format(action, v) 285 | elif k.lower() == 'keyup': 286 | for k_up in v: 287 | action = '{}.key_up({})'.format(action, k_up) 288 | action = '{}.perform()'.format(action) 289 | log_info(action) 290 | eval(action) 291 | except Exception as e: 292 | raise e 293 | 294 | @staticmethod 295 | def key_up(element, value): 296 | ''' 297 | keyUp 298 | :param element 299 | :param value: 300 | :return: 301 | ''' 302 | ActionChains(Var.instance).key_up(value, element).perform() 303 | 304 | @staticmethod 305 | def switch_to_frame(frame_reference): 306 | ''' 307 | switchToFrame 308 | :param frame_reference: 309 | :return: 310 | ''' 311 | Var.instance.switch_to.frame(frame_reference) 312 | 313 | @staticmethod 314 | def switch_to_default_content(): 315 | ''' 316 | switchToDefaultContent 317 | :return: 318 | ''' 319 | Var.instance.switch_to.default_content() 320 | 321 | @staticmethod 322 | def switch_to_parent_frame(): 323 | ''' 324 | switchToParentFrame 325 | :return: 326 | ''' 327 | Var.instance.switch_to.parent_frame() 328 | 329 | @staticmethod 330 | def switch_to_window(handle): 331 | ''' 332 | switchToWindow 333 | :return: 334 | ''' 335 | Var.instance.switch_to.window(handle) 336 | 337 | @staticmethod 338 | def execute_script(js): 339 | ''' 340 | executeScript 341 | :return: 342 | ''' 343 | return Var.instance.execute_script(js) 344 | 345 | @staticmethod 346 | def send_keys(element, text): 347 | ''' 348 | sendKeys 349 | :param element: 350 | :param text: 351 | :return: 352 | ''' 353 | try: 354 | str_list = [] 355 | for t_str in text: 356 | if t_str is None: 357 | raise TypeError("the parms can'not be none") 358 | if re.match(r'Keys\.\w+', t_str): 359 | try: 360 | t_str = eval(t_str) 361 | except: 362 | t_str = t_str 363 | str_list.append(t_str) 364 | if len(str_list) == 1: 365 | element.send_keys(str_list[0]) 366 | elif len(str_list) == 2: 367 | element.send_keys(str_list[0], str_list[1]) 368 | except Exception as e: 369 | raise e 370 | 371 | @staticmethod 372 | def is_selected(element): 373 | ''' 374 | isSelected 375 | :param element: 376 | :return: 377 | ''' 378 | return element.is_selected() 379 | 380 | @staticmethod 381 | def is_displayed(element): 382 | ''' 383 | isDisplayed 384 | :param element: 385 | :return: 386 | ''' 387 | return element.is_displayed() 388 | 389 | @staticmethod 390 | def is_enabled(element): 391 | ''' 392 | isEnabled 393 | :param element: 394 | :return: 395 | ''' 396 | return element.is_enabled() 397 | 398 | @staticmethod 399 | def get_size(element): 400 | ''' 401 | getSize 402 | :param element: 403 | :return: 404 | ''' 405 | return element.size 406 | 407 | @staticmethod 408 | def get_attribute(element, attribute): 409 | ''' 410 | getAttribute 411 | :param element 412 | :param attribute 413 | :return: 414 | ''' 415 | return element.get_attribute(attribute) 416 | 417 | @staticmethod 418 | def get_text(element): 419 | ''' 420 | getText 421 | :param element: 422 | :return: 423 | ''' 424 | return element.text 425 | 426 | @staticmethod 427 | def get_tag_name(element): 428 | ''' 429 | getTagName 430 | :param element: 431 | :return: 432 | ''' 433 | return element.tag_name 434 | 435 | @staticmethod 436 | def get_css_property(element, css): 437 | ''' 438 | getCssProperty 439 | :param element: 440 | :return: 441 | ''' 442 | return element.value_of_css_property(css) 443 | 444 | @staticmethod 445 | def get_location(element): 446 | ''' 447 | getLocation 448 | :param element: 449 | :return: 450 | ''' 451 | return element.location 452 | 453 | @staticmethod 454 | def get_rect(element): 455 | ''' 456 | getRect 457 | ''' 458 | return element.rect 459 | 460 | @staticmethod 461 | def get_name(): 462 | ''' 463 | getName 464 | :return: 465 | ''' 466 | return Var.instance.name 467 | 468 | @staticmethod 469 | def get_title(): 470 | ''' 471 | getTitle 472 | :return: 473 | ''' 474 | return Var.instance.title 475 | 476 | @staticmethod 477 | def get_current_url(): 478 | ''' 479 | getCurrentUrl 480 | :return: 481 | ''' 482 | return Var.instance.current_url 483 | 484 | @staticmethod 485 | def get_current_window_handle(): 486 | ''' 487 | getCurrentWindowHandle 488 | :return: 489 | ''' 490 | return Var.instance.current_window_handle 491 | 492 | @staticmethod 493 | def get_window_handles(): 494 | ''' 495 | getWindowHandles 496 | :return: 497 | ''' 498 | return Var.instance.window_handles 499 | 500 | @staticmethod 501 | def get_cookies(): 502 | ''' 503 | getCookies 504 | :return: 505 | ''' 506 | return Var.instance.get_cookies() 507 | 508 | @staticmethod 509 | def get_cookie(name): 510 | ''' 511 | getCookie 512 | :param name 513 | :return: 514 | ''' 515 | return Var.instance.get_cookie(name) 516 | 517 | @staticmethod 518 | def get_window_position(): 519 | ''' 520 | getWindowPosition 521 | :return: 522 | ''' 523 | return Var.instance.get_window_position() 524 | 525 | @staticmethod 526 | def set_window_position(x, y): 527 | ''' 528 | setWindowPosition 529 | :return: 530 | ''' 531 | return Var.instance.set_window_position(x, y) 532 | 533 | @staticmethod 534 | def get_window_size(): 535 | ''' 536 | getWindowSize 537 | :return: 538 | ''' 539 | return Var.instance.get_window_size() 540 | 541 | @staticmethod 542 | def set_window_size(width, height): 543 | ''' 544 | setWindowSize 545 | :return: 546 | ''' 547 | return Var.instance.set_window_size(width, height) 548 | 549 | @staticmethod 550 | def save_screenshot(element, name): 551 | ''' 552 | saveScreenshot 553 | :return: 554 | ''' 555 | try: 556 | image_dir = os.path.join(Var.snapshot_dir, 'screenshot') 557 | if not os.path.exists(image_dir): 558 | os.makedirs(image_dir) 559 | image_path = os.path.join(image_dir, '{}'.format(name)) 560 | if element: 561 | element.screenshot(image_path) 562 | else: 563 | Var.instance.save_screenshot(image_path) 564 | except Exception as e: 565 | raise e 566 | return image_path 567 | 568 | @staticmethod 569 | def query_displayed(type='', text='',element='' , timeout=10): 570 | ''' 571 | queryDisplayed 572 | :param type: 573 | :param text: 574 | :return: 575 | ''' 576 | if element: 577 | try: 578 | WebDriverWait(Var.instance, int(timeout)).until( 579 | EC.visibility_of(element) 580 | ) 581 | except Exception as e: 582 | raise e 583 | else: 584 | try: 585 | type = type.lower() 586 | WebDriverWait(Var.instance, int(timeout)).until( 587 | EC.visibility_of_element_located((by[type], text)) 588 | ) 589 | except Exception as e: 590 | raise e 591 | 592 | @staticmethod 593 | def query_not_displayed(type='', text='', element='', timeout=10): 594 | ''' 595 | queryNotDisplayed 596 | :param type: 597 | :param text: 598 | :return: 599 | ''' 600 | if element: 601 | try: 602 | WebDriverWait(Var.instance, int(timeout)).until( 603 | EC.invisibility_of_element(element) 604 | ) 605 | except Exception as e: 606 | raise e 607 | else: 608 | try: 609 | type = type.lower() 610 | WebDriverWait(Var.instance, int(timeout)).until( 611 | EC.invisibility_of_element_located((by[type], text)) 612 | ) 613 | except Exception as e: 614 | raise e 615 | 616 | @staticmethod 617 | def get_element(type, text, timeout=10): 618 | ''' 619 | getElement 620 | :param type: 621 | :param text: 622 | :return: 623 | ''' 624 | type = type.lower() 625 | endTime = datetime.datetime.now() + datetime.timedelta(seconds=int(timeout)) 626 | index = 3 627 | while True: 628 | try: 629 | element = Var.instance.find_element(by[type], text) 630 | if element.is_enabled(): 631 | return element 632 | elif element.is_displayed(): 633 | index -= 1 634 | if index < 0: 635 | return element 636 | if datetime.datetime.now() >= endTime: 637 | return element 638 | except NoSuchElementException: 639 | if datetime.datetime.now() >= endTime: 640 | return None 641 | except Exception as e: 642 | raise e 643 | 644 | @staticmethod 645 | def get_elements(type, text, timeout=10): 646 | ''' 647 | getElements 648 | :param type: 649 | :param text: 650 | :return: 651 | ''' 652 | type = type.lower() 653 | try: 654 | element = DriverBaseWeb.get_element(type, text, timeout) 655 | if not element: 656 | return [] 657 | elements = Var.instance.find_elements(by[type], text) 658 | return elements 659 | except NoSuchElementException: 660 | return [] 661 | except Exception as e: 662 | raise e -------------------------------------------------------------------------------- /fasttest/drivers/macaca/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.drivers.macaca.driver_macaca import AndroidDriver, iOSDriver 4 | 5 | 6 | __all__ = ['AndroidDriver','iOSDriver'] 7 | -------------------------------------------------------------------------------- /fasttest/drivers/macaca/driver_macaca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import traceback 5 | import subprocess 6 | from fasttest.common import * 7 | 8 | 9 | class AndroidDriver(object): 10 | 11 | @staticmethod 12 | def adb_shell(cmd): 13 | ''' 14 | :param cmd: 15 | :return: 16 | ''' 17 | try: 18 | log_info(' adb {}'.format(cmd)) 19 | if cmd.startswith('shell'): 20 | cmd = ["adb", "-s", Var.desired_caps.udid, "shell", "{}".format(cmd.lstrip('shell').strip())] 21 | pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, 22 | stdout=subprocess.PIPE) 23 | out = pipe.communicate() 24 | else: 25 | cmd = ["adb", "-s", Var.desired_caps.udid, "{}".format(cmd)] 26 | os.system(' '.join(cmd)) 27 | except: 28 | raise Exception(traceback.format_exc()) 29 | 30 | @staticmethod 31 | def install_app(app_path): 32 | ''' 33 | install app 34 | :param app_path: 35 | :return: 36 | ''' 37 | try: 38 | AndroidDriver.adb_shell('install -r {}'.format(app_path)) 39 | except Exception as e: 40 | raise e 41 | 42 | @staticmethod 43 | def uninstall_app(package_info): 44 | ''' 45 | uninstall app 46 | :param package_info: Android(package) or iOS(bundleId) 47 | :return: 48 | ''' 49 | try: 50 | AndroidDriver.adb_shell('uninstall {}'.format(package_info)) 51 | except Exception as e: 52 | raise e 53 | 54 | @staticmethod 55 | def launch_app(package_info): 56 | ''' 57 | launch app 58 | :param package_info: Android(package/activity) or iOS(bundleId) 59 | :return: 60 | ''' 61 | try: 62 | if not package_info: 63 | AndroidDriver.adb_shell('shell am start -W {}/{}'.format(Var.desired_caps.package, Var.desired_caps.activity)) 64 | else: 65 | AndroidDriver.adb_shell('shell am start -W {}'.format(package_info)) 66 | 67 | except Exception as e: 68 | raise e 69 | 70 | @staticmethod 71 | def close_app(package_info): 72 | ''' 73 | close app 74 | :param package_info: Android(package) or iOS(bundleId) 75 | :return: 76 | ''' 77 | try: 78 | if not package_info: 79 | AndroidDriver.adb_shell('shell am force-stop {}'.format(Var.desired_caps.package)) 80 | else: 81 | AndroidDriver.adb_shell('shell am force-stop {}'.format(package_info)) 82 | except Exception as e: 83 | raise e 84 | 85 | @staticmethod 86 | def tap(x, y): 87 | ''' 88 | :param x: 89 | :param y: 90 | :return: 91 | ''' 92 | try: 93 | width = Var.instance.get_window_size()['width'] 94 | height = Var.instance.get_window_size()['height'] 95 | if x <= 1.0: 96 | x = x * width 97 | if y <= 1.0: 98 | y = y * height 99 | Var.instance.touch('tap', {'x': x, 'y': y}) 100 | except Exception as e: 101 | raise e 102 | 103 | @staticmethod 104 | def double_tap(x, y): 105 | ''' 106 | :param x: 107 | :param y: 108 | :return: 109 | ''' 110 | try: 111 | width = Var.instance.get_window_size()['width'] 112 | height = Var.instance.get_window_size()['height'] 113 | if x <= 1.0: 114 | x = x * width 115 | if y <= 1.0: 116 | y = y * height 117 | Var.instance.touch('doubleTap', {'x': x, 'y': y}) 118 | except Exception as e: 119 | raise e 120 | 121 | @staticmethod 122 | def press(x, y, duration=2): 123 | ''' 124 | :param x: 125 | :param y: 126 | :param duration: 127 | :return: 128 | ''' 129 | try: 130 | width = Var.instance.get_window_size()['width'] 131 | height = Var.instance.get_window_size()['height'] 132 | if x <= 1.0: 133 | x = x * width 134 | if y <= 1.0: 135 | y = y * height 136 | Var.instance.touch('press', {'x': x, 'y': y, 'duration': duration}) 137 | except Exception as e: 138 | raise e 139 | 140 | @staticmethod 141 | def press(element, duration=2): 142 | ''' 143 | :param element: 144 | :param duration: 145 | :return: 146 | ''' 147 | try: 148 | element.touch('press', {'duration': duration}) 149 | except Exception as e: 150 | raise e 151 | 152 | @staticmethod 153 | def swipe_up(duration=2): 154 | ''' 155 | :param duration: 156 | :return: 157 | ''' 158 | try: 159 | width = Var.instance.get_window_size()['width'] 160 | height = Var.instance.get_window_size()['height'] 161 | AndroidDriver.swipe(width / 2, height * 0.65, width / 2, height / 4, duration) 162 | except Exception as e: 163 | raise e 164 | 165 | @staticmethod 166 | def swipe_down(duration=2): 167 | ''' 168 | :param duration: 169 | :return: 170 | ''' 171 | try: 172 | width = Var.instance.get_window_size()['width'] 173 | height = Var.instance.get_window_size()['height'] 174 | AndroidDriver.swipe(width / 2, height / 4, width / 2, height * 3 / 4, duration) 175 | except Exception as e: 176 | raise e 177 | 178 | @staticmethod 179 | def swipe_left(duration=2): 180 | ''' 181 | :param duration: 182 | :return: 183 | ''' 184 | try: 185 | width = Var.instance.get_window_size()['width'] 186 | height = Var.instance.get_window_size()['height'] 187 | AndroidDriver.swipe(width * 3 / 4, height / 2, width / 4, height / 2, duration) 188 | except Exception as e: 189 | raise e 190 | 191 | @staticmethod 192 | def swipe_right(duration=2): 193 | ''' 194 | :param duration: 195 | :return: 196 | ''' 197 | try: 198 | width = Var.instance.get_window_size()['width'] 199 | height = Var.instance.get_window_size()['height'] 200 | AndroidDriver.swipe(width / 4, height / 2, width * 3 / 4, height / 2, duration) 201 | except Exception as e: 202 | raise e 203 | 204 | @staticmethod 205 | def swipe(from_x, from_y, to_x, to_y, duration=2): 206 | ''' 207 | :param from_x: 208 | :param from_y: 209 | :param to_x: 210 | :param to_y: 211 | :param duration: 212 | :return: 213 | ''' 214 | try: 215 | width = Var.instance.get_window_size()['width'] 216 | height = Var.instance.get_window_size()['height'] 217 | if from_x <= 1.0: 218 | from_x = from_x * width 219 | if from_y <= 1.0: 220 | from_y = from_y * height 221 | if to_x <= 1.0: 222 | to_x = to_x * width 223 | if to_y <= 1.0: 224 | to_y = to_y * height 225 | AndroidDriver.adb_shell('shell input swipe {} {} {} {} {}'.format(from_x, from_y, to_x, to_y, duration * 100)) 226 | except Exception as e: 227 | raise e 228 | 229 | @staticmethod 230 | def input(element, text, clear=True, hide_keyboard=True): 231 | ''' 232 | :param element: 233 | :param text: 234 | :param clear: 235 | :param hide_keyboard: 236 | :return: 237 | ''' 238 | try: 239 | # if clear: 240 | # AndroidDriver.clear() 241 | # if hide_keyboard: 242 | # AndroidDriver.hide_keyboard() 243 | # element.click() 244 | element.send_keys(text) 245 | except Exception as e: 246 | raise e 247 | 248 | @staticmethod 249 | def get_text(element): 250 | ''' 251 | :param element: 252 | :return: 253 | ''' 254 | try: 255 | text = element.text 256 | return text 257 | except Exception as e: 258 | raise e 259 | 260 | @staticmethod 261 | def clear(): 262 | ''' 263 | :return: 264 | ''' 265 | try: 266 | Var.instance.clear() 267 | except: 268 | traceback.print_exc() 269 | 270 | @staticmethod 271 | def hide_keyboard(): 272 | ''' 273 | :return: 274 | ''' 275 | try: 276 | AndroidDriver.adb_shell('shell input keyevent 111') 277 | except: 278 | traceback.print_exc() 279 | 280 | @staticmethod 281 | def wait_for_elements_by_id(id, timeout=10, interval=1): 282 | ''' 283 | :param id: 284 | :return: 285 | ''' 286 | try: 287 | elements = Var.instance.wait_for_elements_by_id(id,int(timeout)*1000,int(interval)*1000) 288 | return elements 289 | except: 290 | return None 291 | 292 | @staticmethod 293 | def wait_for_elements_by_name(name, timeout=10, interval=1): 294 | ''' 295 | :param name: 296 | :return:me 297 | ''' 298 | try: 299 | elements = Var.instance.wait_for_elements_by_name(name,int(timeout)*1000,int(interval)*1000) 300 | return elements 301 | except: 302 | return None 303 | 304 | @staticmethod 305 | def wait_for_elements_by_xpath(xpath, timeout=10, interval=1): 306 | ''' 307 | :param xpath: 308 | :return: 309 | ''' 310 | try: 311 | elements = Var.instance.wait_for_elements_by_xpath(xpath,int(timeout)*1000,int(interval)*1000) 312 | return elements 313 | except: 314 | return None 315 | 316 | @staticmethod 317 | def wait_for_elements_by_classname(classname, timeout=10, interval=1): 318 | ''' 319 | :param classname: 320 | :return: 321 | ''' 322 | try: 323 | elements = Var.instance.wait_for_elements_by_class_name(classname,int(timeout)*1000,int(interval)*1000) 324 | return elements 325 | except: 326 | return None 327 | 328 | class iOSDriver(object): 329 | 330 | 331 | @staticmethod 332 | def install_app(app_path): 333 | ''' 334 | install app 335 | :param app_path: 336 | :return: 337 | ''' 338 | try: 339 | os.system('ideviceinstaller -u {} -i {}'.format(Var.desired_caps.udid, app_path)) 340 | except Exception as e: 341 | raise e 342 | 343 | @staticmethod 344 | def uninstall_app(package_info): 345 | ''' 346 | uninstall app 347 | :param package_info: Android(package) or iOS(bundleId) 348 | :return: 349 | ''' 350 | try: 351 | os.system('ideviceinstaller -u {} -U {}'.format(Var.desired_caps.udid, package_info)) 352 | except Exception as e: 353 | raise e 354 | 355 | @staticmethod 356 | def launch_app(package_info): 357 | ''' 358 | launch app 359 | :param package_info: Android(package/activity) or iOS(bundleId) 360 | :return: 361 | ''' 362 | try: 363 | pass # todo 待实现 364 | except Exception as e: 365 | raise e 366 | 367 | @staticmethod 368 | def close_app(package_info): 369 | ''' 370 | close app 371 | :param package_info: Android(package) or iOS(bundleId) 372 | :return: 373 | ''' 374 | try: 375 | pass # todo 待实现 376 | except Exception as e: 377 | raise e 378 | 379 | @staticmethod 380 | def tap(x, y): 381 | ''' 382 | :param x: 383 | :param y: 384 | :return: 385 | ''' 386 | try: 387 | width = Var.instance.get_window_size()['width'] 388 | height = Var.instance.get_window_size()['height'] 389 | if x <= 1.0: 390 | x = x * width 391 | if y <= 1.0: 392 | y = y * height 393 | Var.instance.touch('tap', {'x': x, 'y': y}) 394 | except Exception as e: 395 | raise e 396 | 397 | @staticmethod 398 | def double_tap(x, y): 399 | ''' 400 | :param x: 401 | :param y: 402 | :return: 403 | ''' 404 | try: 405 | width = Var.instance.get_window_size()['width'] 406 | height = Var.instance.get_window_size()['height'] 407 | if x <= 1.0: 408 | x = x * width 409 | if y <= 1.0: 410 | y = y * height 411 | Var.instance.touch('doubleTap', {'x': x, 'y': y}) 412 | except Exception as e: 413 | raise e 414 | 415 | @staticmethod 416 | def press(x, y, duration=2): 417 | ''' 418 | :param x: 419 | :param y: 420 | :param duration: 421 | :return: 422 | ''' 423 | try: 424 | width = Var.instance.get_window_size()['width'] 425 | height = Var.instance.get_window_size()['height'] 426 | if x <= 1.0: 427 | x = x * width 428 | if y <= 1.0: 429 | y = y * height 430 | Var.instance.touch('press', {'x': x, 'y': y, 'duration': duration}) 431 | except Exception as e: 432 | raise e 433 | 434 | @staticmethod 435 | def press(element, duration=2): 436 | ''' 437 | :param element: 438 | :param duration: 439 | :return: 440 | ''' 441 | try: 442 | element.touch('press', {'duration': duration}) 443 | except Exception as e: 444 | raise e 445 | 446 | @staticmethod 447 | def swipe_up(duration=0): 448 | ''' 449 | :param duration: 450 | :return: 451 | ''' 452 | try: 453 | width = Var.instance.get_window_size()['width'] 454 | height = Var.instance.get_window_size()['height'] 455 | iOSDriver.swipe(width / 2, height * 0.65, width / 2, height / 4, duration) 456 | except Exception as e: 457 | raise e 458 | 459 | @staticmethod 460 | def swipe_down(duration=2): 461 | ''' 462 | :param duration: 463 | :return: 464 | ''' 465 | try: 466 | width = Var.instance.get_window_size()['width'] 467 | height = Var.instance.get_window_size()['height'] 468 | iOSDriver.swipe(width / 2, height / 4, width / 2, height * 3 / 4, duration) 469 | except Exception as e: 470 | raise e 471 | 472 | @staticmethod 473 | def swipe_left(duration=2): 474 | ''' 475 | :param duration: 476 | :return: 477 | ''' 478 | try: 479 | width = Var.instance.get_window_size()['width'] 480 | height = Var.instance.get_window_size()['height'] 481 | iOSDriver.swipe(width * 3 / 4, height / 2, width / 4, height / 2, duration) 482 | except Exception as e: 483 | raise e 484 | 485 | @staticmethod 486 | def swipe_right(duration=2): 487 | ''' 488 | :param duration: 489 | :return: 490 | ''' 491 | try: 492 | width = Var.instance.get_window_size()['width'] 493 | height = Var.instance.get_window_size()['height'] 494 | iOSDriver.swipe(width / 4, height / 2, width * 3 / 4, height / 2, duration) 495 | except Exception as e: 496 | raise e 497 | 498 | @staticmethod 499 | def swipe(from_x, from_y, to_x, to_y, duration=2): 500 | ''' 501 | :param from_x: 502 | :param from_y: 503 | :param to_x: 504 | :param to_y: 505 | :param duration: 506 | :return: 507 | ''' 508 | try: 509 | width = Var.instance.get_window_size()['width'] 510 | height = Var.instance.get_window_size()['height'] 511 | if from_x <= 1.0: 512 | from_x = from_x * width 513 | if from_y <= 1.0: 514 | from_y = from_y * height 515 | if to_x <= 1.0: 516 | to_x = to_x * width 517 | if to_y <= 1.0: 518 | to_y = to_y * height 519 | Var.instance.touch('drag', { 'fromX': from_x, 'fromY': from_y, 'toX': to_x, 'toY': to_y, 'duration': duration}) 520 | except Exception as e: 521 | raise e 522 | 523 | @staticmethod 524 | def input(element, text, clear=True, hide_keyboard=True): 525 | ''' 526 | :param element: 527 | :param text: 528 | :param clear: 529 | :param hide_keyboard: 530 | :return: 531 | ''' 532 | try: 533 | # if clear: 534 | # iOSDriver.clear() 535 | # if hide_keyboard: 536 | # iOSDriver.hide_keyboard() 537 | # element.click() 538 | element.send_keys(text) 539 | except Exception as e: 540 | raise e 541 | 542 | @staticmethod 543 | def get_text(element): 544 | ''' 545 | :param element: 546 | :return: 547 | ''' 548 | try: 549 | text = element.text 550 | return text 551 | except Exception as e: 552 | raise e 553 | 554 | @staticmethod 555 | def clear(): 556 | ''' 557 | :return: 558 | ''' 559 | try: 560 | Var.instance.clear() 561 | except: 562 | traceback.print_exc() 563 | 564 | @staticmethod 565 | def hide_keyboard(): 566 | ''' 567 | :return: 568 | ''' 569 | try: 570 | pass # todo 待实现 571 | except: 572 | traceback.print_exc() 573 | 574 | @staticmethod 575 | def wait_for_elements_by_id(id, timeout=10, interval=1): 576 | ''' 577 | :param id: 578 | :return: 579 | ''' 580 | try: 581 | elements = Var.instance.wait_for_elements_by_id(id,int(timeout)*1000,int(interval)*1000) 582 | return elements 583 | except: 584 | return None 585 | 586 | @staticmethod 587 | def wait_for_elements_by_name(name, timeout=10, interval=1): 588 | ''' 589 | :param name: 590 | :return:me 591 | ''' 592 | try: 593 | elements = Var.instance.wait_for_elements_by_name(name,int(timeout)*1000,int(interval)*1000) 594 | return elements 595 | except: 596 | return None 597 | 598 | @staticmethod 599 | def wait_for_elements_by_xpath(xpath, timeout=10, interval=1): 600 | ''' 601 | :param xpath: 602 | :return: 603 | ''' 604 | try: 605 | elements = Var.instance.wait_for_elements_by_xpath(xpath,int(timeout)*1000,int(interval)*1000) 606 | return elements 607 | except: 608 | return None 609 | 610 | @staticmethod 611 | def wait_for_elements_by_classname(classname, timeout=10, interval=1): 612 | ''' 613 | :param classname: 614 | :return: 615 | ''' 616 | try: 617 | elements = Var.instance.wait_for_elements_by_class_name(classname,int(timeout)*1000,int(interval)*1000) 618 | return elements 619 | except: 620 | return None -------------------------------------------------------------------------------- /fasttest/fasttest_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | import getopt 6 | import traceback 7 | from concurrent import futures 8 | from fasttest.version import VERSION 9 | from fasttest.project import * 10 | 11 | def _usage(): 12 | print('') 13 | print(' usage: fasttest [-h|-v|] [arg] ...') 14 | print('') 15 | print(' options:') 16 | print('') 17 | print(' -h, --help show help screen and exit.') 18 | print(' -V, --Version show version.') 19 | print(' -i, --init specify a project name and create the project.') 20 | print(' -r, --run specify the project path and run the project.') 21 | print(' -w, --workers specify number of threads.') 22 | print('') 23 | sys.exit() 24 | 25 | def _init_project(dir): 26 | 27 | try: 28 | if not dir: 29 | print('Please enter a project name...') 30 | sys.exit() 31 | 32 | dirs = ['Common/Android', 'Common/iOS', 'Common/Selenium', 'Resource', 'Scripts', 'TestCase'] 33 | for dir_ in dirs: 34 | path = os.path.join(dir, dir_) 35 | print('create directory: {}'.format(path)) 36 | os.makedirs(path) 37 | 38 | config_path = os.path.join(dir, 'config.yaml') 39 | with open(config_path, "w") as f: 40 | print('create file: {}'.format(config_path)) 41 | config = "driver: 'appium'\n" \ 42 | "reStart: True\n" \ 43 | "saveScreenshot: False\n" \ 44 | "timeOut: 10\n" \ 45 | "desiredCapabilities:\n" \ 46 | " platformName: 'Android'\n" \ 47 | " udid: 'device_id'\n" \ 48 | " appPackage: 'com.android.mobile'\n" \ 49 | " appActivity: 'com.android.mobile.Launcher'\n" \ 50 | " automationName: 'Appium'\n" \ 51 | " deviceName: 'HUWWEI P40 Pro'\n" \ 52 | " noReset: True\n" \ 53 | "testcase:\n" \ 54 | " - TestCase/case.yaml" 55 | f.write(config) 56 | 57 | data_path = os.path.join(dir, 'data.yaml') 58 | with open(data_path, 'w') as f: 59 | print('create file: {}'.format(data_path)) 60 | config = "variable:\n" \ 61 | " userid: 'admin'\n" \ 62 | " password: '13456'\n" \ 63 | "resource:\n" \ 64 | " logo: 'Resource/logo.png'\n" \ 65 | "keywords:\n" \ 66 | " - 'ScriptsTest'\n" 67 | f.write(config) 68 | 69 | common_path = os.path.join(dir, 'Common', 'common.yaml') 70 | with open(common_path, "w") as f: 71 | print('create file: {}'.format(common_path)) 72 | common = "CommonTest:\n" \ 73 | " description: 'common test'\n" \ 74 | " input: [value]\n" \ 75 | " output: []\n" \ 76 | " steps:\n" \ 77 | " - for ${i} in ${value}:\n" \ 78 | " - if ${i} == 3:\n" \ 79 | " - break" 80 | f.write(common) 81 | 82 | case_path = os.path.join(dir, 'TestCase', 'case.yaml') 83 | with open(case_path, "w") as f: 84 | print('create file: {}'.format(case_path)) 85 | case = "module: test_module\n" \ 86 | "skip: False\n" \ 87 | "description: 'this is a test case'\n" \ 88 | "steps:\n" \ 89 | " - ${t1} = $.id(1+2*3)\n\n" \ 90 | " - ${t2} = 6\n\n" \ 91 | " - assert ${t1} > ${t2}\n\n" \ 92 | " - ${ls} = ScriptsTest(${t2})\n\n" \ 93 | " - call CommonTest(${ls})" 94 | f.write(case) 95 | 96 | scripts_path = os.path.join(dir, 'Scripts', 'case.py') 97 | with open(scripts_path, "w") as f: 98 | print('create file: {}'.format(scripts_path)) 99 | scripts = '#!/usr/bin/env python3\n' \ 100 | '# -*- coding: utf-8 -*-\n\n' \ 101 | 'def ScriptsTest(value):\n\n' \ 102 | ' return [1,2,3,4,5,value]' 103 | f.write(scripts) 104 | print('create project successfully.') 105 | 106 | except Exception as e: 107 | raise e 108 | 109 | def _start_project(workers, path): 110 | 111 | if workers <= 1: 112 | project = Project(path=path) 113 | result = project.start() 114 | return result 115 | else: 116 | result_list = [] 117 | with futures.ThreadPoolExecutor() as t: 118 | worker_list = [] 119 | for index in range(workers): 120 | run_info = { 121 | 'index': index, 122 | 'workers': workers, 123 | 'path': path 124 | } 125 | worker_list.append(t.submit(_run_project, run_info)) 126 | 127 | for f in futures.as_completed(worker_list): 128 | if f.result() is not None: 129 | result = f.result() 130 | result_list.append(result) 131 | if f.exception(): 132 | print(f.exception()) 133 | return result_list 134 | 135 | def _run_project(run_info): 136 | 137 | try: 138 | index = run_info['index'] 139 | workers = run_info['workers'] 140 | path = run_info['path'] 141 | project = Project(index=index, workers=workers, path=path) 142 | result = project.start() 143 | return result 144 | except Exception as e: 145 | traceback.print_exc() 146 | return None 147 | 148 | def main(): 149 | ''' 150 | :return: 151 | ''' 152 | try: 153 | opts, args = getopt.getopt(sys.argv[1:], 'hVi:r:w:', ['help', 'Version', 'init=', 'run=', 'workers=']) 154 | except: 155 | _usage() 156 | project_path = '.' 157 | workers = 1 158 | for o, a in opts: 159 | if o in ('-h', '--help'): 160 | _usage() 161 | elif o in ('-V', '--Version'): 162 | print(VERSION) 163 | sys.exit() 164 | elif o in ('-i', '--init'): 165 | _init_project(a) 166 | sys.exit() 167 | elif o in ('-r', '--run'): 168 | project_path = a 169 | elif o in ('-w', '--workers'): 170 | workers = int(a) 171 | else: 172 | _usage() 173 | if not os.path.isdir(project_path): 174 | print('No such directory: {}'.format(project_path)) 175 | _usage() 176 | start_time = time.time() 177 | result = _start_project(workers, project_path) 178 | end_time = time.time() 179 | print('run time: {}s'.format(int(end_time-start_time))) 180 | if isinstance(result, list): 181 | for r in result: 182 | print('\n') 183 | for k, v in r.items(): 184 | if k == 'result': 185 | continue 186 | print('{}: {}'.format(k, v)) 187 | else: 188 | for k, v in result.items(): 189 | if k == 'result': 190 | continue 191 | print('{}: {}'.format(k, v)) 192 | 193 | if __name__ == '__main__': 194 | main() -------------------------------------------------------------------------------- /fasttest/keywords/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /fasttest/keywords/keywords.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | def return_keywords(driver): 5 | keywords_common = [ 6 | "click", # 点击 7 | "check", # 检查 8 | "sleep", # 等待 9 | "setVar", # 设置全局变量 10 | "break", 11 | "$.getText", # 获取文案 12 | "$.id", 13 | "$.getVar", # 获取全局变量 14 | "$.getElement", # 获取元素 15 | "$.getElements", # 获取元素 16 | "$.getLen", # 获取长度 17 | "$.isExist", # 是否存在 18 | "$.isNotExist", # 不存在 19 | "while", 20 | "for", 21 | "if", 22 | "elif", 23 | "else", 24 | "assert", 25 | "setTimeout", 26 | "call", 27 | "variable" 28 | ] 29 | keywords_app= [ 30 | "installApp", # 安装app 31 | "uninstallApp", # 卸载app 32 | "launchApp", # 启动app 33 | "closeApp", # 关闭app 34 | "tap", # 点击 35 | "doubleTap", # 双击 36 | "press", # 长按 37 | "goBack", # 返回 38 | "adb", # adb 39 | "swipe", # 滑动 40 | "input", # 输入 41 | "ifiOS", 42 | "ifAndroid" 43 | ] 44 | 45 | keywords_web = [ 46 | "openUrl", # 打开地址 47 | "close", # 关闭标签页或窗口 48 | "submit", # 提交表单 49 | "back", # 后退 50 | "forward", # 前进 51 | "refresh", # 刷新 52 | "queryDisplayed", # 等待元素可见 53 | "queryNotDisplayed", # 等待元素不可见 54 | "contextClick", # 右击 55 | "doubleClick", # 双击 56 | "holdClick", # 按下鼠标左键 57 | "dragDrop", # 鼠标拖放 58 | "dragDropByOffset", # 拖动元素到某个位置 59 | "moveByOffset", # 鼠标从当前位置移动到某个坐标 60 | "moveToElement", # 鼠标移动 61 | "moveToElementWithOffset", #移动到距某个元素(左上角坐标)多少距离的位置 62 | "sendKeys", # 输入 63 | "clear", # 清除 64 | "maxWindow", # 窗口最大化 65 | "minWindow", # 窗口最小化 66 | "fullscreenWindow", # 全屏窗口 67 | "deleteAllCookies", # 删除所有cookies 68 | "deleteCookie", # 删除指定cookies 69 | "addCookie", # 添加cookies 70 | "switchToFrame", # 切换到指定frame 71 | "switchToDefaultContent", # 切换到主文档 72 | "switchToParentFrame", # 切回到父frame 73 | "switchToWindow", # 切换句柄 74 | "setWindowSize", # 设置窗口大小 75 | "setWindowPosition", # 设置设置窗口位置 76 | "executeScript", # 执行JS 77 | "matchImage", # 匹配图片 78 | "$.executeScript", # 获取JS执行结果 79 | "$.saveScreenshot", # 截图 80 | "$.isSelected", # 判断是否选中 81 | "$.isDisplayed", # 判断元素是否显示 82 | "$.isEnabled", # 判断元素是否被使用 83 | "$.getSize", # 获取元素大小 84 | "$.getLocation", # 获取元素坐标 85 | "$.getRect", # 获取元素位置大小 86 | "$.getAttribute", # 获取元素属性 87 | "$.getTagName", # 获取元素tag Name 88 | "$.getCssProperty", # 获取元素css 89 | "$.getName", # 获取浏览器名字 90 | "$.getTitle", # 获取标题 91 | "$.getCurrentUrl", # 获取当前页面url 92 | "$.getCurrentWindowHandle", # 获取当前窗口句柄 93 | "$.getWindowHandles", # 获取所有窗口句柄 94 | "$.getCookies", # 获取所有cookie 95 | "$.getCookie", # 获取指定cookie 96 | "$.getWindowPosition", # 获取窗口坐标 97 | "$.getWindowSize", # 获取窗口大小 98 | ] 99 | 100 | if driver != 'selenium': 101 | keywords = list(set(keywords_common).union(set(keywords_app))) 102 | else: 103 | keywords = list(set(keywords_common).union(set(keywords_web))) 104 | return keywords 105 | 106 | -------------------------------------------------------------------------------- /fasttest/project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | import time 6 | import math 7 | import unittest 8 | import threading 9 | from fasttest.common import * 10 | from fasttest.utils import * 11 | from fasttest.keywords import keywords 12 | from fasttest.runner.run_case import RunCase 13 | from fasttest.drivers.driver_base_app import DriverBaseApp 14 | from fasttest.drivers.driver_base_web import DriverBaseWeb 15 | from fasttest.result.test_runner import TestRunner 16 | 17 | class Project(object): 18 | 19 | def __init__(self, index=0, workers=1, path='.'): 20 | 21 | self._index = index 22 | self._workers = workers 23 | self._root = path 24 | self._init_project() 25 | self._init_config() 26 | self._init_logging() 27 | self._analytical_testcase_file() 28 | self._analytical_common_file() 29 | self._init_data() 30 | self._init_testcase_suite() 31 | 32 | def _init_project(self): 33 | 34 | if not os.path.isdir(self._root): 35 | raise Exception('No such directory: {}'.format(self._root)) 36 | if self._root == '.': 37 | self._root = os.getcwd() 38 | Var.root = self._root 39 | sys.path.append(Var.root) 40 | sys.path.append(os.path.join(Var.root, 'Scripts')) 41 | Var.global_var = Dict() 42 | Var.extensions_var = Dict() 43 | Var.common_var = Dict() 44 | Var.common_func = Dict() 45 | 46 | def _init_config(self): 47 | 48 | self._config = analytical_file(os.path.join(Var.root, 'config.yaml')) 49 | Var.driver = self._config.driver 50 | Var.re_start = self._config.reStart 51 | Var.save_screenshot = self._config.saveScreenshot 52 | Var.time_out = self._config.timeOut 53 | Var.test_case = self._config.testcase 54 | Var.desired_caps = Dict() 55 | for configK, configV in self._config.desiredCapabilities.items(): 56 | Var.desired_caps[configK] = configV 57 | 58 | if not Var.driver or Var.driver.lower() not in ['appium', 'macaca', 'selenium']: 59 | raise ValueError('Missing/incomplete configuration file: config.yaml, No driver type specified.') 60 | 61 | if not Var.time_out or not isinstance(Var.time_out, int): 62 | Var.time_out = 10 63 | 64 | if Var.driver != 'selenium': 65 | if not Var.desired_caps.platformName: 66 | raise ValueError('Missing/incomplete configuration file: config.yaml, No platformName type specified.') 67 | DriverBaseApp.init() 68 | else: 69 | if not Var.desired_caps.browser or Var.desired_caps.browser not in ['chrome', 'safari', 'firefox', 'ie', 'opera', 'phantomjs']: 70 | raise ValueError('browser parameter is illegal!') 71 | 72 | def _init_logging(self): 73 | 74 | if Var.driver != 'selenium': 75 | # 重置udid 76 | if self._workers > 1: 77 | if isinstance(Var.desired_caps.udid, list): 78 | if not Var.desired_caps.udid: 79 | raise Exception('Can‘t find device, udid("{}") is empty.'.format(Var.desired_caps.udid)) 80 | if self._index >= len(Var.desired_caps.udid): 81 | raise Exception('the number of workers is larger than the list of udid.') 82 | if not Var.desired_caps.udid[self._index]: 83 | raise Exception('Can‘t find device, udid("{}") is empty.'.format(Var.desired_caps.udid[self._index])) 84 | devices = DevicesUtils(Var.desired_caps.platformName, Var.desired_caps.udid[self._index]) 85 | Var.desired_caps['udid'], info = devices.device_info() 86 | else: 87 | raise Exception('the udid list is not configured properly.') 88 | else: 89 | if isinstance(Var.desired_caps.udid, list): 90 | if Var.desired_caps.udid: 91 | devices = DevicesUtils(Var.desired_caps.platformName, Var.desired_caps.udid[0]) 92 | else: 93 | devices = DevicesUtils(Var.desired_caps.platformName, None) 94 | else: 95 | devices = DevicesUtils(Var.desired_caps.platformName, Var.desired_caps.udid) 96 | Var.desired_caps['udid'], info = devices.device_info() 97 | 98 | else: 99 | info = Var.desired_caps.browser 100 | 101 | thr_name = threading.currentThread().getName() 102 | report_time = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) 103 | report_child = "{}_{}_{}".format(info, report_time, thr_name) 104 | Var.report = os.path.join(Var.root, "Report", report_child) 105 | if not os.path.exists(Var.report): 106 | os.makedirs(Var.report) 107 | os.makedirs(os.path.join(Var.report, 'resource')) 108 | 109 | def _analytical_testcase_file(self): 110 | 111 | log_info('******************* analytical config *******************') 112 | for configK, configV in self._config.items(): 113 | log_info(' {}: {}'.format(configK, configV)) 114 | log_info('******************* analytical testcase *******************') 115 | testcase = TestCaseUtils() 116 | self._testcase = testcase.test_case_path(Var.root, Var.test_case) 117 | log_info(' case: {}'.format(len(self._testcase))) 118 | for case in self._testcase: 119 | log_info(' {}'.format(case)) 120 | 121 | def _analytical_common_file(self): 122 | 123 | log_info('******************* analytical common *******************') 124 | common_dir = os.path.join(Var.root, "Common") 125 | for rt, dirs, files in os.walk(common_dir): 126 | if rt == common_dir: 127 | self._load_common_func(rt, files) 128 | elif Var.desired_caps.platformName and (rt.split(os.sep)[-1].lower() == Var.desired_caps.platformName.lower()): 129 | self._load_common_func(rt, files) 130 | for commonk, commonv in Var.common_func.items(): 131 | log_info(' {}: {}'.format(commonk, commonv)) 132 | 133 | def _load_common_func(self,rt ,files): 134 | 135 | for f in files: 136 | if not f.endswith('yaml'): 137 | continue 138 | for commonK, commonV in analytical_file(os.path.join(rt, f)).items(): 139 | Var.common_func[commonK] = commonV 140 | 141 | def _init_data(self): 142 | 143 | data = analytical_file(os.path.join(Var.root, 'data.yaml')) 144 | dict = Dict(data) 145 | Var.extensions_var['variable'] = dict.variable 146 | Var.extensions_var['resource'] = dict.resource 147 | Var.extensions_var['keywords'] = dict.keywords 148 | if not Var.extensions_var.variable: 149 | Var.extensions_var['variable'] = Dict() 150 | if not Var.extensions_var.resource: 151 | Var.extensions_var['resource'] = Dict() 152 | if not Var.extensions_var.keywords: 153 | Var.extensions_var['keywords'] = Dict() 154 | # 注册全局变量 155 | log_info('******************* register variable *******************') 156 | for key, value in Var.extensions_var.variable.items(): 157 | Var.extensions_var.variable[key] = value 158 | log_info(' {}: {}'.format(key, value)) 159 | # 解析文件路径 160 | log_info('******************* register resource *******************') 161 | for resource, path in Var.extensions_var.resource.items(): 162 | resource_file = os.path.join(Var.root, path) 163 | if not os.path.isfile(resource_file): 164 | log_error('No such file or directory: {}'.format(resource_file), False) 165 | continue 166 | Var.extensions_var.resource[resource] = resource_file 167 | log_info(' {}: {}'.format(resource, resource_file)) 168 | # 注册关键字 169 | log_info('******************* register keywords *******************') 170 | Var.default_keywords_data = keywords.return_keywords(Var.driver) 171 | Var.new_keywords_data = Var.extensions_var.keywords 172 | for key in Var.extensions_var.keywords: 173 | log_info(' {}'.format(key)) 174 | 175 | def _init_testcase_suite(self): 176 | 177 | self._suite = [] 178 | # 线程数大于用例数量时,取用例数 179 | if 1 < self._index > len(self._testcase): 180 | self._workers = len(self._testcase) 181 | if self._index == len(self._testcase): 182 | return 183 | if self._workers > 1: 184 | i = self._index 185 | n = self._workers 186 | l = len(self._testcase) 187 | self._testcase = self._testcase[math.floor(i / n * l):math.floor((i + 1) / n * l)] 188 | for case_path in self._testcase: 189 | test_case = analytical_file(case_path) 190 | test_case['test_case_path'] = case_path 191 | Var.case_info = test_case 192 | subsuite = unittest.TestLoader().loadTestsFromTestCase(RunCase) 193 | self._suite.append(subsuite) 194 | Var.case_info = None 195 | 196 | def start(self): 197 | 198 | if not self._suite: 199 | return None 200 | # 组装启动参数 201 | log_info('******************* analytical desired capabilities *******************') 202 | Var.desired_capabilities = Dict({ 203 | 'driver': Var.driver.lower(), 204 | 'timeOut': Var.time_out, 205 | 'desired': Var.desired_caps, 206 | 'index': self._index, 207 | 'root': self._root 208 | }) 209 | # 启动服务 210 | if Var.driver != 'selenium': 211 | server = ServerUtilsApp(Var.desired_capabilities) 212 | Var.instance = server.start_server() 213 | elif not Var.re_start: 214 | server = ServerUtilsWeb(Var.desired_capabilities) 215 | Var.instance = server.start_server() 216 | DriverBaseWeb.init() 217 | else: 218 | server = None 219 | # 用例运行 220 | suite = unittest.TestSuite(tuple(self._suite)) 221 | runner = TestRunner() 222 | runner.run(suite) 223 | 224 | # 结束服务 225 | if Var.driver != 'selenium': 226 | server.stop_server() 227 | elif not Var.re_start: 228 | server.stop_server(Var.instance) 229 | 230 | # 打印失败结果 231 | if Var.all_result: 232 | if Var.all_result.errorsList: 233 | log_info(' Error case:') 234 | for error in Var.all_result.errorsList: 235 | log_error(error, False) 236 | 237 | if Var.all_result.failuresList: 238 | log_info(' Failed case:') 239 | for failure in Var.all_result.failuresList: 240 | log_error(failure, False) 241 | return Var.all_result -------------------------------------------------------------------------------- /fasttest/result/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /fasttest/result/html_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import sys 6 | import time 7 | import shutil 8 | 9 | class Template_mixin(object): 10 | """ 11 | Define a HTML template for report customerization and generation. 12 | 13 | Overall structure of an HTML report 14 | 15 | HTML 16 | +------------------------+ 17 | | | 18 | | | 19 | | | 20 | | STYLESHEET | 21 | | +----------------+ | 22 | | | | | 23 | | +----------------+ | 24 | | | 25 | | | 26 | | | 27 | | | 28 | | | 29 | | HEADING | 30 | | +----------------+ | 31 | | | | | 32 | | +----------------+ | 33 | | | 34 | | REPORT | 35 | | +----------------+ | 36 | | | | | 37 | | +----------------+ | 38 | | | 39 | | ENDING | 40 | | +----------------+ | 41 | | | | | 42 | | +----------------+ | 43 | | | 44 | | | 45 | | | 46 | +------------------------+ 47 | """ 48 | HTML_TMPL = r''' 49 | 50 | 51 | 52 | 53 | 测试报告 54 | 55 | 56 | 57 | 58 | 59 |
60 | {heading} 61 | {tabdiv} 62 |
63 | 64 | 65 | ''' 66 | 67 | # 测试汇总 68 | HEADING_TMPL = r''' 69 |
70 |

{title}

71 |
72 |
73 |
Summarization
74 |
75 |

Total:{total}

76 |

Success:{success}

77 |

Failure:{failure}

78 |

Error:{error}

79 |

Skipped:{skipped}

80 |

StartTime:{startTime}

81 |

Duration:{duration}

82 |
83 |
84 | ''' 85 | 86 | # 详细数据 87 | TABDIV_TMPL = r''' 88 |
89 |
Details
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | {trlist} 101 |
CaseNameDescriptionStartTimeDurationStatusOpen/Close
102 |
103 |
104 | ''' 105 | 106 | # module_name 107 | MODULE_NAME = r''' 108 | 109 | {module_name} 110 |  success:{success}  |  failure:{failure}  |  error:{error}  |  skipped:{skipped}  111 | Open 112 | 113 | ''' 114 | 115 | # case 116 | CASE_TMPL = r''' 117 | 118 | {casename} 119 | {description} 120 | {startTime} 121 | {duration} 122 | {status} 123 | Open 124 | 125 | ''' 126 | 127 | # case details 128 | CASE_DETA_NOT_SNAPSHOT = r''' 129 | 130 | 131 |
132 |

Steps

133 |
{steplist}
134 |
135 | 136 | 137 |
138 |

Logs

139 |
{errlist}
140 |
141 | 142 | 143 | ''' 144 | 145 | CASE_DETA_SNAPSHOT = r''' 146 | 147 | 148 |
149 |

Steps

150 | {steplist} 151 |
152 | 153 | 154 | ''' 155 | 156 | CASE_SNAPSHOT_DIV = r''' 157 |
158 |
159 |
{runtime} | 
160 |
{steps}
161 |
162 | 165 |
166 | ''' 167 | 168 | CASE_NOT_SNAPSHOT_DIV = r''' 169 |
170 |
171 |
{runtime} | 
172 |
{steps}
173 |
174 |
175 | ''' 176 | 177 | CASE_ERROR_DIV = r''' 178 |
179 |
180 |
{runtime} | 
181 |
{steps}
182 |
183 | 187 |
188 | ''' 189 | 190 | CASE_NOT_ERROR_DIV = r''' 191 |
192 |
193 |
{runtime} | 
194 |
{steps}
195 |
196 | 199 |
200 | ''' 201 | 202 | DEFAULT_TITLE = 'Unit Test Report' 203 | 204 | DEFAULT_DESCRIPTION = '' 205 | 206 | class HTMLTestRunner(Template_mixin): 207 | 208 | def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): 209 | self.stream = stream 210 | self.verbosity = verbosity 211 | self.title = title if title else self.DEFAULT_TITLE 212 | self.description = description if description else self.DEFAULT_DESCRIPTION 213 | 214 | def generate_report(self, result): 215 | heading = self._generate_heading(result) 216 | report = self._generate_tabdiv(result) 217 | tabdiv = self.TABDIV_TMPL.format( 218 | trlist = report 219 | ) 220 | output = self.HTML_TMPL.format( 221 | heading = heading, 222 | tabdiv = tabdiv 223 | ) 224 | resource = os.path.join(os.path.split(os.path.abspath(__file__))[0], "resource") 225 | shutil.copy(os.path.join(resource,"css.css"), os.path.join(result.report,'resource')) 226 | shutil.copy(os.path.join(resource,"js.js"), os.path.join(result.report,'resource')) 227 | self.stream.write(output.encode('utf-8')) 228 | 229 | def _generate_heading(self, report): 230 | 231 | if report: 232 | heading = self.HEADING_TMPL.format( 233 | title = self.title, 234 | total = report.total, 235 | success = report.successes, 236 | failure = report.failures, 237 | error = report.errors, 238 | skipped = report.skipped, 239 | startTime = report.startTime, 240 | duration = report.duration 241 | ) 242 | return heading 243 | 244 | def _generate_tabdiv(self, result): 245 | ''' 246 | 解析结果 247 | :param result: 248 | :return: 249 | ''' 250 | table_lsit = [] 251 | for module_name, module_list in result.result.items(): 252 | success = 0 253 | failure = 0 254 | error = 0 255 | skipped = 0 256 | cls_list = [] 257 | for test_info in module_list: 258 | # case模块 259 | case_module = self._generate_case(test_info) 260 | cls_list.append(case_module) 261 | 262 | # 具体case 263 | status = test_info.status 264 | if status != 3: # skip 265 | case_deta = self._generate_case_deta(test_info) 266 | cls_list.append(case_deta) 267 | 268 | # 统计结果 269 | if status == 0: 270 | success += 1 271 | elif status == 1: 272 | failure += 1 273 | elif status == 2: 274 | error += 1 275 | elif status == 3: 276 | skipped += 1 277 | 278 | module_name = self.MODULE_NAME.format( 279 | module_name = module_name, 280 | success = success, 281 | failure = failure, 282 | error = error, 283 | skipped = skipped, 284 | tag_module_name = module_name 285 | ) 286 | 287 | table_lsit.append(module_name) 288 | for tr in cls_list: 289 | table_lsit.append(tr) 290 | 291 | tr_ = '' 292 | for tr in table_lsit: 293 | tr_ = tr_ + tr 294 | return tr_ 295 | 296 | 297 | def _generate_case(self, test_info): 298 | ''' 299 | module 样式 300 | :param testinfo: 301 | :return: 302 | ''' 303 | status_list = ['success', 'failure', 'error', 'skipped'] 304 | casename = test_info.caseName 305 | status = status_list[test_info.status] 306 | description = test_info.description 307 | startTime = test_info.startTime 308 | duration = test_info.duration 309 | dataId = test_info.dataId 310 | module_name = test_info.moduleName 311 | 312 | caseinfo = self.CASE_TMPL.format( 313 | module_name=module_name, 314 | casename=casename, 315 | description=description, 316 | startTime=startTime, 317 | duration=duration, 318 | status=status, 319 | module=module_name, 320 | dataId=dataId, 321 | b_color=status 322 | ) 323 | return caseinfo 324 | 325 | def _generate_case_deta(self, test_info): 326 | ''' 327 | 具体case 328 | :param testinfo: 329 | :return: 330 | ''' 331 | dataId = test_info.dataId 332 | module_name = test_info.moduleName 333 | err = '\n' + test_info.err if test_info.err else 'Nothing' 334 | steps = "" 335 | if os.path.exists(test_info.snapshotDir): 336 | for key in sort_string(test_info.steps): 337 | value = test_info.steps[key] 338 | run_time = value['duration'] 339 | step = value['step'].replace('\n', '') 340 | if value['result'] != '': 341 | step = '{} --> {}'.format(value['step'], value['result']).replace('\n', '') 342 | image_path = value['snapshot'].split(test_info.report)[-1] 343 | image_path = image_path.lstrip(os.sep) 344 | if value['status']: 345 | if os.path.isfile(value['snapshot']): 346 | case_snapshot = self.CASE_SNAPSHOT_DIV.format( 347 | status='result_css_successfont', 348 | runtime=run_time, 349 | steps=step, 350 | image=image_path 351 | ) 352 | else: 353 | case_snapshot = self.CASE_NOT_SNAPSHOT_DIV.format( 354 | status='result_css_successfont', 355 | runtime=run_time, 356 | steps=step 357 | ) 358 | else: 359 | if os.path.isfile(value['snapshot']): 360 | case_snapshot = self.CASE_ERROR_DIV.format( 361 | status='result_css_errorfont', 362 | runtime=run_time, 363 | steps=step, 364 | image=image_path, 365 | errlist=err 366 | ) 367 | else: 368 | case_snapshot = self.CASE_NOT_ERROR_DIV.format( 369 | status='result_css_errorfont', 370 | runtime=run_time, 371 | steps=step, 372 | errlist=err 373 | ) 374 | 375 | steps = steps + case_snapshot 376 | 377 | casedeta = self.CASE_DETA_SNAPSHOT.format( 378 | module_name=module_name, 379 | dataId=dataId, 380 | steplist=steps, 381 | ) 382 | else: 383 | casedeta = self.CASE_DETA_NOT_SNAPSHOT.format( 384 | module_name=module_name, 385 | dataId=dataId, 386 | steplist=steps, 387 | errlist=err 388 | ) 389 | 390 | return casedeta 391 | 392 | 393 | def embedded_numbers(s): 394 | ''' 395 | :param s: 396 | :return: 397 | ''' 398 | re_digits = re.compile(r'(\d+)') 399 | pieces = re_digits.split(s) 400 | pieces[1::2] = map(int,pieces[1::2]) 401 | return pieces 402 | 403 | def sort_string(lst): 404 | 405 | return sorted(lst,key=embedded_numbers) 406 | -------------------------------------------------------------------------------- /fasttest/result/resource/css.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | html,body{ 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgb(240,240,240) 9 | } 10 | h1, h2, h3, h4, h5, h6 { 11 | color: rgba(0, 0, 0, 0.85); 12 | font-weight: 500; 13 | } 14 | font { 15 | 16 | } 17 | 18 | .result_css_root{ 19 | height: 100%; 20 | width: 100%; 21 | /*background-color:white;*/ 22 | } 23 | 24 | .result_css_title{ 25 | width: 100%; 26 | height:65px; 27 | background-color: rgba(64, 64, 64, 0.94); 28 | } 29 | .result_css_content{ 30 | border: 1px solid rgb(220,220,220); 31 | margin: 70px 40px 40px 40px; 32 | background-color:white; 33 | height: 100%; 34 | } 35 | 36 | .result_css_head{ 37 | margin: 60px 120px 20px 120px; 38 | background-color:rgb(240,240,240); 39 | } 40 | .result_css_tabdiv{ 41 | margin: 30px 120px 20px 120px; 42 | background-color:rgb(240,240,240); 43 | } 44 | 45 | .result_css_text{ 46 | line-height: 30px; 47 | color:rgb(110,110,110); 48 | font-size: 15px; 49 | margin-left:20px 50 | } 51 | .result_css_th { 52 | background-color: rgb(240,240,240); 53 | border-right: 1px solid rgb(220,220,220); 54 | color: rgb(110,110,110); 55 | line-height: 28px; 56 | font-weight: normal; 57 | 58 | } 59 | .result_css_module_td{ 60 | border-right: 1px solid rgb(220,220,220); 61 | border-top: 1px solid rgb(220,220,220); 62 | color: rgb(110,110,110); 63 | line-height: 28px; 64 | font-weight: normal; 65 | text-align:center 66 | } 67 | .result_css_module_name{ 68 | border-right: 1px solid rgb(220,220,220); 69 | border-top: 1px solid rgb(220,220,220); 70 | color: rgb(110,110,110); 71 | line-height: 28px; 72 | font-weight: normal; 73 | text-align:center 74 | } 75 | .result_css_module_td_view{ 76 | border-right: 1px solid rgb(220,220,220); 77 | border-top: 1px solid rgb(220,220,220); 78 | color: rgb(110,110,110); 79 | line-height: 28px; 80 | font-weight: normal; 81 | text-align:center 82 | } 83 | .result_css_module_deta{ 84 | background-color: rgb(240,240,240); 85 | border-right: 1px solid rgb(220,220,220); 86 | border-top: 1px solid rgb(220,220,220); 87 | color: rgb(110,110,110); 88 | vertical-align: top 89 | 90 | } 91 | .result_css_table{ 92 | width: calc(100% - 40px); 93 | margin: 20px; 94 | border: 1px solid rgb(220,220,220);result_css_ 95 | border-right-width: 0; 96 | } 97 | .result_css_head_title{ 98 | border: 1px solid rgb(220,220,220); 99 | background-color: white; 100 | border-bottom-width: 0; 101 | color:rgb(88,88,88); 102 | font-size: large; 103 | text-indent: 10px; 104 | line-height: 30px; 105 | 106 | } 107 | 108 | .result_css_success{ 109 | background-color: rgba(57, 121, 4, 0.36); 110 | } 111 | .result_css_successfont{ 112 | color: #333; 113 | } 114 | .result_css_failure{ 115 | background-color: rgba(175, 118, 0, 0.36); 116 | } 117 | .result_css_error{ 118 | background-color: rgba(121, 0, 12, 0.34); 119 | } 120 | .result_css_errorfont{ 121 | color:rgba(121, 0, 12, 0.34); 122 | } 123 | .result_css_skipped{ 124 | background-color: rgba(121, 111, 112, 0.36); 125 | } 126 | .result_css_status{ 127 | border-radius: 4px; 128 | border: 1px solid rgb(220,220,220); 129 | padding: 1px; 130 | } 131 | 132 | .result_css_child { 133 | float: left; 134 | height: 400px; 135 | width: calc(50%); 136 | box-sizing: border-box; 137 | background-clip: content-box; 138 | } 139 | 140 | .result_css_errordiv{ 141 | margin: 20px; 142 | /*background-color: white;*/ 143 | /*border: 1px solid rgb(220,220,220);*/ 144 | width: auto; 145 | height: auto; 146 | vertical-align: bottom; 147 | 148 | } 149 | .result_css_errorp{ 150 | width: calc(100% - 300px); 151 | display: inline-block; 152 | vertical-align: top; 153 | margin-top: 15px; 154 | font-size: 13px; 155 | color: #333; 156 | word-break: break-all; 157 | } 158 | 159 | 160 | /*截图*/ 161 | .result_css_SnapshotDiv_root{ 162 | width: 447px; 163 | height: auto; 164 | display:flex; 165 | margin: auto; 166 | align-items:stretch; 167 | /*text-align: center;*/ 168 | } 169 | 170 | .result_css_Stepsdetails{ 171 | margin-bottom: 2px; 172 | 173 | } 174 | .result_css_StepsdetailsDiv{ 175 | width: 100%; 176 | height: 37px; 177 | border-radius: 4px; 178 | border: 1px solid rgb(220,220,220); 179 | box-shadow:4px 4px 10px rgb(220,220,220); 180 | background-color: white; 181 | } 182 | 183 | .result_css_StepsdetailsPre{ 184 | line-height: 17px; 185 | font-size: 13px; 186 | word-break: break-all; 187 | word-wrap: break-word; 188 | display: inline-block; 189 | border-top-left-radius: 4px; 190 | border-bottom-left-radius: 4px; 191 | overflow: hidden; 192 | text-overflow: ellipsis; 193 | width: 800px; 194 | } 195 | 196 | .result_css_StepsdetailsPre_duration{ 197 | line-height: 18px; 198 | font-size: 10px; 199 | color: #333; 200 | overflow: hidden; 201 | word-break: break-all; 202 | display: inline-block; 203 | text-align: center; 204 | } 205 | 206 | .result_css_show_hide{ 207 | width: 26px; 208 | margin-top: 5.5px 209 | } 210 | .result_css_img { 211 | width: 250px; 212 | vertical-align: top; 213 | background-color: white; 214 | margin: 15px; 215 | margin-left: 0; 216 | border: 1px solid rgb(220,220,220); 217 | box-shadow:4px 4px 10px rgb(220,220,220); 218 | } 219 | 220 | .result_css_imgDiv{ 221 | display: inline-block; 222 | } 223 | 224 | .result_css_stepspan { 225 | display: block; 226 | position: relative; 227 | bottom: 0; 228 | background-color: rgba(0, 0, 0, 0.4); 229 | word-wrap:break-word; 230 | line-height: 1; 231 | text-align:left; 232 | color: white; 233 | padding: 10px; 234 | font-size: 13px; 235 | width: 317px; 236 | } 237 | .result_css_leftbutton{ 238 | display: inline; 239 | position: absolute; 240 | left: 10px; 241 | z-index: 1; 242 | top: 50%; 243 | } 244 | .result_css_rightbutton{ 245 | display: inline; 246 | right: 10px; 247 | position: absolute; 248 | z-index: 1; 249 | 250 | top: 50%; 251 | } 252 | 253 | -------------------------------------------------------------------------------- /fasttest/result/resource/js.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | var imgdivx = 0 3 | $('td.result_css_module_name').click(function(){ 4 | var data_tag = $(this).attr('data-tag') 5 | var txt = $(this).text(); 6 | if(txt == "Open") { 7 | $(this).text("Close"); 8 | $("tr[module-data-tag*='"+data_tag+"']").show() 9 | } else { 10 | $(this).text("Open"); 11 | $("tr[module-data-tag*='"+data_tag+"']").hide() 12 | var _td = $("tr[module-data-tag*='"+data_tag+"']").children('td.result_css_module_td_view') 13 | for (bottomtd in _td) { 14 | var closetr = _td.eq(bottomtd).attr('data-tag') 15 | if (typeof(closetr) != "undefined") 16 | $(_td.eq(bottomtd)).text("Open") 17 | $("tr[module-td-data-tag='" + closetr + "']").hide() 18 | var imgview = $("tr[module-td-data-tag='" + closetr + "']").find('.img_errorp') 19 | imgview.hide() 20 | } 21 | } 22 | }) 23 | 24 | $('td.result_css_module_td_view').click(function(){ 25 | var data_tag = $("tr[module-td-data-tag*='"+$(this).attr('data-tag')+"']") 26 | var txt = $(this).text(); 27 | if(txt == "Open") { 28 | $(this).text("Close"); 29 | $(data_tag).show() 30 | } else { 31 | $(this).text("Open"); 32 | $(data_tag).hide() 33 | var imgview = $(data_tag).find('.img_errorp') 34 | imgview.hide() 35 | } 36 | }) 37 | 38 | 39 | $('pre.result_css_StepsdetailsPre').click(function(){ 40 | var img = $(this).parent('.result_css_steps').next(); 41 | if (img.is(":hidden")){ 42 | img.show() 43 | }else { 44 | img.hide() 45 | } 46 | }) 47 | }) -------------------------------------------------------------------------------- /fasttest/result/test_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import time 5 | import unittest 6 | import collections 7 | from fasttest.common import * 8 | 9 | class TestInfo(object): 10 | """ 11 | This class keeps useful information about the execution of a 12 | test method. 13 | """ 14 | 15 | # Possible test outcomes 16 | (SUCCESS, FAILURE, ERROR, SKIP) = range(4) 17 | 18 | def __init__(self, test_method, status=SUCCESS, err=None): 19 | self.status = status 20 | self.elapsed_time = 0 21 | self.start_time = 0 22 | self.stop_time = 0 23 | self.err = err 24 | 25 | self.report = None 26 | self.case_path = test_method.test_case_path 27 | self.data_id = test_method.test_case_path.split('/')[-1].split(os.sep)[-1].split(".")[0] 28 | self.case_name = test_method.test_case_path.split('/')[-1].split(os.sep)[-1].split(".")[0] 29 | self.snapshot_dir = test_method.snapshot_dir 30 | self.module_name = test_method.module 31 | self.description = test_method.description 32 | self.test_case_steps = {} 33 | 34 | class TestResult(unittest.TextTestResult): 35 | 36 | def __init__(self,stream, descriptions, verbosity): 37 | super(TestResult,self).__init__(stream,descriptions,verbosity) 38 | self.stream = stream 39 | self.showAll = verbosity > 1 40 | self.descriptions = descriptions 41 | self.result = collections.OrderedDict() 42 | self.successes = [] 43 | self.testinfo = None 44 | 45 | def _save_output_data(self): 46 | ''' 47 | :return: 48 | ''' 49 | try: 50 | self._stdout_data = Var.case_message 51 | Var.case_message = "" 52 | Var.case_step_index = 0 53 | Var.case_snapshot_index = 0 54 | except AttributeError as e: 55 | pass 56 | 57 | def startTest(self, test): 58 | ''' 59 | :param test: 60 | :return: 61 | ''' 62 | super(TestResult,self).startTest(test) 63 | self.start_time = time.time() 64 | Var.test_case_steps = {} 65 | Var.is_debug = False 66 | 67 | def stopTest(self, test): 68 | ''' 69 | :param test: 70 | :return: 71 | ''' 72 | self._save_output_data() 73 | unittest.TextTestResult.stopTest(self,test) 74 | self.stop_time = time.time() 75 | self.report = test.report 76 | self.testinfo.start_time = self.start_time 77 | self.testinfo.stop_time = self.stop_time 78 | self.testinfo.report = self.report 79 | self.testinfo.test_case_steps = Var.test_case_steps 80 | if test.module not in self.result.keys(): 81 | self.result[test.module] = [] 82 | self.result[test.module].append(self.testinfo) 83 | self.testinfo = None 84 | Var.test_case_steps = {} 85 | Var.is_debug = False 86 | 87 | def addSuccess(self, test): 88 | ''' 89 | :param test: 90 | :return: 91 | ''' 92 | super(TestResult,self).addSuccess(test) 93 | self._save_output_data() 94 | self.testinfo = TestInfo(test, TestInfo.SUCCESS) 95 | self.successes.append(test) 96 | 97 | def addError(self, test, err): 98 | ''' 99 | :param test: 100 | :return: 101 | ''' 102 | super(TestResult,self).addError(test,err) 103 | self._save_output_data() 104 | _exc_str = self._exc_info_to_string(err, test) 105 | self.testinfo = TestInfo(test, TestInfo.ERROR, _exc_str) 106 | log_error(' case: {}'.format(self.testinfo.case_path), False) 107 | log_error(_exc_str, False) 108 | 109 | def addFailure(self, test, err): 110 | ''' 111 | :param test: 112 | :return: 113 | ''' 114 | super(TestResult,self).addFailure(test,err) 115 | self._save_output_data() 116 | _exc_str = self._exc_info_to_string(err, test) 117 | self.testinfo = TestInfo(test, TestInfo.FAILURE, _exc_str) 118 | log_error(' case: {}'.format(self.testinfo.case_path), False) 119 | log_error(_exc_str, False) 120 | 121 | def addSkip(self, test, reason): 122 | ''' 123 | :param test: 124 | :return: 125 | ''' 126 | super(TestResult,self).addSkip(test,reason) 127 | self._save_output_data() 128 | self.testinfo = TestInfo(test, TestInfo.SKIP) 129 | 130 | def addExpectedFailure(self, test, err): 131 | ''' 132 | :param test: 133 | :param err: 134 | :return: 135 | ''' 136 | super(TestResult, self).addFailure(test, err) 137 | self._save_output_data() 138 | _exc_str = self._exc_info_to_string(err, test) 139 | self.testinfo = TestInfo(test, TestInfo.FAILURE, _exc_str) 140 | log_error(' case: {}'.format(self.testinfo.case_path), False) 141 | log_error(_exc_str, False) 142 | 143 | -------------------------------------------------------------------------------- /fasttest/result/test_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | import time 6 | import json 7 | import unittest 8 | from fasttest.common import Var, Dict, DictEncoder 9 | from fasttest.result.test_result import TestResult 10 | from fasttest.result.html_result import HTMLTestRunner 11 | 12 | 13 | class TestRunner(unittest.TextTestRunner): 14 | 15 | def __init__(self,stream=sys.stderr, 16 | descriptions=True, verbosity=1, 17 | failfast=False, buffer=False,resultclass=None): 18 | unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity, 19 | failfast=failfast, buffer=buffer) 20 | self.descriptions = descriptions 21 | self.verbosity = verbosity 22 | self.failfast = failfast 23 | self.buffer = buffer 24 | if resultclass is None: 25 | self.resultclass = TestResult 26 | else: 27 | self.resultclass = resultclass 28 | 29 | def _makeResult(self): 30 | return self.resultclass(self.stream,self.descriptions,self.verbosity) 31 | 32 | def run(self, test): 33 | ''' 34 | :param test: 35 | :return: 36 | ''' 37 | result = self._makeResult() 38 | result.failfast = self.failfast 39 | result.buffer = self.buffer 40 | starTime = time.time() 41 | test(result) 42 | stopTime = time.time() 43 | 44 | test_result = Dict() 45 | for modulek, modulev in result.result.items(): 46 | test_list = [] 47 | for info in modulev: 48 | case_info = Dict({ 49 | 'caseName': info.case_name, 50 | 'casePath': info.case_path, 51 | 'dataId': info.data_id, 52 | 'description': info.description, 53 | 'moduleName': info.module_name, 54 | 'report': info.report, 55 | 'snapshotDir': info.snapshot_dir, 56 | 'startTime': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(info.start_time)), 57 | 'duration': str(int(info.stop_time - info.start_time)) + 's', 58 | 'status': info.status, 59 | 'err': info.err, 60 | 'steps': info.test_case_steps 61 | }) 62 | test_list.append(case_info) 63 | test_result[modulek] = test_list 64 | 65 | failures_list = [] 66 | for failure in result.failures: 67 | cast_info = failure[0] 68 | failures_list.append(cast_info.test_case_path) 69 | 70 | errors_list = [] 71 | for errors in result.errors: 72 | cast_info = errors[0] 73 | errors_list.append(cast_info.test_case_path) 74 | 75 | result = Dict({ 76 | 'report': result.report, 77 | 'total': result.testsRun, 78 | 'successes': len(result.successes), 79 | 'failures': len(result.failures), 80 | 'errors': len(result.errors), 81 | 'skipped': len(result.skipped), 82 | 'startTime': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(starTime)), 83 | 'duration': str(int(stopTime - starTime)) + 's', 84 | 'result': test_result, 85 | 'errorsList': errors_list, 86 | 'failuresList': failures_list 87 | }) 88 | 89 | properties_path = os.path.join(Var.root, 'result.properties') 90 | with open(properties_path, "w") as f: 91 | f.write(f'report={result.report}\n') 92 | f.write(f'total={result.total}\n') 93 | f.write(f'successes={result.successes}\n') 94 | f.write(f'failures={result.failures}\n') 95 | f.write(f'errors={result.errors}\n') 96 | f.write(f'skipped={result.skipped}\n') 97 | 98 | json_path = os.path.join(result.report, 'result.json') 99 | with open(json_path, 'w') as f: 100 | json.dump(result, fp=f, cls=DictEncoder, indent=4) 101 | 102 | html_file = os.path.join(Var.report,'report.html') 103 | fp = open(html_file,'wb') 104 | html_runner = HTMLTestRunner(stream=fp, 105 | title='Test Results', 106 | description='Test') 107 | html_runner.generate_report(result) 108 | Var.all_result = result 109 | fp.close() 110 | 111 | return result 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /fasttest/runner/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /fasttest/runner/action_analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import json 6 | from colorama import Fore, Back, Style 7 | from fasttest.common import Var, Dict, log_info 8 | from fasttest.common.decorator import mach_keywords, executor_keywords 9 | from fasttest.runner.action_executor_app import ActionExecutorApp 10 | from fasttest.runner.action_executor_web import ActionExecutorWeb 11 | 12 | class ActionAnalysis(object): 13 | 14 | def __init__(self): 15 | self.variables = {} 16 | self.for_variables = {} 17 | if Var.driver != 'selenium': 18 | self.action_executor = ActionExecutorApp() 19 | else: 20 | self.action_executor = ActionExecutorWeb() 21 | 22 | def _get_variables(self, name): 23 | ''' 24 | 获取变量 25 | :param name: 26 | :return: 27 | ''' 28 | if not re.match(r'^\${(\w+)}$', name): 29 | raise NameError("name '{}' is not defined".format(name)) 30 | name = name[2:-1] 31 | if name in self.for_variables.keys(): 32 | object_var = self.for_variables[name] 33 | elif name in self.variables: 34 | object_var = self.variables[name] 35 | elif name in self.common_var.keys(): 36 | object_var = self.common_var[name] 37 | elif name in Var.extensions_var.variable.keys(): 38 | object_var = Var.extensions_var.variable[name] 39 | elif name in Var.extensions_var.resource.keys(): 40 | object_var = Var.extensions_var.resource[name] 41 | else: 42 | raise NameError("name '{}' is not defined".format(name)) 43 | return object_var 44 | 45 | def _replace_string(self, content): 46 | """ 47 | 字符串替换 48 | :param content: 49 | :return: 50 | """ 51 | if isinstance(content, str): 52 | if re.match(r"^'(.*)'$", content): 53 | content = '"{}"'.format(content) 54 | elif re.match(r'^"(.*)"$', content): 55 | content = "'{}'".format(content) 56 | else: 57 | content = '"{}"'.format(content) 58 | else: 59 | content = str(content) 60 | return content 61 | 62 | def _get_replace_string(self, content): 63 | ''' 64 | 65 | :param content: 66 | :return: 67 | ''' 68 | pattern_content = re.compile(r'(\${\w+}+)') 69 | while True: 70 | if isinstance(content, str): 71 | search_contains = re.search(pattern_content, content) 72 | if search_contains: 73 | search_name = self._get_variables(search_contains.group()) 74 | if search_name is None: 75 | search_name = 'None' 76 | elif isinstance(search_name, str): 77 | if re.search(r'(\'.*?\')', search_name): 78 | search_name = '"{}"'.format(search_name) 79 | elif re.search(r'(".*?")', search_name): 80 | search_name = '\'{}\''.format(search_name) 81 | else: 82 | search_name = '"{}"'.format(search_name) 83 | else: 84 | search_name = str(search_name) 85 | content = content[0:search_contains.span()[0]] + search_name + content[search_contains.span()[1]:] 86 | else: 87 | break 88 | else: 89 | content = str(content) 90 | break 91 | 92 | return content 93 | 94 | def _get_params_type(self, param): 95 | ''' 96 | 获取参数类型 97 | :param param: 98 | :return: 99 | ''' 100 | if re.match(r"^'(.*)'$", param): 101 | param = param.strip("'") 102 | elif re.match(r'^"(.*)"$', param): 103 | param = param.strip('"') 104 | elif re.match(r'(^\${\w+}?$)', param): 105 | param = self._get_variables(param) 106 | elif re.match(r'(^\${\w+}?\[.+\]$)', param): 107 | index = param.index('}[') 108 | param_value = self._get_variables(param[:index+1]) 109 | key = self._get_params_type(param[index + 2:-1]) 110 | try: 111 | param = param_value[key] 112 | except Exception as e: 113 | raise SyntaxError('{}: {}'.format(param, e)) 114 | else: 115 | param = self._get_eval(param.strip()) 116 | return param 117 | 118 | def _get_eval(self, str): 119 | ''' 120 | :param parms: 121 | :return: 122 | ''' 123 | try: 124 | str = eval(str) 125 | except: 126 | str = str 127 | 128 | return str 129 | 130 | def _get_parms(self, parms): 131 | ''' 132 | 获取参数,传参()形式 133 | :param parms: 134 | :return: 135 | ''' 136 | parms = parms.strip() 137 | if re.match('^\(.*\)$', parms): 138 | params = [] 139 | pattern_content = re.compile(r'(".*?")|(\'.*?\')|(\${\w*?}\[.*?\])|(\${\w*?})|,| ') 140 | find_content = re.split(pattern_content, parms[1:-1]) 141 | find_content = [x.strip() for x in find_content if x] 142 | for param in find_content: 143 | var_content = self._get_params_type(param) 144 | params.append(var_content) 145 | return params 146 | else: 147 | raise SyntaxError(parms) 148 | 149 | def _analysis_exist_parms_keywords(self, step): 150 | key = step.split('(', 1)[0].strip() 151 | parms = self._get_parms(step.lstrip(key)) 152 | action_data = Dict({ 153 | 'key': key, 154 | 'parms': parms, 155 | 'step': step 156 | }) 157 | return action_data 158 | 159 | def _analysis_not_exist_parms_keywords(self, step): 160 | key = step 161 | parms = None 162 | action_data = Dict({ 163 | 'key': key, 164 | 'parms': parms, 165 | 'step': step 166 | }) 167 | return action_data 168 | 169 | def _analysis_variable_keywords(self, step): 170 | step_split = step.split('=', 1) 171 | if len(step_split) != 2: 172 | raise SyntaxError(f'"{step}"') 173 | elif not step_split[-1].strip(): 174 | raise SyntaxError(f'"{step}"') 175 | name = step_split[0].strip()[2:-1] 176 | var_value = step_split[-1].strip() 177 | 178 | if re.match(r'\$\.(\w)+\(.*\)', var_value): 179 | key = var_value.split('(', 1)[0].strip() 180 | if key == '$.id': 181 | parms = [self._get_replace_string(var_value.split(key, 1)[-1][1:-1])] 182 | else: 183 | parms = self._get_parms(var_value.split(key, 1)[-1]) 184 | elif re.match(r'(\w)+\(.*\)', var_value): 185 | key = var_value.split('(', 1)[0].strip() 186 | parms = self._get_parms(var_value.lstrip(key)) 187 | else: 188 | key = None 189 | parms = [self._get_params_type(var_value)] 190 | action_data = Dict({ 191 | 'key': 'variable', 192 | 'parms': parms, 193 | 'name': name, 194 | 'func': key, 195 | 'step': step 196 | }) 197 | return action_data 198 | 199 | def _analysis_common_keywords(self, step, style): 200 | key = step.split('call', 1)[-1].strip().split('(', 1)[0].strip() 201 | parms = step.split('call', 1)[-1].strip().split(key, 1)[-1] 202 | parms = self._get_parms(parms) 203 | action_data = Dict({ 204 | 'key': 'call', 205 | 'parms': parms, 206 | 'func': key, 207 | 'style': style, 208 | 'step': step 209 | }) 210 | return action_data 211 | 212 | def _analysis_other_keywords(self, step): 213 | key = step.split(' ', 1)[0].strip() 214 | parms = self._get_replace_string(step.lstrip(key).strip()) 215 | action_data = Dict({ 216 | 'key': key, 217 | 'parms': [parms], 218 | 'step': f'{key} {parms}' 219 | }) 220 | return action_data 221 | 222 | def _analysis_for_keywords(self, step): 223 | f_p = re.search(r'for\s+(\$\{\w+\})\s+in\s+(\S+)', step) 224 | f_t = f_p.groups() 225 | if len(f_t) != 2: 226 | raise SyntaxError(f'"{step}"') 227 | 228 | # 迭代值 229 | iterating = f_t[0][2:-1] 230 | # 迭代对象 231 | parms = self._get_params_type(f_t[1]) 232 | 233 | action_data = Dict({ 234 | 'key': 'for', 235 | 'parms': [parms], 236 | 'value': iterating, 237 | 'step': f'for {f_t[0]} in {self._get_params_type(f_t[1])}' 238 | }) 239 | return action_data 240 | 241 | @mach_keywords 242 | def _match_keywords(self, step, style): 243 | 244 | if re.match(' ', step): 245 | raise SyntaxError(f'"{step}"') 246 | step = step.strip() 247 | 248 | if re.match(r'\w+\((.*)\)', step): 249 | return self._analysis_exist_parms_keywords(step) 250 | elif re.match(r'^\w+$', step): 251 | return self._analysis_not_exist_parms_keywords(step) 252 | elif re.match(r'\$\{\w+\}=|\$\{\w+\} =', step): 253 | return self._analysis_variable_keywords(step) 254 | elif re.match(r'call \w+\(.*\)', step): 255 | return self._analysis_common_keywords(step, style) 256 | elif re.match(r'if |elif |while |assert .+', step): 257 | return self._analysis_other_keywords(step) 258 | elif re.match(r'for\s+(\$\{\w+\})\s+in\s+(\S+)+', step): 259 | return self._analysis_for_keywords(step) 260 | else: 261 | raise SyntaxError(step) 262 | 263 | @executor_keywords 264 | def executor_keywords(self, action, style): 265 | 266 | try: 267 | if action.key in Var.default_keywords_data: 268 | result = self.action_executor._action_executor(action) 269 | elif action.key in Var.new_keywords_data: 270 | result = self.action_executor._new_action_executo(action) 271 | else: 272 | raise NameError("'{}' is not defined".format(action.key)) 273 | 274 | if action.key == 'variable': 275 | # 变量赋值 276 | self.variables[action.name] = result 277 | return result 278 | except Exception as e: 279 | raise e 280 | 281 | def action_analysis(self, step, style, common, iterating_var): 282 | ''' 283 | @param step: 执行步骤 284 | @param style: 缩进 285 | @param common: call 所需参数 286 | @param iterating_var: for 迭代值 287 | @return: 288 | ''' 289 | log_info(' {}'.format(step), Fore.GREEN) 290 | if not iterating_var: 291 | self.for_variables = {} 292 | else: 293 | self.for_variables.update(iterating_var) 294 | log_info(' --> {}'.format(self.for_variables)) 295 | self.common_var = common 296 | # 匹配关键字、解析参数 297 | action_dict = self._match_keywords(step, style) 298 | log_info(' --> key: {}'.format(action_dict['key'])) 299 | log_info(' --> value: {}'.format(action_dict['parms'])) 300 | # 执行关键字 301 | result = self.executor_keywords(action_dict, style) 302 | return result 303 | 304 | if __name__ == '__main__': 305 | action = ActionAnalysis() 306 | 307 | 308 | -------------------------------------------------------------------------------- /fasttest/runner/action_executor_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from fasttest.common import Var 5 | from fasttest.drivers.driver_base_app import DriverBaseApp 6 | from fasttest.utils.opcv_utils import OpencvUtils 7 | from fasttest.runner.action_executor_base import ActionExecutorBase 8 | 9 | 10 | class ActionExecutorApp(ActionExecutorBase): 11 | 12 | def _installApp(self, action): 13 | ''' 14 | :param action: 15 | :return: 16 | ''' 17 | parms = self._getParms(action, 0) 18 | DriverBaseApp.install_app(parms) 19 | 20 | def _uninstallApp(self, action): 21 | ''' 22 | :param action: 23 | :return: 24 | ''' 25 | parms = self._getParms(action, 0) 26 | DriverBaseApp.uninstall_app(parms) 27 | 28 | def _launchApp(self, action): 29 | ''' 30 | :param action: 31 | :return: 32 | ''' 33 | parms = self._getParms(action, 0) 34 | DriverBaseApp.launch_app(parms) 35 | 36 | def _closeApp(self, action): 37 | ''' 38 | :param action: 39 | :return: 40 | ''' 41 | parms = self._getParms(action, 0, ignore=True) 42 | if parms: 43 | DriverBaseApp.close_app(parms) 44 | else: 45 | package = Var.desired_caps.package if Var.desired_caps.package else Var.desired_caps.appPackage 46 | DriverBaseApp.close_app(package) 47 | 48 | def _tap(self, action): 49 | ''' 50 | :param action: 51 | :return: 52 | ''' 53 | parms_x = self._getParms(action, 0) 54 | parms_y = self._getParms(action, 1) 55 | DriverBaseApp.tap(parms_x, parms_y) 56 | 57 | def _doubleTap(self, action): 58 | ''' 59 | :param action: 60 | :return: 61 | ''' 62 | parms_x = self._getParms(action, 0) 63 | parms_y = self._getParms(action, 1) 64 | DriverBaseApp.double_tap(parms_x, parms_y) 65 | 66 | def _press(self, action): 67 | ''' 68 | :param action: 69 | :return: 70 | ''' 71 | parms_x = self._getParms(action, 0) 72 | parms_y = self._getParms(action, 1) 73 | parms_s = self._getParms(action, 2, ignore=True) 74 | if not parms_s: 75 | DriverBaseApp.press(parms_x, parms_y) 76 | else: 77 | DriverBaseApp.press(parms_x, parms_y, parms_s) 78 | 79 | def _goBack(self, action): 80 | ''' 81 | :param action: 82 | :return: 83 | ''' 84 | DriverBaseApp.adb_shell('shell input keyevent 4') 85 | 86 | def _adb(self, action): 87 | ''' 88 | :param action: 89 | :return: 90 | ''' 91 | parms = self._getParms(action, 0) 92 | DriverBaseApp.adb_shell(parms) 93 | 94 | def _swipe(self, action): 95 | ''' 96 | :param action: 97 | :return: 98 | ''' 99 | parms_fx = self._getParms(action, 0) 100 | parms_fy = self._getParms(action, 1, ignore=True) 101 | parms_tx = self._getParms(action, 2, ignore=True) 102 | parms_ty = self._getParms(action, 3, ignore=True) 103 | parms_s = self._getParms(action, 4, ignore=True) 104 | try: 105 | if len(action.parms) == 1: 106 | swipe_f = getattr(DriverBaseApp, 'swipe_{}'.format(parms_fx.lower())) 107 | swipe_f() 108 | elif len(action.parms) == 2: 109 | swipe_f = getattr(DriverBaseApp, 'swipe_{}'.format(parms_fx.lower())) 110 | swipe_f(parms_fy) 111 | elif len(action.parms) == 4: 112 | DriverBaseApp.swipe(parms_fx, parms_fy, parms_tx, parms_ty) 113 | elif len(action.parms) == 5: 114 | DriverBaseApp.swipe(parms_fx, parms_fy, parms_tx, parms_ty, parms_s) 115 | else: 116 | raise 117 | except: 118 | raise TypeError('swipe takes 1 positional argument but {} were giver'.format(len(action.step))) 119 | 120 | def _input(self, action): 121 | ''' 122 | :param action: 123 | :return: 124 | ''' 125 | text = self._getParms(action, 1) 126 | element = self._getElement(action) 127 | DriverBaseApp.input(element, text) 128 | 129 | def _click(self, action): 130 | ''' 131 | :param action: 132 | :return: 133 | ''' 134 | parms = self._getParms(action, 0) 135 | image_name = '{}.png'.format(action.step) 136 | img_info = self._ocrAnalysis(image_name, parms) 137 | if not isinstance(img_info, bool): 138 | if img_info is not None: 139 | Var.ocrimg = img_info['ocrimg'] 140 | x = img_info['x'] 141 | y = img_info['y'] 142 | DriverBaseApp.tap(x, y) 143 | else: 144 | raise Exception("Can't find element {}".format(parms)) 145 | else: 146 | element = self._getElement(action) 147 | DriverBaseApp.click(element) 148 | 149 | def _check(self, action): 150 | ''' 151 | :param action: 152 | :return: 153 | ''' 154 | parms = self._getParms(action, 0) 155 | image_name = '{}.png'.format(action.step) 156 | img_info = self._ocrAnalysis(image_name, parms) 157 | if not isinstance(img_info, bool): 158 | if img_info is not None: 159 | Var.ocrimg = img_info['ocrimg'] 160 | else: 161 | raise Exception("Can't find element {}".format(parms)) 162 | else: 163 | self._getElement(action) 164 | 165 | def _ifiOS(self, action): 166 | ''' 167 | :param action: 168 | :return: 169 | ''' 170 | if Var.desired_caps.platformName.lower() == 'ios': 171 | return True 172 | return False 173 | 174 | def _ifAndroid(self, action): 175 | ''' 176 | :param action: 177 | :return: 178 | ''' 179 | if Var.desired_caps.platformName.lower() == 'android': 180 | return True 181 | return False 182 | 183 | def _getText(self, action): 184 | ''' 185 | :param action: 186 | :return: 187 | ''' 188 | element = self._getElement(action) 189 | text = DriverBaseApp.get_text(element) 190 | return text 191 | 192 | def _getElement(self, action): 193 | ''' 194 | :param action: 195 | :return: 196 | ''' 197 | parms = self._getParms(action, 0) 198 | if Var.driver == 'appium': 199 | from appium.webdriver import WebElement 200 | if Var.driver == 'macaca': 201 | from macaca.webdriver import WebElement 202 | if isinstance(parms, WebElement): 203 | element = parms 204 | else: 205 | element = DriverBaseApp.find_elements_by_key(key=parms, timeout=Var.time_out, interval=Var.interval) 206 | if not element: 207 | raise Exception("Can't find element {}".format(parms)) 208 | return element 209 | 210 | def _getElements(self, action): 211 | ''' 212 | :param action: 213 | :return: 214 | ''' 215 | parms = self._getParms(action, 0) 216 | elements = DriverBaseApp.find_elements_by_key(key=parms, timeout=Var.time_out, interval=Var.interval, 217 | not_processing=True) 218 | if not elements: 219 | raise Exception("Can't find element {}".format(parms)) 220 | return elements 221 | 222 | def _isExist(self, action): 223 | ''' 224 | :param action: 225 | :return: 226 | ''' 227 | parms = self._getParms(action, 0) 228 | image_name = '{}.png'.format(action.step) 229 | img_info = self._ocrAnalysis(image_name, parms) 230 | result = True 231 | if not isinstance(img_info, bool): 232 | if img_info is not None: 233 | Var.ocrimg = img_info['ocrimg'] 234 | else: 235 | result = False 236 | else: 237 | elements = DriverBaseApp.find_elements_by_key(key=parms, timeout=Var.time_out, interval=Var.interval, not_processing=True) 238 | result = bool(elements) 239 | return result 240 | 241 | def _isNotExist(self, action): 242 | ''' 243 | :param action: 244 | :return: 245 | ''' 246 | parms = self._getParms(action, 0) 247 | image_name = '{}.png'.format(action.step) 248 | img_info = self._ocrAnalysis(image_name, parms) 249 | result = False 250 | if not isinstance(img_info, bool): 251 | if img_info is not None: 252 | Var.ocrimg = img_info['ocrimg'] 253 | result = True 254 | else: 255 | elements = DriverBaseApp.find_elements_by_key(key=parms, timeout=0, interval=Var.interval, not_processing=True) 256 | result = bool(elements) 257 | return not result 258 | 259 | def _ocrAnalysis(self,image_name, match_image): 260 | ''' 261 | :param image_name: 262 | :param match_image: 263 | :return: 264 | ''' 265 | try: 266 | if not isinstance(match_image, str): 267 | return False 268 | if not os.path.isfile(match_image): 269 | return False 270 | 271 | image_dir = os.path.join(Var.snapshot_dir, 'screenshot') 272 | if not os.path.exists(image_dir): 273 | os.makedirs(image_dir) 274 | base_image = os.path.join(image_dir, '{}'.format(image_name)) 275 | Var.instance.save_screenshot(base_image) 276 | height = Var.instance.get_window_size()['height'] 277 | 278 | orcimg = OpencvUtils(base_image, match_image, height) 279 | img_info = orcimg.extract_minutiae() 280 | if img_info: 281 | return img_info 282 | else: 283 | return None 284 | except: 285 | return False -------------------------------------------------------------------------------- /fasttest/runner/action_executor_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import time 5 | import copy 6 | from typing import Iterable 7 | from fasttest.common import Var, log_info, log_error 8 | 9 | 10 | class ActionExecutorBase(object): 11 | 12 | def _import(self): 13 | file_list = [] 14 | try: 15 | for rt, dirs, files in os.walk(os.path.join(Var.root, "Scripts")): 16 | for f in files: 17 | if f == "__init__.py" or f.endswith(".pyc") or f.startswith(".") or not f.endswith('.py'): 18 | continue 19 | file_list.append(f'from Scripts.{f[:-3]} import *') 20 | except Exception as e: 21 | log_error(' {}'.format(e), False) 22 | 23 | return file_list 24 | 25 | def _out(self, key, result): 26 | if isinstance(result, list): 27 | log_info(f' <-- {key}: {type(result)}') 28 | for l in result: 29 | log_info(' - {}'.format(l)) 30 | elif isinstance(result, dict): 31 | log_info(f' <-- {key}: {type(result)}') 32 | for k, v in result.items(): 33 | log_info(' - {}: {}'.format(k, v)) 34 | else: 35 | log_info(f' <-- {key}: {type(result)} {result}') 36 | 37 | def _getParms(self, action, index=0, ignore=False): 38 | parms = action.parms 39 | if len(parms) <= index or not len(parms): 40 | if ignore: 41 | return None 42 | raise TypeError('missing {} required positional argument'.format(index + 1)) 43 | value = parms[index] 44 | return value 45 | 46 | def _sleep(self, action): 47 | ''' 48 | :param action: 49 | :return: 50 | ''' 51 | parms = self._getParms(action, 0) 52 | time.sleep(float(parms)) 53 | return 54 | 55 | def _setVar(self, action): 56 | ''' 57 | :param action: 58 | :return: 59 | ''' 60 | key = self._getParms(action, 0) 61 | values = self._getParms(action, 1) 62 | Var.global_var[key] = values 63 | return 64 | 65 | def _getVar(self, action): 66 | ''' 67 | :param action: 68 | :return: 69 | ''' 70 | key = self._getParms(action, 0) 71 | if Var.global_var: 72 | if key in Var.global_var: 73 | result = Var.global_var[key] 74 | else: 75 | result = None 76 | else: 77 | result = None 78 | return result 79 | 80 | def _getLen(self, action): 81 | ''' 82 | :param action: 83 | :return: 84 | ''' 85 | value = self._getParms(action, 0) 86 | if value: 87 | return len(value) 88 | return 0 89 | 90 | def _break(self, action): 91 | ''' 92 | :param action: 93 | :return: 94 | ''' 95 | return True 96 | 97 | def _if(self, action): 98 | ''' 99 | :param action: 100 | :return: 101 | ''' 102 | parms = self._getParms(action, 0) 103 | try: 104 | parms = parms.replace('\n', '') 105 | result = eval(parms) 106 | log_info(' <-- {}'.format(bool(result))) 107 | return bool(result) 108 | except Exception as e: 109 | raise e 110 | 111 | def _elif(self, action): 112 | ''' 113 | :param action: 114 | :return: 115 | ''' 116 | parms = self._getParms(action, 0) 117 | try: 118 | parms = parms.replace('\n', '') 119 | result = eval(parms) 120 | log_info(' <-- {}'.format(bool(result))) 121 | return bool(result) 122 | except Exception as e: 123 | raise e 124 | 125 | def _else(self, action): 126 | ''' 127 | :param action: 128 | :return: 129 | ''' 130 | return True 131 | 132 | def _while(self, action): 133 | ''' 134 | :param action: 135 | :return: 136 | ''' 137 | parms = self._getParms(action, 0) 138 | try: 139 | parms = parms.replace('\n', '') 140 | result = eval(parms) 141 | log_info(' <-- {}'.format(bool(result))) 142 | return bool(result) 143 | except Exception as e: 144 | raise e 145 | 146 | def _for(self, action): 147 | ''' 148 | :param action: 149 | :return: 150 | ''' 151 | items = self._getParms(action, 0) 152 | value = action.value 153 | if not isinstance(items, Iterable): 154 | raise TypeError("'{}' object is not iterable".format(items)) 155 | return {'key': value, 'value': items} 156 | 157 | def _assert(self, action): 158 | ''' 159 | :param action: 160 | :return: 161 | ''' 162 | parms = self._getParms(action, 0) 163 | try: 164 | parms = parms.replace('\n', '') 165 | result = eval(parms) 166 | log_info(' <-- {}'.format(bool(result))) 167 | assert result 168 | except Exception as e: 169 | raise e 170 | 171 | def _setTimeout(self, action): 172 | ''' 173 | :param action: 174 | :return: 175 | ''' 176 | parms = self._getParms(action, 0) 177 | Var.time_out = int(parms) 178 | 179 | def _id(self, action): 180 | ''' 181 | :param action: 182 | :return: 183 | ''' 184 | parms = self._getParms(action, 0) 185 | parms = parms.replace('\n', '') 186 | result = eval(parms) 187 | return result 188 | 189 | def _call(self, action): 190 | ''' 191 | :param action: 192 | :return: 193 | ''' 194 | parms = action.parms 195 | func = action.func 196 | if not func in Var.common_func.keys(): 197 | raise NameError("name '{}' is not defined".format(func)) 198 | if len(Var.common_func[func].input) != len(parms): 199 | raise TypeError('{}() takes {} positional arguments but {} was given'.format(func, len( 200 | Var.common_func[func].input), len(parms))) 201 | common_var = dict(zip(Var.common_func[func].input, parms)) 202 | 203 | try: 204 | from fasttest.runner.case_analysis import CaseAnalysis 205 | case = CaseAnalysis() 206 | case.iteration(Var.common_func[func].steps, '{} '.format(action.style), common_var) 207 | except Exception as e: 208 | # call action中如果某一句step异常,此处会往上抛异常,导致call action也是失败状态,需要标记 209 | Var.exception_flag = True 210 | raise e 211 | 212 | def _variable(self, action): 213 | ''' 214 | 调用$.类型方法 215 | :param action: 216 | :return: 217 | ''' 218 | try: 219 | func = action.func 220 | if func and func.startswith('$.'): 221 | func_ = getattr(self, '_{}'.format(func[2:])) 222 | result = func_(action) 223 | elif func: 224 | new_action = action.copy() #todo 225 | new_action['key'] = action.func 226 | result = self._new_action_executo(new_action) 227 | else: 228 | result = self._getParms(action, 0) 229 | except Exception as e: 230 | raise e 231 | 232 | self._out(action.name, result) 233 | return result 234 | 235 | def _action_executor(self, action): 236 | ''' 237 | 默认关键字 238 | :param action: 239 | :return: 240 | ''' 241 | try: 242 | func = getattr(self, '_{}'.format(action.key)) 243 | except Exception as e: 244 | raise NameError("keyword '{}' is not defined".format(action.key)) 245 | result = func(action) 246 | return result 247 | 248 | def _new_action_executo(self, action, output=True): 249 | ''' 250 | 自定义关键字 251 | :param action: 252 | :return: 253 | ''' 254 | list = self._import() 255 | for l in list: 256 | exec(l) 257 | parms = None 258 | for index, par in enumerate(action.parms): 259 | if not parms: 260 | parms = 'action.parms[{}]'.format(index) 261 | continue 262 | parms = '{}, action.parms[{}]'.format(parms, index) 263 | if not parms: 264 | result = eval('locals()[action.key]()') 265 | else: 266 | result = eval('locals()[action.key]({})'.format(parms)) 267 | if result and output: 268 | self._out(action.key, result) 269 | return result 270 | -------------------------------------------------------------------------------- /fasttest/runner/action_executor_web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import time 6 | import datetime 7 | from selenium.webdriver.remote.webelement import WebElement 8 | from selenium.common.exceptions import JavascriptException 9 | from fasttest.common import Var 10 | from fasttest.common.check import check 11 | from fasttest.drivers.driver_base_web import DriverBaseWeb 12 | from fasttest.utils.opcv_utils import OpencvUtils 13 | from fasttest.runner.action_executor_base import ActionExecutorBase 14 | 15 | 16 | class ActionExecutorWeb(ActionExecutorBase): 17 | 18 | def _openUrl(self, action): 19 | ''' 20 | :param action: 21 | :return: 22 | ''' 23 | url = self._getParms(action, 0) 24 | DriverBaseWeb.open_url(url) 25 | 26 | def _close(self, action): 27 | ''' 28 | :param action: 29 | :return: 30 | ''' 31 | DriverBaseWeb.close() 32 | 33 | def _quit(self, action): 34 | ''' 35 | :param action: 36 | :return: 37 | ''' 38 | DriverBaseWeb.quit() 39 | 40 | @check 41 | def _submit(self, action): 42 | ''' 43 | :param action: 44 | :return: 45 | ''' 46 | element = self._getElement(action) 47 | DriverBaseWeb.submit(element) 48 | 49 | def _back(self, action): 50 | ''' 51 | :param action: 52 | :return: 53 | ''' 54 | DriverBaseWeb.back() 55 | 56 | def _forward(self, action): 57 | ''' 58 | :param action: 59 | :return: 60 | ''' 61 | DriverBaseWeb.forward() 62 | 63 | def _refresh(self, action): 64 | ''' 65 | :param action: 66 | :return: 67 | ''' 68 | DriverBaseWeb.refresh() 69 | 70 | def _queryDisplayed(self, action): 71 | ''' 72 | :param action: 73 | :return: 74 | ''' 75 | parms = self._getParms(action, 0) 76 | if isinstance(parms, WebElement): 77 | element = parms 78 | DriverBaseWeb.query_displayed(element=element, timeout=Var.time_out) 79 | elif isinstance(parms, str): 80 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 81 | parms.strip(), re.I): 82 | raise TypeError('input parameter format error:{}'.format(parms)) 83 | key = parms.split('=', 1)[0].strip() 84 | value = parms.split('=', 1)[-1].strip() 85 | DriverBaseWeb.query_displayed(type=key, text=value, timeout=Var.time_out) 86 | else: 87 | raise TypeError('the parms type must be: WebElement or str') 88 | 89 | def _queryNotDisplayed(self, action): 90 | ''' 91 | :param action: 92 | :return: 93 | ''' 94 | parms = self._getParms(action, 0) 95 | if isinstance(parms, WebElement): 96 | element = parms 97 | DriverBaseWeb.query_not_displayed(element=element, timeout=Var.time_out) 98 | elif isinstance(parms, str): 99 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 100 | parms.strip(), re.I): 101 | raise TypeError('input parameter format error:{}'.format(parms)) 102 | key = parms.split('=', 1)[0].strip() 103 | value = parms.split('=', 1)[-1].strip() 104 | DriverBaseWeb.query_not_displayed(type=key, text=value, timeout=Var.time_out) 105 | else: 106 | raise TypeError('the parms type must be: WebElement or str') 107 | 108 | @check 109 | def _click(self, action): 110 | ''' 111 | :param action: 112 | :return: 113 | ''' 114 | element = self._getElement(action) 115 | DriverBaseWeb.click(element) 116 | 117 | @check 118 | def _check(self, action): 119 | ''' 120 | :param action: 121 | :return: 122 | ''' 123 | self._getElement(action) 124 | 125 | @check 126 | def _contextClick(self, action): 127 | ''' 128 | :param action: 129 | :return: 130 | ''' 131 | element = self._getElement(action) 132 | DriverBaseWeb.context_click(element) 133 | 134 | @check 135 | def _doubleClick(self, action): 136 | ''' 137 | :param action: 138 | :return: 139 | ''' 140 | element = self._getElement(action) 141 | DriverBaseWeb.double_click(element) 142 | 143 | @check 144 | def _holdClick(self, action): 145 | ''' 146 | :param action: 147 | :return: 148 | ''' 149 | element = self._getElement(action) 150 | DriverBaseWeb.click_and_hold(element) 151 | 152 | @check 153 | def _dragDrop(self, action): 154 | ''' 155 | :param action: 156 | :return: 157 | ''' 158 | element = self._getElement(action, 0) 159 | target = self._getElement(action, 1) 160 | DriverBaseWeb.drag_and_drop(element, target) 161 | 162 | @check 163 | def _dragDropByOffset(self, action): 164 | ''' 165 | :param action: 166 | :return: 167 | ''' 168 | element = self._getElement(action) 169 | xoffset = self._getParms(action, 1) 170 | yoffset = self._getParms(action, 2) 171 | DriverBaseWeb.drag_and_drop_by_offse(element, float(xoffset), float(yoffset)) 172 | 173 | def _moveByOffset(self, action): 174 | ''' 175 | :param action: 176 | :return: 177 | ''' 178 | xoffset = self._getParms(action, 0) 179 | yoffset = self._getParms(action, 1) 180 | DriverBaseWeb.move_by_offset(float(xoffset), float(yoffset)) 181 | 182 | @check 183 | def _moveToElement(self, action): 184 | ''' 185 | :param action: 186 | :return: 187 | ''' 188 | element = self._getElement(action) 189 | DriverBaseWeb.move_to_element(element) 190 | 191 | @check 192 | def _moveToElementWithOffset(self, action): 193 | ''' 194 | :param action: 195 | :return: 196 | ''' 197 | element = self._getElement(action) 198 | xoffset = self._getParms(action, 1) 199 | yoffset = self._getParms(action, 2) 200 | DriverBaseWeb.move_to_element_with_offset(element, float(xoffset), float(yoffset)) 201 | 202 | @check 203 | def _sendKeys(self, action): 204 | ''' 205 | :param action: 206 | :return: 207 | ''' 208 | element = self._getElement(action) 209 | text_list = [] 210 | if len(action.parms) == 2: 211 | text_list.append(self._getParms(action, 1)) 212 | elif len(action.parms) == 3: 213 | text_list.append(self._getParms(action, 1)) 214 | text_list.append(self._getParms(action, 2)) 215 | else: 216 | raise TypeError('missing 1 required positional argument') 217 | DriverBaseWeb.send_keys(element, text_list) 218 | 219 | @check 220 | def _clear(self, action): 221 | ''' 222 | :param action: 223 | :return: 224 | ''' 225 | element = self._getElement(action) 226 | DriverBaseWeb.clear(element) 227 | 228 | def _maxWindow(self, action): 229 | ''' 230 | :param action: 231 | :return: 232 | ''' 233 | DriverBaseWeb.maximize_window() 234 | 235 | def _minWindow(self, action): 236 | ''' 237 | :param action: 238 | :return: 239 | ''' 240 | DriverBaseWeb.minimize_window() 241 | 242 | def _fullscreenWindow(self, action): 243 | ''' 244 | :param action: 245 | :return: 246 | ''' 247 | DriverBaseWeb.fullscreen_window() 248 | 249 | def _deleteAllCookies(self, action): 250 | ''' 251 | :param action: 252 | :return: 253 | ''' 254 | DriverBaseWeb.delete_all_cookies() 255 | 256 | def _deleteCookie(self, action): 257 | ''' 258 | :param action: 259 | :return: 260 | ''' 261 | key = self._getParms(action, 0) 262 | DriverBaseWeb.delete_cookie(key) 263 | 264 | def _addCookie(self, action): 265 | ''' 266 | :param action: 267 | :return: 268 | ''' 269 | key = self._getParms(action, 0) 270 | DriverBaseWeb.add_cookie(key) 271 | 272 | def _switchToFrame(self, action): 273 | ''' 274 | :param action: 275 | :return: 276 | ''' 277 | frame_reference = self._getParms(action) 278 | DriverBaseWeb.switch_to_frame(frame_reference) 279 | 280 | def _switchToDefaultContent(self, action): 281 | ''' 282 | :param action: 283 | :return: 284 | ''' 285 | DriverBaseWeb.switch_to_default_content() 286 | 287 | def _switchToParentFrame(self, action): 288 | ''' 289 | :param action: 290 | :return: 291 | ''' 292 | DriverBaseWeb.switch_to_parent_frame() 293 | 294 | def _switchToWindow(self, action): 295 | ''' 296 | :param action: 297 | :return: 298 | ''' 299 | handle = self._getParms(action) 300 | DriverBaseWeb.switch_to_window(handle) 301 | 302 | def _setWindowSize(self, action): 303 | ''' 304 | :param action: 305 | :return: 306 | ''' 307 | width = self._getParms(action, 0) 308 | height = self._getParms(action, 0) 309 | DriverBaseWeb.set_window_size(float(width), float(height)) 310 | 311 | def _setWindowPosition(self, action): 312 | ''' 313 | :param action: 314 | :return: 315 | ''' 316 | x = self._getParms(action, 0) 317 | y = self._getParms(action, 1) 318 | DriverBaseWeb.set_window_position(float(x), float(y)) 319 | 320 | def _executeScript(self, action): 321 | ''' 322 | :param action: 323 | :return: 324 | ''' 325 | endTime = datetime.datetime.now() + datetime.timedelta(seconds=int(Var.time_out)) 326 | while True: 327 | try: 328 | js_value = self._getParms(action) 329 | return DriverBaseWeb.execute_script(js_value) 330 | except JavascriptException as e: 331 | if datetime.datetime.now() >= endTime: 332 | raise e 333 | time.sleep(0.1) 334 | except Exception as e: 335 | raise e 336 | 337 | def _matchImage(self, action): 338 | ''' 339 | :param action: 340 | :return: 341 | ''' 342 | base_image = self._getParms(action, 0) 343 | match_image = self._getParms(action, 1) 344 | if not os.path.isfile(match_image): 345 | raise FileNotFoundError("No such file: {}".format(match_image)) 346 | if not os.path.isfile(base_image): 347 | raise FileNotFoundError("No such file: {}".format(base_image)) 348 | height = Var.instance.get_window_size()['height'] 349 | orc_img = OpencvUtils(base_image, match_image, height) 350 | img_info = orc_img.extract_minutiae() 351 | if img_info: 352 | Var.ocrimg = img_info['ocrimg'] 353 | else: 354 | raise Exception("Can't find image {}".format(match_image)) 355 | 356 | @check 357 | def _saveScreenshot(self, action): 358 | ''' 359 | :param action: 360 | :return: 361 | ''' 362 | if len(action.parms) == 1: 363 | element = None 364 | name = self._getParms(action, 0) 365 | else: 366 | element = self._getElement(action) 367 | name = self._getParms(action, 1) 368 | return DriverBaseWeb.save_screenshot(element, name) 369 | 370 | @check 371 | def _isSelected(self, action): 372 | ''' 373 | :param action: 374 | :return: 375 | ''' 376 | element = self._getElement(action) 377 | return DriverBaseWeb.is_selected(element) 378 | 379 | @check 380 | def _isDisplayed(self, action): 381 | ''' 382 | :param action: 383 | :return: 384 | ''' 385 | element = self._getElement(action) 386 | return DriverBaseWeb.is_displayed(element) 387 | 388 | @check 389 | def _isEnabled(self, action): 390 | ''' 391 | :param action: 392 | :return: 393 | ''' 394 | element = self._getElement(action) 395 | return DriverBaseWeb.is_enabled(element) 396 | 397 | @check 398 | def _getSize(self, action): 399 | ''' 400 | :param action: 401 | :return: 402 | ''' 403 | element = self._getElement(action) 404 | return DriverBaseWeb.get_size(element) 405 | 406 | @check 407 | def _getLocation(self, action): 408 | ''' 409 | :param action: 410 | :return: 411 | ''' 412 | element = self._getElement(action) 413 | return DriverBaseWeb.get_location(element) 414 | 415 | @check 416 | def _getRect(self, action): 417 | ''' 418 | :param action: 419 | :return: 420 | ''' 421 | element = self._getElement(action) 422 | return DriverBaseWeb.get_rect(element) 423 | 424 | @check 425 | def _getAttribute(self, action): 426 | ''' 427 | :param action: 428 | :return: 429 | ''' 430 | element = self._getElement(action) 431 | attribute = self._getParms(action, 1) 432 | return DriverBaseWeb.get_attribute(element, attribute) 433 | 434 | @check 435 | def _getTagName(self, action): 436 | ''' 437 | :param action: 438 | :return: 439 | ''' 440 | element = self._getElement(action) 441 | return DriverBaseWeb.get_tag_name(element) 442 | 443 | @check 444 | def _getCssProperty(self, action): 445 | ''' 446 | :param action: 447 | :return: 448 | ''' 449 | element = self._getElement(action) 450 | css_value = self._getParms(action, 1) 451 | return DriverBaseWeb.get_css_property(element, css_value) 452 | 453 | def _getName(self, action): 454 | ''' 455 | :param action: 456 | :return: 457 | ''' 458 | return DriverBaseWeb.get_name() 459 | 460 | def _getTitle(self, action): 461 | ''' 462 | :param action: 463 | :return: 464 | ''' 465 | return DriverBaseWeb.get_title() 466 | 467 | def _getCurrentUrl(self, action): 468 | ''' 469 | :param action: 470 | :return: 471 | ''' 472 | return DriverBaseWeb.get_current_url() 473 | 474 | def _getCurrentWindowHandle(self, action): 475 | ''' 476 | :param action: 477 | :return: 478 | ''' 479 | return DriverBaseWeb.get_current_window_handle() 480 | 481 | def _getWindowHandles(self, action): 482 | ''' 483 | :param action: 484 | :return: 485 | ''' 486 | return DriverBaseWeb.get_window_handles() 487 | 488 | def _getCookies(self, action): 489 | ''' 490 | :param action: 491 | :return: 492 | ''' 493 | return DriverBaseWeb.get_cookies() 494 | 495 | def _getCookie(self, action): 496 | ''' 497 | :param action: 498 | :return: 499 | ''' 500 | key = self._getParms(action) 501 | return DriverBaseWeb.get_cookie(key) 502 | 503 | def _getWindowPosition(self, action): 504 | ''' 505 | :param action: 506 | :return: 507 | ''' 508 | return DriverBaseWeb.get_window_position() 509 | 510 | def _getWindowSize(self, action): 511 | ''' 512 | :param action: 513 | :return: 514 | ''' 515 | return DriverBaseWeb.get_window_size() 516 | 517 | @check 518 | def _getText(self, action): 519 | ''' 520 | :param action: 521 | :return: 522 | ''' 523 | element = self._getElement(action) 524 | return DriverBaseWeb.get_text(element) 525 | 526 | def _getElement(self, action, index=0): 527 | ''' 528 | :param action: 529 | :return: 530 | ''' 531 | parms = action.parms 532 | if len(parms) <= index or not len(parms): 533 | raise TypeError('missing {} required positional argument'.format(index + 1)) 534 | if isinstance(parms[index], WebElement): 535 | element = parms[index] 536 | elif isinstance(parms[index], str): 537 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 538 | parms[index].strip(), re.I): 539 | raise TypeError('input parameter format error:{}'.format(parms[index])) 540 | key = parms[index].split('=', 1)[0].strip() 541 | value = parms[index].split('=', 1)[-1].strip() 542 | element = DriverBaseWeb.get_element(key, value, Var.time_out) 543 | else: 544 | raise TypeError('the parms type must be: WebElement or str') 545 | 546 | if not element: 547 | raise Exception("Can't find element: {}".format(parms[index])) 548 | return element 549 | 550 | def _getElements(self, action): 551 | ''' 552 | :param action: 553 | :return: 554 | ''' 555 | parms = self._getParms(action, 0) 556 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 557 | parms.strip(), re.I): 558 | raise TypeError('input parameter format error:{}'.format(parms)) 559 | key = parms.strip().split('=', 1)[0] 560 | value = parms.strip().split('=', 1)[-1] 561 | elements = DriverBaseWeb.get_elements(key, value, Var.time_out) 562 | if not elements: 563 | raise Exception("Can't find elements: {}".format(parms)) 564 | return elements 565 | 566 | def _isExist(self, action): 567 | ''' 568 | :param action: 569 | :return: 570 | ''' 571 | parms = self._getParms(action, 0) 572 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 573 | parms.strip(), re.I): 574 | raise TypeError('input parameter format error:{}'.format(parms)) 575 | key = parms.strip().split('=', 1)[0] 576 | value = parms.strip().split('=', 1)[-1] 577 | elements = DriverBaseWeb.get_elements(key, value, Var.time_out) 578 | return bool(elements) 579 | 580 | def _isNotExist(self, action): 581 | ''' 582 | :param action: 583 | :return: 584 | ''' 585 | parms = self._getParms(action, 0) 586 | if not re.match(r'^(id|name|class|tag_name|link_text|partial_link_text|xpath|css_selector)\s*=.+', 587 | parms.strip(), re.I): 588 | raise TypeError('input parameter format error:{}'.format(parms)) 589 | key = parms.strip().split('=', 1)[0] 590 | value = parms.strip().split('=', 1)[-1] 591 | elements = DriverBaseWeb.get_elements(key, value, 0) 592 | return not bool(elements) -------------------------------------------------------------------------------- /fasttest/runner/case_analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | import traceback 6 | from fasttest.common import Var 7 | from fasttest.runner.action_analysis import ActionAnalysis 8 | 9 | class CaseAnalysis(object): 10 | 11 | def __init__(self): 12 | self.action_nalysis = ActionAnalysis() 13 | self.testcase_steps = [] 14 | self.is_run = None 15 | self.timeout = 10 16 | 17 | def iteration(self, steps, style='', common={}, iterating_var=None): 18 | ''' 19 | 20 | @param steps: 21 | @param style: 控制结果报告中每句case的缩进 22 | @param common: call 调用时需要的参数 23 | @param iterating_var: for 迭代对象 24 | @return: 25 | ''' 26 | if isinstance(steps, list): 27 | for step in steps: 28 | if isinstance(step, str): 29 | self.case_executor(step, style, common, iterating_var) 30 | if step.startswith('break'): 31 | return 'break' 32 | elif isinstance(step, dict): 33 | result = self.iteration(step, style, common, iterating_var) 34 | if result == 'break': 35 | return 'break' 36 | elif isinstance(steps, dict): 37 | for key, values in steps.items(): 38 | if key.startswith('while'): 39 | while self.case_executor(key, style, common, iterating_var): 40 | result = self.iteration(values, f'{style} ', common, iterating_var) 41 | if result == 'break': 42 | break 43 | elif key.startswith('if') or key.startswith('elif') or key.startswith('else'): 44 | if self.case_executor(key, style, common, iterating_var): 45 | result = self.iteration(values, f'{style} ', common, iterating_var) 46 | if result == 'break': 47 | return 'break' 48 | break # 判断下执行完毕,跳出循环 49 | elif re.match('for\s+(\$\{\w+\})\s+in\s+(\S+)', key): 50 | parms = self.case_executor(key, style, common, iterating_var) 51 | for f in parms['value']: 52 | iterating_var = {parms['key']: f} 53 | result = self.iteration(values, f'{style} ', common, iterating_var) 54 | if result == 'break': 55 | break 56 | else: 57 | raise SyntaxError('- {}:'.format(key)) 58 | 59 | def case_executor(self, step, style, common, iterating_var): 60 | 61 | # call 需要全局变量判断是否是debug模式 62 | if step.strip().endswith('--Debug') or step.strip().endswith('--debug') or Var.is_debug: 63 | Var.is_debug = True 64 | while True: 65 | try: 66 | if self.is_run is False: 67 | print(step) 68 | out = input('>') 69 | elif not (step.strip().endswith('--Debug') or step.strip().endswith('--debug')): 70 | self.is_run = True 71 | result = self.action_nalysis.action_analysis(self.rstrip_step(step), style, common, iterating_var) 72 | return result 73 | else: 74 | print(step) 75 | out = input('>') 76 | 77 | if not len(out): 78 | self.is_run = False 79 | continue 80 | elif out.lower() == 'r': 81 | # run 82 | self.is_run = True 83 | result = self.action_nalysis.action_analysis(self.rstrip_step(step), style, common, iterating_var) 84 | return result 85 | elif out.lower() == 'c': 86 | # continue 87 | self.is_run = False 88 | break 89 | elif out.lower() == 'n': 90 | # next 91 | self.is_run = False 92 | result = self.action_nalysis.action_analysis(self.rstrip_step(step), style, common, iterating_var) 93 | return result 94 | elif out.lower() == 'q': 95 | # quit 96 | sys.exit() 97 | else: 98 | # runtime 99 | self.is_run = False 100 | self.timeout = Var.time_out 101 | Var.time_out = 0.5 102 | self.action_nalysis.action_analysis(out, style, common, iterating_var) 103 | Var.time_out = self.timeout 104 | continue 105 | except Exception as e: 106 | Var.time_out = self.timeout 107 | self.is_run = False 108 | traceback.print_exc() 109 | continue 110 | else: 111 | result = self.action_nalysis.action_analysis(step, style, common, iterating_var) 112 | return result 113 | 114 | 115 | def rstrip_step(self, step): 116 | if step.strip().endswith('--Debug') or step.strip().endswith('--debug'): 117 | return step.strip()[:-7].strip() 118 | return step -------------------------------------------------------------------------------- /fasttest/runner/run_case.py: -------------------------------------------------------------------------------- 1 | from fasttest.common import Var 2 | from fasttest.runner.test_case import TestCase 3 | from fasttest.drivers.driver_base_app import DriverBaseApp 4 | from fasttest.drivers.driver_base_web import DriverBaseWeb 5 | from fasttest.runner.case_analysis import CaseAnalysis 6 | 7 | 8 | class RunCase(TestCase): 9 | 10 | def setUp(self): 11 | if self.skip: 12 | self.skipTest('skip') 13 | if Var.re_start: 14 | if Var.driver != 'selenium': 15 | DriverBaseApp.launch_app(None) 16 | else: 17 | DriverBaseWeb.createSession() 18 | 19 | def testCase(self): 20 | case = CaseAnalysis() 21 | case.iteration(self.steps) 22 | 23 | def tearDown(self): 24 | if Var.re_start: 25 | try: 26 | if Var.driver != 'selenium': 27 | DriverBaseApp.close_app(None) 28 | else: 29 | DriverBaseWeb.quit() 30 | except: 31 | pass 32 | -------------------------------------------------------------------------------- /fasttest/runner/test_case.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import unittest 6 | import traceback 7 | from fasttest.common import * 8 | 9 | 10 | class TestCase(unittest.TestCase): 11 | def __getattr__(self, item): 12 | try: 13 | return self.__getattribute__(item) 14 | except: 15 | attrvalue = None 16 | self.__setattr__(item, attrvalue) 17 | return attrvalue 18 | 19 | def __init__(self, methodName="runTest"): 20 | super(TestCase, self).__init__(methodName) 21 | for key, value in Var.case_info.items(): 22 | setattr(self, key, value) 23 | self.snapshot_dir = os.path.join(Var.report,'Steps', self.module, self.test_case_path.split('/')[-1].split(os.sep)[-1].split(".")[0]) 24 | self.report = Var.report 25 | 26 | def run(self, result=None): 27 | 28 | try: 29 | Var.case_step_index = 0 30 | Var.case_snapshot_index = 0 31 | Var.snapshot_dir = self.snapshot_dir 32 | testcase_steps = [] 33 | if not os.path.exists(Var.snapshot_dir): 34 | os.makedirs(Var.snapshot_dir) 35 | with open(self.test_case_path, 'r', encoding='UTF-8') as r: 36 | s = r.readlines() 37 | index = s.index('steps:\n') 38 | for step in s[index+1:]: 39 | if not (step.lstrip().startswith('#') or re.match('#', step.lstrip().lstrip('-').lstrip())): 40 | if step != '\n': 41 | testcase_steps.append(step) 42 | log_info("******************* TestCase {} Start *******************".format(self.description)) 43 | unittest.TestCase.run(self, result) 44 | log_info("******************* Total: {}, Success: {}, Failed: {}, Error: {}, Skipped: {} ********************\n" 45 | .format(result.testsRun, len(result.successes), len(result.failures), len(result.errors), len(result.skipped))) 46 | except: 47 | traceback.print_exc() -------------------------------------------------------------------------------- /fasttest/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from fasttest.utils.yaml_utils import analytical_file 4 | from fasttest.utils.devices_utils import DevicesUtils 5 | from fasttest.utils.opcv_utils import OpencvUtils 6 | from fasttest.utils.server_utils_app import ServerUtilsApp 7 | from fasttest.utils.server_utils_web import ServerUtilsWeb 8 | from fasttest.utils.testcast_utils import TestCaseUtils 9 | 10 | __all__ = ['analytical_file', 'DevicesUtils', 'OpencvUtils', 'ServerUtilsApp', 'ServerUtilsWeb', 'TestCaseUtils'] -------------------------------------------------------------------------------- /fasttest/utils/devices_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import platform 6 | 7 | 8 | class DevicesUtils(object): 9 | 10 | def __init__(self, platformName, udid): 11 | self._platformName = platformName 12 | self._udid = udid 13 | 14 | def device_info(self): 15 | 16 | if self._platformName.lower() == 'android': 17 | devices = self.get_devices() 18 | if self._udid and (self._udid not in devices): 19 | raise Exception("device {} not found!".format(self._udid)) 20 | elif not self._udid and devices: 21 | self._udid = devices[0] 22 | elif not self._udid: 23 | raise Exception("Can‘t find device!") 24 | 25 | if platform.system() == "Windows": 26 | pipe = os.popen("adb -s {} shell getprop | findstr product".format(self._udid)) 27 | else: 28 | pipe = os.popen("adb -s {} shell getprop | grep product".format(self._udid)) 29 | result = pipe.read() 30 | manufacturer = "None" if not result else \ 31 | re.search(r"\[ro.product.manufacturer\]:\s*\[(.[^\]]*)\]", result).groups()[0] 32 | model = "None" if not result else \ 33 | re.search(r"\[ro.product.model\]:\s*\[(.[^\]]*)\]", result).groups()[0].split()[-1] 34 | device_type = "{}_{}".format(manufacturer, model).replace(" ", "_") 35 | elif self._platformName.lower() == 'ios': 36 | devices = self.get_devices('idevice_id -l') 37 | simulator_devices = self.get_devices('instruments -s Devices') 38 | if self._udid and (self._udid not in (devices or simulator_devices)): 39 | raise Exception("device {} not found!".format(self._udid)) 40 | elif not self._udid and devices: 41 | self._udid = devices[0] 42 | elif not self._udid: 43 | raise Exception("Can‘t find device!") 44 | 45 | if self._udid in devices: 46 | DeviceName = os.popen('ideviceinfo -u {} -k DeviceName'.format(self._udid)).read() 47 | if not DeviceName: 48 | DeviceName = 'iOS' 49 | device_type = DeviceName.replace(' ', '_') 50 | else: 51 | device_type = self._platformName 52 | else: 53 | raise Exception("Test Platform must be Android or iOS!") 54 | 55 | return self._udid,device_type 56 | 57 | def get_devices(self,cmd=''): 58 | if self._platformName.lower() == 'android': 59 | pipe = os.popen("adb devices") 60 | deviceinfo = pipe.read() 61 | devices = deviceinfo.replace('\tdevice', "").split('\n') 62 | devices.pop(0) 63 | while "" in devices: 64 | devices.remove("") 65 | else: 66 | pipe = os.popen(cmd) 67 | deviceinfo = pipe.read() 68 | r = re.compile(r'\[(.*?)\]', re.S) 69 | devices = re.findall(r, deviceinfo) 70 | devices = devices if devices else deviceinfo.split('\n') 71 | while "" in devices: 72 | devices.remove("") 73 | 74 | return devices 75 | -------------------------------------------------------------------------------- /fasttest/utils/opcv_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | try: 5 | import cv2 6 | except: 7 | pass 8 | 9 | class OpencvUtils(object): 10 | 11 | def __init__(self,baseimage, matchimage, height): 12 | 13 | self.baseimage = baseimage 14 | self.matchimage = matchimage 15 | self.height = height 16 | self.iszoom = False 17 | 18 | def extract_minutiae(self): 19 | """ 20 | 提取特征点 21 | :return: 22 | """ 23 | if os.path.exists(self.matchimage): 24 | self.baseimage = cv2.imread(self.baseimage) 25 | # self.baseimage = cv2.resize(self.baseimage, dsize=(int(self.baseimage.shape[1] / 2), int(self.baseimage.shape[0] / 2))) 26 | self.matchimage = cv2.imread(self.matchimage) 27 | 28 | view_height = self.height 29 | image_height = self.baseimage.shape[0] 30 | if view_height * 2 == image_height: 31 | self.iszoom = True 32 | 33 | else: 34 | raise FileExistsError(self.matchimage) 35 | 36 | # 创建一个SURF对象 37 | surf = cv2.xfeatures2d.SURF_create(1000) 38 | 39 | # SIFT对象会使用Hessian算法检测关键点,并且对每个关键点周围的区域计算特征向量。该函数返回关键点的信息和描述符 40 | keypoints1, descriptor1 = surf.detectAndCompute(self.baseimage, None) 41 | keypoints2, descriptor2 = surf.detectAndCompute(self.matchimage, None) 42 | 43 | if descriptor2 is None: 44 | return None 45 | 46 | # 特征点匹配 47 | matcher = cv2.FlannBasedMatcher() 48 | matchePoints = matcher.match(descriptor1, descriptor2) 49 | 50 | # #提取强匹配特征点 51 | minMatch = 1 52 | maxMatch = 0 53 | for i in range(len(matchePoints)): 54 | if minMatch > matchePoints[i].distance: 55 | minMatch = matchePoints[i].distance 56 | if maxMatch < matchePoints[i].distance: 57 | maxMatch = matchePoints[i].distance 58 | if minMatch > 0.2: 59 | return None 60 | # #获取排列在前边的几个最优匹配结果 61 | DMatch = None 62 | MatchePoints = [] 63 | for i in range(len(matchePoints)): 64 | if matchePoints[i].distance == minMatch: 65 | try: 66 | keypoint = keypoints1[matchePoints[i].queryIdx] 67 | x, y = keypoint.pt 68 | if self.iszoom: 69 | x = x / 2.0 70 | y = y / 2.0 71 | keypoints1 = [keypoint] 72 | 73 | dmatch = matchePoints[i] 74 | dmatch.queryIdx = 0 75 | MatchePoints.append(dmatch) 76 | except: 77 | continue 78 | 79 | # 绘制最优匹配点 80 | outImg = None 81 | outImg = cv2.drawMatches(self.baseimage, keypoints1, self.matchimage, keypoints2, MatchePoints, outImg, matchColor=(0, 255, 0), 82 | flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT) 83 | # cv2.imwrite("outimg.png", outImg) 84 | 85 | matchinfo = { 86 | 'x':int(x), 87 | 'y':int(y), 88 | 'ocrimg':outImg 89 | } 90 | return matchinfo 91 | 92 | -------------------------------------------------------------------------------- /fasttest/utils/server_utils_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import time 6 | import random 7 | import platform 8 | import threading 9 | import subprocess 10 | from fasttest.common import * 11 | 12 | 13 | class ServerUtilsApp(object): 14 | 15 | def __getattr__(self, item): 16 | try: 17 | return self.__getattribute__(item) 18 | except: 19 | return None 20 | 21 | def __init__(self, desired_capabilities): 22 | 23 | self.instance = None 24 | self.driver = desired_capabilities.driver 25 | self.time_out = desired_capabilities.timeOut 26 | self.url = 'http://127.0.0.1' 27 | self.desired_capabilities = self._check_desired_capabilities(desired_capabilities.desired) 28 | self.port = self._get_device_port() 29 | self.browser = desired_capabilities.browser 30 | 31 | def start_server(self): 32 | 33 | try: 34 | log_info('Start the server...') 35 | self.stop_server() 36 | if self.driver == 'appium': 37 | bp_port = self._get_device_port() 38 | wda_port = self._get_device_port() 39 | udid = self.desired_capabilities['udid'] 40 | p = f'appium ' \ 41 | f'-a 127.0.0.1 ' \ 42 | f'-p {self.port} ' \ 43 | f'-U {udid} ' \ 44 | f'-bp {bp_port} ' \ 45 | f'--webdriveragent-port {wda_port} ' \ 46 | f'--session-override ' \ 47 | f'--log-level info' 48 | self.pipe = subprocess.Popen(p, stdout=subprocess.PIPE, shell=True) 49 | thread = threading.Thread(target=self._print_appium_log) 50 | thread.start() 51 | time.sleep(5) 52 | 53 | from appium import webdriver 54 | self.instance = webdriver.Remote(command_executor='{}:{}/wd/hub'.format(self.url, self.port), 55 | desired_capabilities=self.desired_capabilities) 56 | self.instance.implicitly_wait(int(self.time_out)) 57 | else: 58 | ob = subprocess.Popen('macaca server -p {}'.format(self.port), stdout=subprocess.PIPE, shell=True) 59 | for out_ in ob.stdout: 60 | out_ = str(out_, encoding='utf-8') 61 | log_info(out_.strip()) 62 | if 'Macaca server started' in out_: break 63 | 64 | from macaca import WebDriver 65 | self.instance = WebDriver(url='{}:{}/wd/hub'.format(self.url, self.port), 66 | desired_capabilities=self.desired_capabilities) 67 | self.instance.init() 68 | 69 | return self.instance 70 | 71 | except Exception as e: 72 | log_error('Unable to connect to the server, please reconnect!', False) 73 | if self.platformName.lower() == "android": 74 | if self.driver == 'macaca': 75 | os.system('adb uninstall io.appium.uiautomator2.server') 76 | os.system('adb uninstall io.appium.uiautomator2.server.test') 77 | else: 78 | os.system('adb uninstall com.macaca.android.testing') 79 | os.system('adb uninstall com.macaca.android.testing.test') 80 | os.system('adb uninstall xdf.android_unlock') 81 | self.stop_server() 82 | raise e 83 | 84 | def stop_server(self): 85 | 86 | try: 87 | if self.platformName.lower() == "android": 88 | os.system('adb -s {} shell am force-stop {}'.format(self.udid, 89 | self.package if self.package else self.appPackage)) 90 | elif self.platformName.lower() == "ios": 91 | pass 92 | 93 | try: 94 | self.instance.quit() 95 | except: 96 | pass 97 | 98 | if self.port is not None: 99 | result, pid = self._check_port_is_used(self.port) 100 | if result: 101 | p = platform.system() 102 | if p == "Windows": 103 | sys_command = "taskkill /pid %s -t -f" % pid 104 | info = subprocess.check_output(sys_command) 105 | log_info(str(info, encoding='GB2312')) 106 | elif p == "Darwin" or p == "Linux": 107 | sys_command = "kill -9 %s" % pid 108 | os.system(sys_command) 109 | except Exception as e: 110 | raise e 111 | 112 | def _check_desired_capabilities(self, desired_capabilities): 113 | desired_capabilities_dict = {} 114 | for key, value in desired_capabilities.items(): 115 | if self.driver == 'appium': 116 | if key in ['package', 'appPackage']: 117 | key = 'appPackage' 118 | elif key in ['activity', 'appActivity']: 119 | key = 'appActivity' 120 | else: 121 | if key in ['package', 'appPackage']: 122 | key = 'package' 123 | elif key in ['activity', 'appActivity']: 124 | key = 'activity' 125 | if isinstance(value, Dict): 126 | value_dict = {} 127 | for key_, value_ in value.items(): 128 | value_dict[key_] = value_ 129 | value = value_dict 130 | desired_capabilities_dict[key] = value 131 | log_info(' {}: {}'.format(key, value)) 132 | object.__setattr__(self, key, value) 133 | return desired_capabilities_dict 134 | 135 | def _check_port_is_used(self, port): 136 | 137 | p = platform.system() 138 | if p == 'Windows': 139 | sys_command = "netstat -ano|findstr %s" % port 140 | pipe = subprocess.Popen(sys_command, stdout=subprocess.PIPE, shell=True) 141 | out, error = pipe.communicate() 142 | if str(out, encoding='utf-8') != "" and "LISTENING" in str(out, encoding='utf-8'): 143 | pid = re.search(r"\s+LISTENING\s+(\d+)\r\n", str(out, encoding='utf-8')).groups()[0] 144 | return True, pid 145 | else: 146 | return False, None 147 | elif p == 'Darwin' or p == 'Linux': 148 | sys_command = "lsof -i:%s" % port 149 | pipe = subprocess.Popen(sys_command, stdout=subprocess.PIPE, shell=True) 150 | for line in pipe.stdout.readlines(): 151 | if "LISTEN" in str(line, encoding='utf-8'): 152 | pid = str(line, encoding='utf-8').split()[1] 153 | return True, pid 154 | return False, None 155 | else: 156 | log_error('The platform is {} ,this platform is not support.'.format(p)) 157 | 158 | def _get_device_port(self): 159 | 160 | for i in range(10): 161 | port = random.randint(3456, 9999) 162 | result, pid = self._check_port_is_used(port) 163 | if result: 164 | continue 165 | else: 166 | log_info('get port return {}'.format(port)) 167 | return port 168 | return 3456 169 | 170 | def _print_appium_log(self): 171 | 172 | log_tag = False 173 | while True: 174 | out = self.pipe.stdout.readline() 175 | out = str(out, encoding='utf-8').strip() 176 | if 'Appium REST http interface' in out: 177 | log_tag = True 178 | log_info(out) 179 | elif out: 180 | if not log_tag: 181 | log_info(out) 182 | else: 183 | break 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /fasttest/utils/server_utils_web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from selenium import webdriver 5 | from fasttest.common import * 6 | 7 | 8 | 9 | class ServerUtilsWeb(object): 10 | 11 | def __getattr__(self, item): 12 | try: 13 | return self.__getattribute__(item) 14 | except: 15 | return None 16 | 17 | def __init__(self, desired_capabilities): 18 | 19 | self.instance = None 20 | self.driver = desired_capabilities.driver 21 | self.time_out = desired_capabilities.timeOut 22 | self.desired_capabilities = desired_capabilities.desired 23 | self.index = desired_capabilities.index 24 | self.root = desired_capabilities.root 25 | self.browser = self.desired_capabilities.browser 26 | self.max_window = self.desired_capabilities.maxWindow 27 | # hub url 28 | remote_url = self.desired_capabilities.remoteUrl 29 | if remote_url and isinstance(remote_url, str): 30 | self.remote_url = remote_url if self.index == 0 else None 31 | elif remote_url and isinstance(remote_url, list): 32 | self.remote_url = remote_url[self.index] if self.index < len(remote_url) else None 33 | else: 34 | self.remote_url = None 35 | # driver path 36 | if self.desired_capabilities[self.browser] and 'driver' in self.desired_capabilities[self.browser].keys(): 37 | self.driver_path = self.desired_capabilities[self.browser]['driver'] 38 | if not os.path.isfile(self.driver_path): 39 | self.driver_path = os.path.join(self.root, self.driver_path) 40 | else: 41 | self.driver_path = None 42 | # options 43 | if self.desired_capabilities[self.browser] and 'options' in self.desired_capabilities[self.browser].keys(): 44 | self.options = self.desired_capabilities[self.browser]['options'] 45 | else: 46 | self.options = None 47 | 48 | def start_server(self): 49 | 50 | try: 51 | if self.browser == 'chrome': 52 | options = webdriver.ChromeOptions() 53 | if self.options: 54 | for opt in self.options: 55 | options.add_argument(opt) 56 | if self.remote_url: 57 | self.instance = webdriver.Remote(command_executor=self.remote_url, 58 | desired_capabilities={ 59 | 'platform': 'ANY', 60 | 'browserName': self.browser, 61 | 'version': '', 62 | 'javascriptEnabled': True 63 | }, 64 | options=options) 65 | else: 66 | if self.driver_path: 67 | self.instance = webdriver.Chrome(executable_path=self.driver_path, 68 | chrome_options=options) 69 | else: 70 | self.instance = webdriver.Chrome(chrome_options=options) 71 | elif self.browser == 'firefox': 72 | options = webdriver.FirefoxOptions() 73 | if self.options: 74 | for opt in self.options: 75 | options.add_argument(opt) 76 | if self.remote_url: 77 | self.instance = webdriver.Remote(command_executor=self.remote_url, 78 | desired_capabilities={ 79 | 'platform': 'ANY', 80 | 'browserName': self.browser, 81 | 'version': '', 82 | 'javascriptEnabled': True 83 | }, 84 | options=options) 85 | else: 86 | if self.driver_path: 87 | self.instance = webdriver.Firefox(executable_path=self.driver_path, 88 | firefox_options=options) 89 | else: 90 | self.instance = webdriver.Firefox(firefox_options=options) 91 | elif self.browser == 'edge': 92 | if self.driver_path: 93 | self.instance = webdriver.Edge(executable_path=self.driver_path) 94 | else: 95 | self.instance = webdriver.Edge() 96 | elif self.browser == 'safari': 97 | self.instance = webdriver.Safari() 98 | elif self.browser == 'ie': 99 | if self.driver_path: 100 | self.instance = webdriver.Ie(executable_path=self.driver_path) 101 | else: 102 | self.instance = webdriver.Ie() 103 | elif self.browser == 'opera': 104 | if self.driver_path: 105 | self.instance = webdriver.Opera(executable_path=self.driver_path) 106 | else: 107 | self.instance = webdriver.Opera() 108 | elif self.browser == 'phantomjs': 109 | if self.driver_path: 110 | self.instance = webdriver.PhantomJS(executable_path=self.driver_path) 111 | else: 112 | self.instance = webdriver.PhantomJS() 113 | 114 | if self.max_window: 115 | self.instance.maximize_window() 116 | return self.instance 117 | except Exception as e: 118 | raise e 119 | 120 | def stop_server(self, instance): 121 | 122 | try: 123 | instance.quit() 124 | except: 125 | pass 126 | 127 | -------------------------------------------------------------------------------- /fasttest/utils/testcast_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from fasttest.common.log import log_error 5 | 6 | class TestCaseUtils(object): 7 | 8 | def __init__(self): 9 | self._testcase_list = [] 10 | 11 | def _traversal_dir(self,path): 12 | for rt, dirs, files in os.walk(path): 13 | files.sort() 14 | for f in files: 15 | file_path = os.path.join(rt, f) 16 | if os.path.isfile(file_path): 17 | if not file_path.endswith('.yaml'): 18 | continue 19 | self._testcase_list.append(file_path) 20 | else: 21 | log_error(' No such file or directory: {}'.format(path), False) 22 | 23 | def test_case_path(self,dirname,paths): 24 | if not paths: 25 | raise Exception('test case is empty.') 26 | for path in paths: 27 | file_path = os.path.join(dirname,path) 28 | if os.path.isdir(file_path): 29 | self._traversal_dir(os.path.join(dirname, path)) 30 | elif os.path.isfile(file_path): 31 | if not file_path.endswith('.yaml'): 32 | continue 33 | self._testcase_list.append(file_path) 34 | else: 35 | log_error(' No such file or directory: {}'.format(path), False) 36 | if not self._testcase_list: 37 | raise Exception('test case is empty.') 38 | return self._testcase_list -------------------------------------------------------------------------------- /fasttest/utils/yaml_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import yaml 4 | from fasttest.common import Dict 5 | 6 | def analytical_file(path): 7 | ''' 8 | analytical file 9 | :param path: 10 | :return: 11 | ''' 12 | with open(path, "r", encoding='utf-8') as f: 13 | yaml_data = yaml.load(f, Loader=yaml.FullLoader) 14 | yaml_dict = Dict() 15 | if yaml_data: 16 | for key, value in yaml_data.items(): 17 | yaml_dict[key] = value 18 | return yaml_dict 19 | -------------------------------------------------------------------------------- /fasttest/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | VERSION = '1.0.0' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=5.1.2 2 | wd>=1.0.1 3 | opencv-contrib-python==3.4.2.16 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ############################################# 5 | # File Name: setup.py 6 | # Author: IMJIE 7 | # Email: imjie@outlook.com 8 | # Created Time: 2020-1-29 9 | ############################################# 10 | 11 | import sys 12 | import setuptools 13 | 14 | with open("README.md", "r", encoding='UTF-8') as fh: 15 | long_description = fh.read() 16 | 17 | info = sys.version_info 18 | if info.major == 3 and info.minor <= 7: 19 | requires = [ 20 | 'PyYAML>=5.1.2', 21 | 'wd>=1.0.1', 22 | 'selenium', 23 | 'colorama', 24 | 'opencv-contrib-python==3.4.2.16' 25 | ] 26 | else: 27 | requires = [ 28 | 'PyYAML>=5.1.2', 29 | 'wd>=1.0.1', 30 | 'selenium', 31 | 'colorama', 32 | 'opencv-contrib-python' 33 | ] 34 | setuptools.setup( 35 | name="fasttest", 36 | version="1.0.1", 37 | author="IMJIE", 38 | author_email="imjie@outlook.com", 39 | keywords=('macaca', 'appium', 'selenium', 'APP自动化', 'WEB自动化', '关键字驱动'), 40 | description="关键字驱动自动化框架", 41 | long_description=long_description, 42 | long_description_content_type="text/markdown", 43 | url="https://github.com/Jodeee/fasttest", 44 | packages=setuptools.find_packages(), 45 | include_package_data=True, 46 | package_data={'fasttest/result':['resource/*']}, 47 | classifiers=[ 48 | "Programming Language :: Python :: 3", 49 | "License :: OSI Approved :: MIT License", 50 | ], 51 | python_requires='>=3.6', 52 | install_requires=requires, 53 | entry_points={ 54 | 'console_scripts':[ 55 | 'fasttest = fasttest.fasttest_runner:main' 56 | ] 57 | } 58 | ) --------------------------------------------------------------------------------