├── .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 | 
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 | 
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 | CaseName |
94 | Description |
95 | StartTime |
96 | Duration |
97 | Status |
98 | Open/Close |
99 |
100 | {trlist}
101 |
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 |
163 |

164 |
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 |
184 |

185 |
{errlist}
186 |
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 | )
--------------------------------------------------------------------------------