├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── doc └── img │ └── logo-simple-pocounit.png ├── pocounit ├── __init__.py ├── addons │ ├── __init__.py │ ├── airtest │ │ ├── __init__.py │ │ ├── action_tracking.py │ │ ├── polyfill.py │ │ └── utils.py │ ├── hunter │ │ ├── __init__.py │ │ └── runtime_logging.py │ └── poco │ │ ├── __init__.py │ │ ├── action_tracking.py │ │ └── capturing.py ├── assets_manager.py ├── case.py ├── fixture.py ├── result │ ├── __init__.py │ ├── action.py │ ├── app_runtime.py │ ├── assertion.py │ ├── collector.py │ ├── emitter.py │ ├── logger.py │ ├── metainfo.py │ ├── record.py │ ├── runner_runtime.py │ ├── site_snapshot.py │ └── trace.py ├── runner.py ├── suite.py └── utils │ ├── __init__.py │ ├── misc.py │ ├── outcome.py │ └── trace.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── poco_uiautomation_android ├── __init__.py └── simple.py └── test_runner.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | 5 | # Packages 6 | *.egg 7 | *.egg-info 8 | dist 9 | build 10 | eggs 11 | parts 12 | bin 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | tmp/ 37 | log/ 38 | *.log 39 | _site 40 | apps 41 | _build/ 42 | *.spec 43 | core/ios/certificate_config.py 44 | htmlcov/ 45 | cover/ 46 | 47 | .idea/ 48 | 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | group: travis_latest 2 | language: python 3 | cache: pip 4 | python: 5 | - 2.7 6 | - 3.6 7 | #- nightly 8 | #- pypy 9 | #- pypy3 10 | matrix: 11 | allow_failures: 12 | - python: nightly 13 | - python: pypy 14 | - python: pypy3 15 | install: 16 | - pip install -r requirements.txt 17 | - pip install flake8 # pytest # add another testing frameworks later 18 | before_script: 19 | # stop the build if there are Python syntax errors or undefined names 20 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 21 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 22 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 23 | script: 24 | - true # pytest --capture=sys # add other tests here 25 | notifications: 26 | on_success: change 27 | on_failure: change # `always` will be the setting once code changes slow down 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. raw:: html 3 | 4 |
5 | pocounit 6 |
7 | 8 | PocoUnit (unittest framework for poco) 9 | ====================================== 10 | 11 | 如需构建自动化工程项目,请直接使用 `my-testflow`_ 12 | 13 | 可配合airtest和poco使用的单元测试框架。规范了脚本编写的格式,提供流式日志(stream log)记录服务,然后可以使用 `PocoResultPlayer`_ 将运行的内容回放。 14 | 15 | Installation 16 | ------------ 17 | 18 | .. code-block:: bash 19 | 20 | pip install pocounit 21 | 22 | 23 | 用法 24 | ---- 25 | 26 | 首先需要继承基类PocoTestCase实现项目组自己的MyBaseTestCase,在MyBaseTestCase预处理中将需要用到的对象准备好(包括实例化hunter和poco和动作捕捉),以后在其余用例中继承MyBaseTestCase即可。 27 | 28 | 基本用法可参考一下代码模板。 29 | 30 | .. code-block:: python 31 | 32 | # coding=utf-8 33 | 34 | from pocounit.case import PocoTestCase 35 | from pocounit.addons.poco.action_tracking import ActionTracker 36 | 37 | from poco.drivers.unity3d import UnityPoco 38 | 39 | 40 | class MyBaseTestCase(PocoTestCase): 41 | @classmethod 42 | def setUpClass(cls): 43 | super(MyBaseTestCase, cls).setUpClass() 44 | cls.poco = UnityPoco() 45 | 46 | # 启用动作捕捉(action tracker) 47 | action_tracker = ActionTracker(cls.poco) 48 | cls.register_addon(action_tracker) 49 | 50 | 51 | 然后可以开始编写自己的testcase 52 | 53 | .. code-block:: python 54 | 55 | # coding=utf8 56 | 57 | from ... import MyBaseTestCase 58 | 59 | 60 | # 一个文件里建议就只有一个TestCase 61 | # 一个Case做的事情尽量简单,不要把一大串操作都放到一起 62 | class MyTestCase(MyBaseTestCase): 63 | def setUp(self): 64 | # 可以调用一些前置条件指令和预处理指令 65 | pass 66 | 67 | # 函数名就是这个,用其他名字无效 68 | def runTest(self): 69 | # 普通语句跟原来一样 70 | self.poco(text='角色').click() 71 | 72 | # 断言语句跟python unittest写法一模一样 73 | self.assertTrue(self.poco(text='最大生命').wait(3).exists(), "看到了最大生命") 74 | 75 | self.poco('btn_close').click() 76 | self.poco('movetouch_panel').offspring('point_img').swipe('up') 77 | 78 | def tearDown(self): 79 | # 如果没有清场操作,这个函数就不用写出来 80 | pass 81 | 82 | # 不要写以test开头的函数,除非你知道会发生什么 83 | # def test_xxx(): 84 | # pass 85 | 86 | 87 | if __name__ in '__main__': 88 | import pocounit 89 | pocounit.main() 90 | 91 | 92 | .. _my-testflow: https://github.com/AirtestProject/my-testflow 93 | .. _PocoResultPlayer: http://poco.readthedocs.io/en/latest/source/doc/about-test-result-player.html 94 | -------------------------------------------------------------------------------- /doc/img/logo-simple-pocounit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/doc/img/logo-simple-pocounit.png -------------------------------------------------------------------------------- /pocounit/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | 4 | import os 5 | import inspect 6 | 7 | from pocounit.case import PocoTestCase 8 | from pocounit.runner import PocoTestRunner 9 | from pocounit.suite import PocoTestSuite 10 | 11 | from pocounit.utils.misc import has_override 12 | 13 | 14 | def run(suite): 15 | if isinstance(suite, PocoTestCase): 16 | case = suite 17 | suite = PocoTestSuite() 18 | suite.addTest(case) 19 | runner = PocoTestRunner() 20 | return runner.run(suite) 21 | 22 | 23 | def main(): 24 | # testcase detection 25 | current_frame = inspect.currentframe() 26 | caller = current_frame.f_back 27 | test_case_filename = os.path.abspath(caller.f_code.co_filename) # 脚本务必是绝对路径才行 28 | caller_scope = caller.f_locals 29 | print('this testcase filename is "{}".'.format(test_case_filename)) 30 | 31 | # 这部分代码放到loader里 32 | Cases = [v for k, v in caller_scope.items() 33 | if type(v) is type 34 | and v is not PocoTestCase 35 | and issubclass(v, PocoTestCase) 36 | and has_override("runTest", v, PocoTestCase) 37 | ] 38 | 39 | suite = PocoTestSuite() 40 | for Case in Cases: 41 | case = Case() 42 | suite.addTest(case) 43 | 44 | runner = PocoTestRunner() 45 | result = runner.run(suite) 46 | 47 | if not result.wasSuccessful(): 48 | exit(-1) 49 | 50 | -------------------------------------------------------------------------------- /pocounit/addons/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class PocoUnitAddon(object): 5 | def initialize(self, Case): 6 | pass 7 | -------------------------------------------------------------------------------- /pocounit/addons/airtest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/pocounit/addons/airtest/__init__.py -------------------------------------------------------------------------------- /pocounit/addons/airtest/action_tracking.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ 4 | 这个文件别引用,未完成 5 | """ 6 | 7 | import inspect 8 | import sys 9 | import traceback 10 | 11 | from pocounit.addons import PocoUnitAddon 12 | from pocounit.addons.airtest.utils import Hooker 13 | 14 | from airtest.core.api import loop_find, touch, swipe, exists, assert_exists 15 | from airtest.core.cv import _cv_match 16 | from airtest.core.api import Template as Target 17 | from airtest.cli.runner import device as current_device 18 | 19 | 20 | class ActionTracker(PocoUnitAddon): 21 | """ 22 | general airtest action pattern and invocation stack 23 | 24 | | touch start 25 | | loop find 26 | | cv match, return match result 27 | | touch end 28 | 29 | """ 30 | 31 | def __init__(self): 32 | self.action_recorder = None 33 | self.assertion_recorder = None 34 | self._doing_actions = False # 在touch或swipe中时设为true 35 | 36 | self.loop_find_ = Hooker(loop_find) 37 | self.touch_ = Hooker(touch) 38 | self._cv_match_ = Hooker(_cv_match) 39 | self.assert_exists_ = Hooker(assert_exists) 40 | 41 | self.touch_.pre(self.pre_touch) 42 | self.touch_.post(self.post_touch) 43 | self.loop_find_.post(self.post_loop_find) 44 | self._cv_match_.post(self.post_cv_match) 45 | self.assert_exists_.pre(self.pre_assert_exists) 46 | self.assert_exists_.post(self.post_assert_exists) 47 | 48 | import airtest.core.main as acm 49 | import airtest.core.cv as acc 50 | acm.touch = self.touch_ 51 | acm.loop_find = self.loop_find_ 52 | acm.assert_exists = self.assert_exists_ 53 | acc._cv_match = self._cv_match_ 54 | 55 | def initialize(self, Case): 56 | self.action_recorder = Case.get_result_emitter('actionRecorder') 57 | self.assertion_recorder = Case.get_result_emitter('assertionRecorder') 58 | 59 | frame = sys._getframe(0) 60 | while frame.f_back is not None: 61 | frame.f_globals['touch'] = self.touch_ 62 | frame.f_globals['loop_find'] = self.loop_find_ 63 | frame.f_globals['assert_exists'] = self.assert_exists_ 64 | frame = frame.f_back 65 | 66 | def post_loop_find(self, pos, v, *args, **kwargs): 67 | if pos is not None and isinstance(v, Target): 68 | w, h = current_device().getCurrentScreenResolution() 69 | x, y = pos 70 | tid = id(v) 71 | if self._doing_actions: 72 | self.action_recorder.click(tid, [1.0 * x / w, 1.0 * y / h], v.filepath) 73 | 74 | def post_cv_match(self, cv_ret, *args, **kwargs): 75 | if not cv_ret: 76 | return 77 | 78 | # 如果当前函数是由loop_find调用的,那就可以找到一个rect,这个rect由airtest.core.cv._cv_match里给出 79 | # 以下就是从frame stack中一直找到loop_find这一帧,然后找出loop_find的第一个argument,通过argument求出tid 80 | frame = sys._getframe(0) 81 | while frame and frame.f_code.co_name != 'loop_find': 82 | frame = frame.f_back 83 | if frame: 84 | # more details about inspect parameter name in runtime, 85 | # see https://docs.python.org/2/library/inspect.html#inspect.getargvalues 86 | args, varargs, keywords, locals = inspect.getargvalues(frame) 87 | if len(args) > 0: 88 | v_name = args[0] 89 | elif varargs is not None and len(locals[varargs]) > 0: 90 | v_name = locals[varargs][0] 91 | else: 92 | raise ValueError('loop_find第一个参数不支持使用keyword args') 93 | 94 | # 取到loop_find的第一个argument 95 | v = locals[v_name] 96 | tid = id(v) 97 | rect = cv_ret.get("rectangle") 98 | if rect: 99 | # a rect's each vertex in screen as following 100 | # [0] [3] 101 | # [1] [2] 102 | t = rect[0][1] * 1.0 103 | r = rect[3][0] * 1.0 104 | b = rect[1][1] * 1.0 105 | l = rect[0][0] * 1.0 106 | w, h = current_device().getCurrentScreenResolution() 107 | self.action_recorder.bounding(tid, [t / h, r / w, b / h, l / w]) 108 | 109 | def pre_touch(self, v, *args, **kwargs): 110 | self._doing_actions = True 111 | if isinstance(v, (list, tuple)): 112 | tid = id(v) 113 | w, h = current_device().getCurrentScreenResolution() 114 | x, y = v 115 | self.action_recorder.click(tid, [1.0 * x / w, 1.0 * y / h], 'fixed point: {}'.format(v)) 116 | 117 | def post_touch(self, ret, v, *args, **kwargs): 118 | self._doing_actions = False 119 | if isinstance(v, (Target, list, tuple)): 120 | tid = id(v) 121 | self.action_recorder.clear(tid) 122 | return 123 | 124 | def pre_assert_exists(self, v, message, *args, **kwargs): 125 | if isinstance(v, Target): 126 | pass 127 | 128 | def post_assert_exists(self, ret, v, message, *args, **kwargs): 129 | if isinstance(v, Target): 130 | tid = id(v) 131 | self.assertion_recorder.assert_('Existence', [v.filename], message) 132 | self.action_recorder.clear(tid) 133 | -------------------------------------------------------------------------------- /pocounit/addons/airtest/polyfill.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | def preload_airtest_device_context(run_on_win=False, window_title='^.*errors and.*$'): 5 | from airtest.cli.runner import device as current_device 6 | if not current_device(): 7 | from airtest.core.main import set_serialno, set_windows 8 | from airtest.core.settings import Settings 9 | from airtest.core.android.adb import ADB 10 | adb_client = ADB() 11 | available_devices = adb_client.devices('device') 12 | if run_on_win or not available_devices: 13 | Settings.FIND_INSIDE = [8, 30] # 窗口边框偏移 14 | set_windows(window_title=window_title) 15 | else: 16 | set_serialno() 17 | exec("from airtest.core.main import *") in globals() 18 | 19 | 20 | -------------------------------------------------------------------------------- /pocounit/addons/airtest/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | 5 | 6 | class Hooker(object): 7 | def __init__(self, func): 8 | self.pre_func = None 9 | self.hooked_func = func 10 | self.post_func = None 11 | self.exc_info = None 12 | 13 | def __call__(self, *args, **kwargs): 14 | if self.pre_func: 15 | self.pre_func(*args, **kwargs) 16 | 17 | ret = None 18 | try: 19 | ret = self.hooked_func(*args, **kwargs) 20 | except: 21 | self.exc_info = sys.exc_info() 22 | 23 | if self.post_func: 24 | self.post_func(ret, *args, **kwargs) 25 | 26 | if self.exc_info: 27 | raise self.exc_info 28 | else: 29 | return ret 30 | 31 | def pre(self, func): 32 | self.pre_func = func 33 | return func 34 | 35 | def post(self, func): 36 | self.post_func = func 37 | return func 38 | 39 | def __repr__(self): 40 | return '<{}.{} for {} object @+id/0x{}>'.format(self.__module__, self.__class__.__name__, 41 | repr(self.hooked_func), hex(id(self))) 42 | __str__ = __repr__ 43 | -------------------------------------------------------------------------------- /pocounit/addons/hunter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/pocounit/addons/hunter/__init__.py -------------------------------------------------------------------------------- /pocounit/addons/hunter/runtime_logging.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import unicode_literals 3 | 4 | from pocounit.addons import PocoUnitAddon 5 | from hunter_cli.device_output import trace 6 | 7 | 8 | class AppRuntimeLogging(PocoUnitAddon): 9 | def __init__(self, hunter): 10 | self.hunter = hunter 11 | self.logger = None 12 | 13 | def initialize(self, Case): 14 | self.logger = Case.get_result_emitter('appRuntimeLog') 15 | central_server_url = 'http://hunter.nie.netease.com/webterm' 16 | trace(self.hunter.tokenid, self.hunter.device_info['id'], central_server_url, self.on_app_runtime_log) 17 | 18 | def on_app_runtime_log(self, data): 19 | content = '[{data[level]}] {data[data]}\n'.format(data=data) 20 | self.logger.log(content) 21 | -------------------------------------------------------------------------------- /pocounit/addons/poco/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/pocounit/addons/poco/__init__.py -------------------------------------------------------------------------------- /pocounit/addons/poco/action_tracking.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.addons import PocoUnitAddon 4 | 5 | 6 | class ActionTracker(PocoUnitAddon): 7 | def __init__(self, poco): 8 | super(ActionTracker, self).__init__() 9 | self.poco = poco 10 | self.action_recorder = None 11 | 12 | def initialize(self, Case): 13 | self.action_recorder = Case.get_result_emitter('actionRecorder') 14 | self.poco.add_pre_action_callback(self.on_pre_action) 15 | self.poco.add_post_action_callback(self.on_post_action) 16 | 17 | def on_pre_action(self, poco, action, ui, args): 18 | tid = id(ui) 19 | if action in ('click', 'long_click'): 20 | bounds = ui.get_bounds() # t r b l 21 | self.action_recorder.bounding(tid, bounds) 22 | self.action_recorder.click(tid, args, repr(ui)) 23 | elif action == 'swipe': 24 | bounds = ui.get_bounds() 25 | self.action_recorder.bounding(tid, bounds) 26 | origin, direction = args 27 | self.action_recorder.swipe(tid, origin, direction, repr(ui)) 28 | 29 | def on_post_action(self, poco, action, ui, args): 30 | tid = id(ui) 31 | if action in ('click', 'long_click', 'swipe'): 32 | self.action_recorder.clear(tid) 33 | -------------------------------------------------------------------------------- /pocounit/addons/poco/capturing.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.addons import PocoUnitAddon 4 | 5 | 6 | class SiteCaptor(PocoUnitAddon): 7 | def __init__(self, poco): 8 | super(SiteCaptor, self).__init__() 9 | self.poco = poco 10 | self.site_snapshot = None 11 | 12 | def initialize(self, Case): 13 | self.site_snapshot = Case.get_result_emitter('siteSnapshot') 14 | self.site_snapshot.set_poco_instance(self.poco) 15 | 16 | def snapshot(self, site_id): 17 | return self.site_snapshot.snapshot(site_id) 18 | -------------------------------------------------------------------------------- /pocounit/assets_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | 5 | 6 | class AssetsManager(object): 7 | def __init__(self, project_root): 8 | super(AssetsManager, self).__init__() 9 | self.project_root = project_root 10 | 11 | def get_abspath(self, p): 12 | return os.path.join(self.project_root, p) 13 | 14 | get_resource_path = get_abspath 15 | -------------------------------------------------------------------------------- /pocounit/case.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import re 4 | import six 5 | import sys 6 | import traceback 7 | import unittest 8 | import warnings 9 | 10 | from unittest.util import safe_repr 11 | 12 | from pocounit.fixture import FixtureUnit 13 | from pocounit.result import PocoTestResult 14 | from pocounit.result.metainfo import MetaInfo 15 | from pocounit.result.collector import PocoResultCollector 16 | from pocounit.result.trace import ScriptTracer 17 | from pocounit.result.record import ScreenRecorder 18 | from pocounit.result.action import ActionRecorder 19 | from pocounit.result.app_runtime import AppRuntimeLog 20 | from pocounit.result.runner_runtime import RunnerRuntimeLog 21 | from pocounit.result.assertion import AssertionRecorder 22 | from pocounit.result.site_snapshot import SiteSnapshot 23 | 24 | from pocounit.utils.outcome import Outcome 25 | 26 | 27 | SPECIAL_CHARS = re.compile(r'[\/\\\.:*?"<>|]') 28 | 29 | 30 | class _UnexpectedSuccess(Exception): 31 | """ 32 | The test was supposed to fail, but it didn't! 33 | """ 34 | 35 | 36 | class PocoTestCase(unittest.TestCase, FixtureUnit): 37 | """ 38 | * longMessage: determines whether long messages (including repr of 39 | objects used in assert methods) will be printed on failure in *addition* 40 | to any explicit message passed. 41 | """ 42 | longMessage = True 43 | 44 | _resule_collector = None 45 | _result_emitters = None # {name -> PocoTestResultEmitter} 46 | _addons = None # [addons] 47 | 48 | def __init__(self): 49 | unittest.TestCase.__init__(self) 50 | FixtureUnit.__init__(self) 51 | 52 | # check testcase name 53 | self.testcase_name = re.sub(SPECIAL_CHARS, lambda g: '-', self.name()) 54 | 55 | collector = PocoResultCollector(self.project_root, [self.test_case_filename], self.testcase_name, self.test_case_dir) 56 | self.set_result_collector(collector) 57 | 58 | meta_info_emitter = MetaInfo(collector) 59 | runner_runtime_log = RunnerRuntimeLog(collector) 60 | tracer = ScriptTracer(collector) 61 | screen_recorder = ScreenRecorder(collector) 62 | action_recorder = ActionRecorder(collector) 63 | app_runtime_log = AppRuntimeLog(collector) 64 | assertion_recorder = AssertionRecorder(collector) 65 | site_snapshot = SiteSnapshot(collector) 66 | 67 | self.add_result_emitter('metaInfo', meta_info_emitter) 68 | self.add_result_emitter('runnerRuntimeLog', runner_runtime_log) 69 | self.add_result_emitter('tracer', tracer) 70 | self.add_result_emitter('screenRecorder', screen_recorder) 71 | self.add_result_emitter('actionRecorder', action_recorder) 72 | self.add_result_emitter('appRuntimeLog', app_runtime_log) 73 | self.add_result_emitter('assertionRecorder', assertion_recorder) 74 | self.add_result_emitter('siteSnapshot', site_snapshot) 75 | 76 | self.meta_info_emitter = meta_info_emitter 77 | self._exceptions = set() 78 | 79 | @classmethod 80 | def name(cls): 81 | """ 82 | 改写此方法来自定义testcase的名字,允许中文,请使用utf-8编码的str或者unicode 83 | 84 | :return: 85 | """ 86 | 87 | if type(cls) is type: 88 | return cls.__name__ 89 | else: 90 | return cls.__class__.__name__ 91 | 92 | @classmethod 93 | def getMetaInfo(cls): 94 | return {} 95 | 96 | @classmethod 97 | def setUpClass(cls): 98 | """ 99 | 用例预处理,加载自定义插件等 100 | 101 | :return: 102 | """ 103 | pass 104 | 105 | def runTest(self): 106 | """ 107 | testcase的正文,把要执行的测试和包括断言语句都写到这里 108 | 109 | """ 110 | 111 | raise NotImplementedError 112 | 113 | @classmethod 114 | def tearDownClass(cls): 115 | """ 116 | 用例后处理 117 | 118 | :return: 119 | """ 120 | pass 121 | 122 | def shortDescription(self): 123 | """ 124 | 描述这个testcase的细节,如果需要的话就override这个方法 125 | 126 | :return: 127 | """ 128 | 129 | return super(PocoTestCase, self).shortDescription() 130 | 131 | def defaultTestResult(self): 132 | return PocoTestResult() 133 | 134 | @classmethod 135 | def set_result_collector(cls, collector): 136 | if type(cls) is not type: 137 | cls = cls.__class__ 138 | cls._resule_collector = collector 139 | 140 | @classmethod 141 | def get_result_collector(cls): 142 | return cls._resule_collector 143 | 144 | @classmethod 145 | def add_result_emitter(cls, name, emitter): 146 | if type(cls) is not type: 147 | cls = cls.__class__ 148 | if not cls._result_emitters: 149 | cls._result_emitters = {} 150 | cls._result_emitters[name] = emitter 151 | 152 | @classmethod 153 | def get_result_emitter(cls, name): 154 | return cls._result_emitters.get(name) 155 | 156 | @classmethod 157 | def register_addon(cls, addon): 158 | if type(cls) is not type: 159 | cls = cls.__class__ 160 | if not cls._addons: 161 | cls._addons = [] 162 | cls._addons.append(addon) 163 | 164 | # deprecation warning 165 | @classmethod 166 | def register_addin(cls, v): 167 | warnings.warn('`register_addin` is deprecated. Please use `register_addon` instead.') 168 | return cls.register_addon(v) 169 | 170 | def run(self, result=None): 171 | result = result or self.defaultTestResult() 172 | if not isinstance(result, PocoTestResult): 173 | raise TypeError('Test result class should be subclass of PocoTestResult. ' 174 | 'Current test result instance is "{}".'.format(result)) 175 | 176 | # 自动把当前脚本add到collector的脚本watch列表里 177 | collector = self.get_result_collector() 178 | collector.add_testcase_file(self.test_case_filename) 179 | 180 | self.meta_info_emitter.set_testcase_metainfo(self.testcase_name, self.getMetaInfo()) 181 | 182 | # register addon 183 | if not self.__class__._addons: 184 | self.__class__._addons = [] 185 | for addon in self._addons: 186 | addon.initialize(self) 187 | 188 | self.meta_info_emitter.test_started(self.testcase_name) 189 | 190 | # start result emitter 191 | for name, emitter in self._result_emitters.items(): 192 | try: 193 | emitter.start() 194 | except Exception as e: 195 | warnings.warn('Fail to start result emitter: "{}". You can report this error to the developers or just ' 196 | 'ignore it. Error message: \n"{}"' 197 | .format(emitter.__class__.__name__, traceback.format_exc())) 198 | 199 | # run test 200 | # this method never raises 201 | ret = self._super_run_modified(result) 202 | self.record_exceptions(result.errors) 203 | 204 | # stop result emitter 205 | for name, emitter in self._result_emitters.items(): 206 | try: 207 | emitter.stop() 208 | except Exception as e: 209 | warnings.warn('Fail to stop result emitter: "{}". You can report this error to the developers or just ' 210 | 'ignore it. Error message: \n"{}"' 211 | .format(emitter.__class__.__name__, traceback.format_exc())) 212 | 213 | self.meta_info_emitter.test_ended(self.testcase_name) 214 | 215 | # handle result 216 | if result.detail_errors or result.errors or result.failures: 217 | self.meta_info_emitter.test_fail(self.testcase_name) 218 | else: 219 | self.meta_info_emitter.test_succeed(self.testcase_name) 220 | return ret 221 | 222 | def _super_run_modified(self, result=None): 223 | """ 224 | Modify run method in super class. Add some event notification methods. 225 | Note: this method never raises 226 | """ 227 | 228 | orig_result = result 229 | if result is None: 230 | result = self.defaultTestResult() 231 | startTestRun = getattr(result, 'startTestRun', None) 232 | if startTestRun is not None: 233 | startTestRun() 234 | 235 | result.startTest(self) 236 | 237 | testMethod = getattr(self, self._testMethodName) 238 | if (getattr(self.__class__, "__unittest_skip__", False) or 239 | getattr(testMethod, "__unittest_skip__", False)): 240 | # If the class or method was skipped. 241 | try: 242 | skip_why = (getattr(self.__class__, '__unittest_skip_why__', '') 243 | or getattr(testMethod, '__unittest_skip_why__', '')) 244 | self._addSkip(result, self, skip_why) 245 | finally: 246 | result.stopTest(self) 247 | return 248 | expecting_failure_method = getattr(testMethod, 249 | "__unittest_expecting_failure__", False) 250 | expecting_failure_class = getattr(self, 251 | "__unittest_expecting_failure__", False) 252 | expecting_failure = expecting_failure_class or expecting_failure_method 253 | outcome = Outcome(result) 254 | try: 255 | self._outcome = outcome 256 | 257 | with outcome.testPartExecutor(self): 258 | self.setUp() 259 | if outcome.success: 260 | outcome.expecting_failure = expecting_failure 261 | with outcome.testPartExecutor(self, isTest=True): 262 | testMethod() 263 | 264 | # 当前用例失败时触发on_errors回调 265 | if not outcome.success: 266 | with outcome.testPartExecutor(self): 267 | self.on_errors(outcome.errors) 268 | 269 | outcome.expecting_failure = False 270 | with outcome.testPartExecutor(self): 271 | self.tearDown() 272 | 273 | self.doCleanups() 274 | for test, reason in outcome.skipped: 275 | self._addSkip(result, test, reason) 276 | self._feedErrorsToResult(result, outcome.errors) 277 | if outcome.success: 278 | if expecting_failure: 279 | if outcome.expectedFailure: 280 | self._addExpectedFailure(result, outcome.expectedFailure) 281 | else: 282 | self._addUnexpectedSuccess(result) 283 | else: 284 | result.addSuccess(self) 285 | return result 286 | finally: 287 | result.stopTest(self) 288 | if orig_result is None: 289 | stopTestRun = getattr(result, 'stopTestRun', None) 290 | if stopTestRun is not None: 291 | stopTestRun() 292 | 293 | # explicitly break reference cycles: 294 | # outcome.errors -> frame -> outcome -> outcome.errors 295 | # outcome.expectedFailure -> frame -> outcome -> outcome.expectedFailure 296 | del outcome.errors[:] # equivalent to [].clear in py3 297 | outcome.expectedFailure = None 298 | 299 | # clear the outcome, no more needed 300 | self._outcome = None 301 | 302 | def _addSkip(self, result, test_case, reason): 303 | # copy from python3.5.3 unittest.case 304 | addSkip = getattr(result, 'addSkip', None) 305 | if addSkip is not None: 306 | addSkip(test_case, reason) 307 | else: 308 | warnings.warn("TestResult has no addSkip method, skips not reported", 309 | RuntimeWarning, 2) 310 | result.addSuccess(test_case) 311 | 312 | def _feedErrorsToResult(self, result, errors): 313 | # copy from python3.5.3 unittest.case 314 | for test, exc_info in errors: 315 | if exc_info is not None: 316 | if issubclass(exc_info[0], self.failureException): 317 | result.addFailure(test, exc_info) 318 | else: 319 | result.addError(test, exc_info) 320 | 321 | def _addExpectedFailure(self, result, exc_info): 322 | # copy from python3.5.3 unittest.case 323 | try: 324 | addExpectedFailure = result.addExpectedFailure 325 | except AttributeError: 326 | warnings.warn("TestResult has no addExpectedFailure method, reporting as passes", 327 | RuntimeWarning) 328 | result.addSuccess(self) 329 | else: 330 | addExpectedFailure(self, exc_info) 331 | 332 | def _addUnexpectedSuccess(self, result): 333 | # copy from python3.5.3 unittest.case 334 | try: 335 | addUnexpectedSuccess = result.addUnexpectedSuccess 336 | except AttributeError: 337 | warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failure", 338 | RuntimeWarning) 339 | # We need to pass an actual exception and traceback to addFailure, 340 | # otherwise the legacy result can choke. 341 | try: 342 | raise _UnexpectedSuccess 343 | except _UnexpectedSuccess: 344 | result.addFailure(self, sys.exc_info()) 345 | else: 346 | addUnexpectedSuccess(self) 347 | 348 | def on_errors(self, errors): 349 | if len(errors) > 0: 350 | site_snapshot = self.get_result_emitter('siteSnapshot') 351 | site_snapshot.snapshot('testcase_err_{}'.format(errors[0][1])) 352 | 353 | self.record_exceptions(errors) 354 | 355 | def record_exceptions(self, errors): 356 | assertionRecorder = self.get_result_emitter('assertionRecorder') 357 | for case, exc_info in errors: 358 | if not exc_info or exc_info in self._exceptions: 359 | continue 360 | self._exceptions.add(exc_info) 361 | if type(exc_info) is tuple: 362 | exc_type, e, tb = exc_info 363 | assertionRecorder.traceback(exc_type, e, tb) 364 | 365 | def fail(self, fail_msg=None): 366 | e = self.failureException(fail_msg) 367 | errmsg = six.text_type(e) 368 | err_typename = self.failureException.__name__ 369 | assertionRecorder = self.get_result_emitter('assertionRecorder') 370 | assertionRecorder.fail(fail_msg, errmsg, 'Traceback: (failure cause)\n{}{}: {}' 371 | .format(''.join(traceback.format_stack()), err_typename, fail_msg)) 372 | raise super(PocoTestCase, self).fail(fail_msg) 373 | 374 | # assertions 375 | def assertTrue(self, expr, msg=None): 376 | expr = bool(expr) 377 | assertionRecorder = self.get_result_emitter('assertionRecorder') 378 | assertionRecorder.assert_('True', [expr], msg) 379 | 380 | if not expr: 381 | msg = self._formatMessage(msg, "%s is not true" % safe_repr(expr)) 382 | self.fail(msg) 383 | 384 | def assertFalse(self, expr, msg=None): 385 | expr = bool(expr) 386 | assertionRecorder = self.get_result_emitter('assertionRecorder') 387 | assertionRecorder.assert_('False', [expr], msg) 388 | 389 | if expr: 390 | msg = self._formatMessage(msg, "%s is not false" % safe_repr(expr)) 391 | self.fail(msg) 392 | 393 | def assertEqual(self, first, second, msg=None): 394 | assertionRecorder = self.get_result_emitter('assertionRecorder') 395 | assertionRecorder.assert_('Equal', [first, second], msg) 396 | return super(PocoTestCase, self).assertEqual(first, second, msg) 397 | 398 | def assertNotEqual(self, first, second, msg=None): 399 | assertionRecorder = self.get_result_emitter('assertionRecorder') 400 | assertionRecorder.assert_('NotEqual', [first, second], msg) 401 | return super(PocoTestCase, self).assertNotEqual(first, second, msg) 402 | 403 | def assertLess(self, a, b, msg=None): 404 | assertionRecorder = self.get_result_emitter('assertionRecorder') 405 | assertionRecorder.assert_('Less', [a, b], msg) 406 | return super(PocoTestCase, self).assertLess(a, b, msg) 407 | 408 | def assertLessEqual(self, a, b, msg=None): 409 | assertionRecorder = self.get_result_emitter('assertionRecorder') 410 | assertionRecorder.assert_('LessEqual', [a, b], msg) 411 | return super(PocoTestCase, self).assertLessEqual(a, b, msg) 412 | 413 | def assertGreater(self, a, b, msg=None): 414 | assertionRecorder = self.get_result_emitter('assertionRecorder') 415 | assertionRecorder.assert_('Greater', [a, b], msg) 416 | return super(PocoTestCase, self).assertGreater(a, b, msg) 417 | 418 | def assertGreaterEqual(self, a, b, msg=None): 419 | assertionRecorder = self.get_result_emitter('assertionRecorder') 420 | assertionRecorder.assert_('GreaterEqual', [a, b], msg) 421 | return super(PocoTestCase, self).assertGreaterEqual(a, b, msg) 422 | 423 | def assertIn(self, member, container, msg=None): 424 | assertionRecorder = self.get_result_emitter('assertionRecorder') 425 | assertionRecorder.assert_('In', [member, container], msg) 426 | return super(PocoTestCase, self).assertIn(member, container, msg) 427 | 428 | def assertNotIn(self, member, container, msg=None): 429 | assertionRecorder = self.get_result_emitter('assertionRecorder') 430 | assertionRecorder.assert_('NotIn', [member, container], msg) 431 | return super(PocoTestCase, self).assertNotIn(member, container, msg) 432 | -------------------------------------------------------------------------------- /pocounit/fixture.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import inspect 5 | 6 | from pocounit.assets_manager import AssetsManager 7 | from pocounit.utils.misc import get_project_root 8 | 9 | 10 | class FixtureUnit(object): 11 | _assets_manager = None 12 | 13 | def __init__(self): 14 | test_case_filename = os.path.abspath(inspect.getsourcefile(self.__class__)) 15 | test_case_dir = os.path.dirname(test_case_filename) 16 | project_root = get_project_root(test_case_filename) 17 | print('using "{}" as project root. This testcase is "{}"'.format(project_root, self.__class__.__name__)) 18 | print('testcase locates in "{}"'.format(test_case_filename)) 19 | self.set_assets_manager(AssetsManager(project_root)) 20 | 21 | self.test_case_filename = test_case_filename 22 | self.test_case_dir = test_case_dir 23 | self.project_root = project_root 24 | 25 | def setUp(self): 26 | """ 27 | 初始化一个testcase 28 | 不要把测试逻辑放到这里写,setUp报错的话也相当于整个case报错 29 | 30 | """ 31 | 32 | pass 33 | 34 | def tearDown(self): 35 | """ 36 | 一个testcase的清场工作 37 | 38 | """ 39 | 40 | pass 41 | 42 | @classmethod 43 | def set_assets_manager(cls, v): 44 | if type(cls) is not type: 45 | cls = cls.__class__ 46 | cls._assets_manager = v 47 | 48 | @classmethod 49 | def get_assets_manager(cls): 50 | return cls._assets_manager 51 | 52 | @classmethod 53 | def R(cls, respath): 54 | return cls._assets_manager.get_resource_path(respath) 55 | -------------------------------------------------------------------------------- /pocounit/result/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | from unittest.runner import TextTestResult 5 | 6 | 7 | class PocoTestResult(TextTestResult): 8 | def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1): 9 | super(PocoTestResult, self).__init__(stream, descriptions, verbosity) 10 | self.detail_errors = [] 11 | 12 | def addError(self, test, err): 13 | exc_type, e, exc_tb = err 14 | self.detail_errors.append((test, exc_type, e, exc_tb)) 15 | return super(PocoTestResult, self).addError(test, err) 16 | 17 | def addFailure(self, test, err): 18 | exc_type, e, exc_tb = err 19 | self.detail_errors.append((test, exc_type, e, exc_tb)) 20 | return super(PocoTestResult, self).addFailure(test, err) 21 | -------------------------------------------------------------------------------- /pocounit/result/action.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.result.emitter import PocoTestResultEmitter 4 | from pocounit.utils.trace import get_current_lineno_of 5 | 6 | 7 | class ActionRecorder(PocoTestResultEmitter): 8 | TAG = 'action' 9 | 10 | def motion(self, tid, desc, points): 11 | """ 12 | 记录motion event,一个motion event就是一个点的轨迹,比如从A拖动到B,每个motion event使用tid分组,同一个tid分组的会归并在一起, 13 | 多点触控的两组轨迹就必须是相同的tid,调用两次这个方法并传入相同的tid即可 14 | 15 | :param tid: event 分组标号 16 | :param desc: 事件描述 17 | :param points: 点序列,按顺序构成一系列轨迹 18 | :return: None 19 | """ 20 | 21 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames()) 22 | self.emit(self.TAG, {'action': 'click', 'tid': tid, 'targetDescription': desc, 'points': points, 23 | 'lineno': lineno, 'srcfilename': srcfilename}) 24 | 25 | def key(self, tid, keyname): 26 | """ 27 | 记录一个按键事件 28 | 29 | :param tid: 事件编号,配合clear清除 30 | :param keyname: 按键键名,如 A B Enter等 31 | :return: None 32 | """ 33 | 34 | raise NotImplementedError 35 | 36 | def click(self, tid, pos, desc): 37 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames()) 38 | self.emit(self.TAG, {'action': 'click', 'tid': tid, 'origin': pos, 'targetDescription': desc, 39 | 'lineno': lineno, 'srcfilename': srcfilename}) 40 | 41 | def swipe(self, tid, origin, direction, desc): 42 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames()) 43 | self.emit(self.TAG, {'action': 'swipe', 'tid': tid, 'origin': origin, 'direction': direction, 'targetDescription': desc, 44 | 'lineno': lineno, 'srcfilename': srcfilename}) 45 | 46 | def bounding(self, tid, bound): 47 | self.emit(self.TAG, {'action': 'bounding', 'tid': tid, 'bounds': bound}) 48 | 49 | def clear(self, tid): 50 | self.emit(self.TAG, {'action': 'clear', 'tid': tid}) 51 | -------------------------------------------------------------------------------- /pocounit/result/app_runtime.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.result.emitter import PocoTestResultEmitter 4 | 5 | 6 | class AppRuntimeLog(PocoTestResultEmitter): 7 | TAG = 'appRuntimeLog' 8 | 9 | def __init__(self, collector): 10 | super(AppRuntimeLog, self).__init__(collector) 11 | self.started = False 12 | 13 | def start(self): 14 | self.started = True 15 | 16 | def stop(self): 17 | self.started = False 18 | 19 | def log(self, content): 20 | if self.started: 21 | self.emit(self.TAG, {'content': content}) 22 | -------------------------------------------------------------------------------- /pocounit/result/assertion.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import traceback 5 | 6 | from pocounit.result.emitter import PocoTestResultEmitter 7 | from pocounit.utils.trace import get_current_lineno_of 8 | 9 | 10 | class AssertionRecorder(PocoTestResultEmitter): 11 | TAG = 'assertion' 12 | 13 | def assert_(self, assertionType, args, assertionMsg): 14 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames()) 15 | self.emit(self.TAG, {'method': 'assert', 'type': assertionType, 'args': args, 'msg': assertionMsg, 16 | 'lineno': lineno, 'srcfilename': srcfilename}) 17 | 18 | def fail(self, errmsg, assertionMsg, tb): 19 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames()) 20 | self.emit(self.TAG, {'method': 'fail', 'errmsg': errmsg, 'msg': assertionMsg, 'traceback': tb, 21 | 'lineno': lineno, 'srcfilename': srcfilename}) 22 | 23 | def traceback(self, errtype, e, tb): 24 | # tb1 用来获取最近报错调用帧栈 25 | # tb用来format一个完整的tb 26 | # 去到离现场最近的tb位置 27 | tb1 = tb 28 | tb_stack = [tb1] 29 | while tb1.tb_next: 30 | tb1 = tb1.tb_next 31 | tb_stack.append(tb1) 32 | 33 | # 跳过unittest相关的tb frame 34 | while tb and '__unittest' in tb.tb_frame.f_globals: 35 | tb = tb.tb_next 36 | 37 | lineno, srcfilename = None, None 38 | if tb: 39 | while tb_stack: 40 | lineno, srcfilename = get_current_lineno_of(self.collector.get_testcases_filenames(), tb_stack.pop().tb_frame) 41 | if srcfilename: 42 | break 43 | if srcfilename: 44 | srcfilename = os.path.relpath(srcfilename, self.collector.get_project_root_path()) 45 | srcfilename = srcfilename.replace('\\', '/') 46 | formatted_tb = ''.join(traceback.format_exception(errtype, e, tb)) 47 | else: 48 | formatted_tb = '' 49 | self.emit(self.TAG, {'method': 'traceback', 'errtype': errtype.__name__, 'traceback': formatted_tb, 50 | 'lineno': lineno, 'srcfilename': srcfilename}) 51 | -------------------------------------------------------------------------------- /pocounit/result/collector.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import time 5 | import json 6 | 7 | from pocounit.result.logger import StreamLogger 8 | 9 | 10 | class PocoResultCollector(object): 11 | def __init__(self, project_root, testcases_filenames, testcase_name, testcase_dir='.', 12 | logfilename='poco-result.log', 13 | metainfofilename='metainfo.txt'): 14 | self.testcases_filenames = set(testcases_filenames) # [full path], multiple files 15 | self.testcase_dir = testcase_dir 16 | self.project_root = project_root 17 | 18 | root_paths = [project_root, 'pocounit-results'] 19 | root_paths += os.path.relpath(testcase_dir, project_root).replace('\\', '/').split('/') 20 | root_paths.append(os.path.splitext(os.path.basename(testcases_filenames[0]))[0]) 21 | root_paths.append(testcase_name) 22 | 23 | self.root = os.path.join(*root_paths) 24 | self.root = os.path.normpath(self.root) 25 | if os.path.isfile(self.root): 26 | raise RuntimeError('"{}" already exists as non-directory, please remove it first.') 27 | if not os.path.exists(self.root): 28 | os.makedirs(self.root) 29 | self.logger = StreamLogger(os.path.join(self.root, logfilename)) 30 | self.metainfo_logger = StreamLogger(os.path.join(self.root, metainfofilename)) 31 | 32 | def collect(self, tag, value): 33 | self.logger.write(json.dumps({'tag': tag, 'value': value, 'ts': time.time()})) 34 | 35 | def collect_metainfo(self, tag, value): 36 | self.metainfo_logger.write(json.dumps({'tag': tag, 'value': value})) 37 | 38 | def get_root_path(self): 39 | return self.root 40 | 41 | def get_project_root_path(self): 42 | return self.project_root 43 | 44 | def get_resource_relative_path(self, respath): 45 | return os.path.relpath(respath, self.root) 46 | 47 | def get_testcases_filenames(self): 48 | return self.testcases_filenames 49 | 50 | def add_testcase_file(self, path): 51 | """ 52 | 除了TestCase class所定义的那个文件之外,的其他关联的文件都可以加进来 53 | 54 | :param path: 55 | :return: 56 | """ 57 | 58 | if not os.path.isabs(path): 59 | path = os.path.join(self.project_root, path) 60 | self.testcases_filenames.add(path) 61 | -------------------------------------------------------------------------------- /pocounit/result/emitter.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class PocoTestResultEmitter(object): 5 | def __init__(self, collector): 6 | self.collector = collector 7 | 8 | def start(self): 9 | pass 10 | 11 | def stop(self): 12 | pass 13 | 14 | # def finalize(self): 15 | # pass 16 | 17 | def emit(self, tag, value): 18 | self.collector.collect(tag, value) 19 | -------------------------------------------------------------------------------- /pocounit/result/logger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import threading 4 | 5 | 6 | class StreamLogger(object): 7 | def __init__(self, filename='poco-stream-result.log'): 8 | print('log filename will be "{}"'.format(filename)) 9 | self.logfile = open(filename, 'w') 10 | self.mutex = threading.Lock() 11 | 12 | def write(self, val): 13 | with self.mutex: 14 | self.logfile.write(val) 15 | self.logfile.write('\n') 16 | self.logfile.flush() 17 | -------------------------------------------------------------------------------- /pocounit/result/metainfo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import datetime 4 | 5 | from pocounit.result.emitter import PocoTestResultEmitter 6 | 7 | 8 | class MetaInfo(PocoTestResultEmitter): 9 | TAG = 'MetaInfo' 10 | 11 | def __init__(self, collector): 12 | super(MetaInfo, self).__init__(collector) 13 | 14 | def test_started(self, name): 15 | self.emit(self.TAG, { 16 | 'type': 'testcase', 17 | 'value': { 18 | 'name': name, 19 | 'started_at': str(datetime.datetime.now()), 20 | } 21 | }) 22 | 23 | def test_ended(self, name): 24 | self.emit(self.TAG, { 25 | 'type': 'testcase', 26 | 'value': { 27 | 'name': name, 28 | 'ended_at': str(datetime.datetime.now()), 29 | } 30 | }) 31 | 32 | def test_succeed(self, name): 33 | self.emit(self.TAG, { 34 | 'type': 'testcase', 35 | 'value': { 36 | 'name': name, 37 | 'success': True, 38 | } 39 | }) 40 | 41 | def test_fail(self, name): 42 | self.emit(self.TAG, { 43 | 'type': 'testcase', 44 | 'value': { 45 | 'name': name, 46 | 'success': False, 47 | } 48 | }) 49 | 50 | def set_testcase_metainfo(self, name, metainfo): 51 | self.emit(self.TAG, { 52 | 'type': 'testcase', 53 | 'value': { 54 | 'name': name, 55 | 'metainfo': metainfo, 56 | } 57 | }) 58 | 59 | def snapshot_device_info(self, udid, info): 60 | """ 61 | Take down the device info according udid. 62 | 63 | :param udid: serialno for Android 64 | :param info: a dict hold any information of the device 65 | """ 66 | 67 | self.emit(self.TAG, { 68 | 'type': 'device', 69 | 'value': { 70 | 'udid': udid, 71 | 'info': info, 72 | } 73 | }) 74 | 75 | def emit(self, tag, value): 76 | self.collector.collect_metainfo(tag, value) 77 | -------------------------------------------------------------------------------- /pocounit/result/record.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | 5 | from pocounit.result.emitter import PocoTestResultEmitter 6 | from airtest.core.api import device as current_device 7 | from airtest.core.helper import G 8 | 9 | 10 | def get_udid(dev): 11 | return dev.uuid 12 | 13 | 14 | # 多设备录屏待测试 15 | class ScreenRecorder(PocoTestResultEmitter): 16 | TAG = 'record' 17 | 18 | def __init__(self, collector, devices=None): 19 | super(ScreenRecorder, self).__init__(collector) 20 | self.devices = devices 21 | self.record_filepaths = {} # udid -> path 22 | self.started = {} # udid -> True/False 23 | 24 | def start(self): 25 | if self.started: 26 | return False 27 | 28 | # initialize details 29 | self.devices = self.devices or G.DEVICE_LIST 30 | for dev in self.devices: 31 | udid = get_udid(dev) 32 | self.record_filepaths[udid] = os.path.join(self.collector.get_root_path(), 'screen-{}.mp4'.format(udid)) 33 | 34 | # start recording 35 | for dev in self.devices: 36 | udid = get_udid(dev) 37 | self.started[udid] = self.start_device_recorder(udid, dev) 38 | 39 | return True 40 | 41 | def stop(self): 42 | if not self.started: 43 | return False 44 | 45 | for dev in self.devices: 46 | udid = get_udid(dev) 47 | if self.started[udid]: 48 | self.stop_device_recorder(udid, dev) 49 | del self.started[udid] 50 | 51 | return True 52 | 53 | def start_device_recorder(self, udid, device): 54 | record_file = self.record_filepaths[udid] 55 | if hasattr(device, 'start_recording') and hasattr(device, 'stop_recording'): 56 | try: 57 | # force to stop before starting new recoding 58 | device.stop_recording(record_file) 59 | except: 60 | pass 61 | 62 | success = device.start_recording() 63 | if success: 64 | relpath = self.collector.get_resource_relative_path(record_file) 65 | display_info = device.display_info 66 | if display_info['orientation'] in (1, 3): 67 | orientation = 'horizontal' 68 | resolution = [display_info['height'], display_info['width']] 69 | else: 70 | orientation = 'vertical' 71 | resolution = [display_info['width'], display_info['height']] 72 | 73 | # orientation: current orientation 74 | # resolution: recording resolution. 当前默认等于屏幕分辨率 75 | relpath = relpath.replace('\\', '/') 76 | self.emit(self.TAG, {'event': 'started', 'filename': relpath, 'orientation': orientation, 'resolution': resolution}) 77 | return success 78 | return False 79 | 80 | def stop_device_recorder(self, udid, device): 81 | if hasattr(device, 'stop_recording'): 82 | record_file = self.record_filepaths[udid] 83 | relpath = self.collector.get_resource_relative_path(record_file).replace('\\', '/') 84 | self.emit(self.TAG, {'event': 'stopped', 'filename': relpath}) 85 | 86 | try: 87 | return device.stop_recording(record_file) 88 | except: 89 | try: 90 | # 如果停止失败,就尝试直接pull最后一次录屏文件 91 | return device.recorder.pull_last_recording_file(record_file) 92 | except: 93 | pass 94 | return False 95 | -------------------------------------------------------------------------------- /pocounit/result/runner_runtime.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | 5 | from pocounit.result.emitter import PocoTestResultEmitter 6 | 7 | 8 | class Redirector(object): 9 | def __init__(self, origin, _type, logger): 10 | super(Redirector, self).__init__() 11 | self.buf = '' 12 | self.type = _type 13 | self.logger = logger 14 | if hasattr(sys, '__save_origin_std_' + _type): 15 | self.origin = getattr(sys, '__save_origin_std_' + _type) 16 | else: 17 | self.origin = origin 18 | setattr(sys, '__save_origin_std_' + _type, origin) 19 | 20 | def write(self, text): 21 | # 如果原流不支持unicode,那就忽略 22 | try: 23 | self.origin.write(text) 24 | except: 25 | pass 26 | 27 | self.buf += text 28 | lines = None 29 | if self.buf.endswith('\n'): 30 | lines, self.buf = self.buf.rstrip(), '' 31 | elif '\n' in self.buf: 32 | lines, self.buf = self.buf.rsplit('\n', 1) 33 | 34 | if lines: 35 | self.logger.log(lines) 36 | 37 | def __getattr__(self, key): 38 | if hasattr(self.origin, key): 39 | return getattr(self.origin, key) 40 | else: 41 | raise AttributeError('{} has no attribute {}'.format(self.origin, key)) 42 | 43 | 44 | class RunnerRuntimeLog(PocoTestResultEmitter): 45 | TAG = 'runnerRuntimeLog' 46 | 47 | def __init__(self, collector): 48 | super(RunnerRuntimeLog, self).__init__(collector) 49 | 50 | def start(self): 51 | if not hasattr(sys, '__redirected_stdout'): 52 | setattr(sys, '__redirected_stdout', sys.stdout) 53 | sys.stdout = Redirector(sys.stdout, 'stdout', self) 54 | if not hasattr(sys, '__redirected_stderr'): 55 | setattr(sys, '__redirected_stderr', sys.stderr) 56 | sys.stderr = Redirector(sys.stderr, 'stderr', self) 57 | 58 | def stop(self): 59 | if hasattr(sys, '__redirected_stdout'): 60 | sys.stdout = sys.stdout.origin 61 | delattr(sys, '__redirected_stdout') 62 | if hasattr(sys, '__redirected_stderr'): 63 | sys.stderr = sys.stderr.origin 64 | delattr(sys, '__redirected_stderr') 65 | 66 | def log(self, content): 67 | self.emit(self.TAG, {'content': content}) 68 | -------------------------------------------------------------------------------- /pocounit/result/site_snapshot.py: -------------------------------------------------------------------------------- 1 | # coding-utf-8 2 | 3 | import base64 4 | import hashlib 5 | import time 6 | import json 7 | import os 8 | import six 9 | 10 | from pocounit.result.emitter import PocoTestResultEmitter 11 | 12 | 13 | def make_hash(src): 14 | if isinstance(src, six.text_type): 15 | src = src.encode('utf-8') 16 | else: 17 | src = six.binary_type(src) 18 | return hashlib.md5(src).hexdigest() 19 | 20 | 21 | class SiteSnapshot(PocoTestResultEmitter): 22 | TAG = 'siteSnapshot' 23 | 24 | def __init__(self, collector): 25 | super(SiteSnapshot, self).__init__(collector) 26 | self.save_path = os.path.join(collector.get_root_path(), 'snapshots') 27 | if not os.path.exists(self.save_path): 28 | os.mkdir(self.save_path) 29 | self.poco = None 30 | 31 | def stop(self): 32 | self.snapshot('caseEnd') 33 | 34 | def set_poco_instance(self, poco): 35 | self.poco = poco 36 | 37 | def snapshot(self, site_id=None): 38 | self.snapshot_screen(site_id) 39 | self.snapshot_hierarchy(site_id) 40 | 41 | def snapshot_hierarchy(self, site_id=None): 42 | if not self.poco: 43 | return 44 | site_id = site_id or time.time() 45 | hierarchy_data = self.poco.agent.hierarchy.dump() 46 | basename = 'hierarchy-{}.json'.format(make_hash(site_id)) 47 | fpath = os.path.join(self.save_path, basename) 48 | with open(fpath, 'wb') as f: 49 | h = json.dumps(hierarchy_data) 50 | if six.PY3: 51 | h = h.encode('utf-8') 52 | f.write(h) 53 | self.emit(self.TAG, {'type': 'hierarchy', 'dataPath': 'snapshots/{}'.format(basename), 'site_id': site_id}) 54 | return fpath 55 | 56 | def snapshot_screen(self, site_id=None): 57 | if not self.poco: 58 | return 59 | site_id = site_id or time.time() 60 | b64img, fmt = self.poco.snapshot() 61 | width, height = self.poco.get_screen_size() 62 | basename = 'screen-{}.{}'.format(make_hash(site_id), fmt) 63 | fpath = os.path.join(self.save_path, basename) 64 | with open(fpath, 'wb') as f: 65 | f.write(base64.b64decode(b64img)) 66 | self.emit(self.TAG, {'type': 'screen', 'dataPath': 'snapshots/{}'.format(basename), 'site_id': site_id, 67 | 'width': width, 'height': height, 'format': fmt}) 68 | return fpath 69 | -------------------------------------------------------------------------------- /pocounit/result/trace.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import fnmatch 4 | import os 5 | import sys 6 | import shutil 7 | 8 | from pocounit.result.emitter import PocoTestResultEmitter 9 | 10 | 11 | class ScriptTracer(PocoTestResultEmitter): 12 | TAG = 'trace' 13 | 14 | def __init__(self, collector): 15 | super(ScriptTracer, self).__init__(collector) 16 | self.project_root = self.collector.get_project_root_path() 17 | self._script_filenames = None 18 | self._script_filenames_lower = None 19 | self._tracing_file_pattern = [] 20 | self._origin_trace_func = None 21 | 22 | def start(self): 23 | self._script_filenames = self.collector.get_testcases_filenames() 24 | self._script_filenames_lower = [f.lower().replace('\\', '/') for f in self._script_filenames] 25 | for f in self._script_filenames: 26 | src = os.path.relpath(f, self.project_root) 27 | dst = os.path.join(self.collector.get_root_path(), src) 28 | dst_dir = os.path.dirname(dst) 29 | if not os.path.exists(dst_dir): 30 | os.makedirs(dst_dir) 31 | shutil.copyfile(f, dst) 32 | self._origin_trace_func = sys.gettrace() 33 | sys.settrace(self._make_tracer()) 34 | 35 | def stop(self): 36 | sys.settrace(self._origin_trace_func) 37 | 38 | def file_hit(self, fname): 39 | for pat in self._script_filenames_lower: 40 | if fnmatch.fnmatch(fname, pat): 41 | return True 42 | for pat in self._tracing_file_pattern: 43 | if fnmatch.fnmatch(fname, pat): 44 | return True 45 | return False 46 | 47 | def _make_tracer(self): 48 | def tracer(frame, event, arg): 49 | co_filename = frame.f_code.co_filename 50 | if event == 'line' and self.file_hit(co_filename.lower().replace('\\', '/')): 51 | line_num = frame.f_lineno 52 | fname = os.path.relpath(co_filename, self.project_root) 53 | fname = fname.replace('\\', '/') 54 | self.emit(self.TAG, {'filename': fname, 'lineno': line_num}) 55 | return tracer 56 | return tracer 57 | -------------------------------------------------------------------------------- /pocounit/runner.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import unittest 4 | 5 | from pocounit.result import PocoTestResult 6 | 7 | 8 | class PocoTestRunner(unittest.TextTestRunner): 9 | """ 10 | 先占坑,先默认用text方式展示,以后再换成自定义的 11 | """ 12 | 13 | resultclass = PocoTestResult 14 | -------------------------------------------------------------------------------- /pocounit/suite.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import unittest 4 | 5 | from pocounit.fixture import FixtureUnit 6 | 7 | 8 | class PocoTestSuite(unittest.TestSuite, FixtureUnit): 9 | def __init__(self, tests=()): 10 | unittest.TestSuite.__init__(self, tests) 11 | FixtureUnit.__init__(self) 12 | 13 | def run(self, result, debug=False): 14 | self.setUp() 15 | super(PocoTestSuite, self).run(result, debug) 16 | self.tearDown() 17 | -------------------------------------------------------------------------------- /pocounit/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/pocounit/utils/__init__.py -------------------------------------------------------------------------------- /pocounit/utils/misc.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import re 5 | 6 | 7 | def detect_package_root(mod_filename): 8 | """ 9 | 从某个模块的文件路径向上搜索,一直找到这个模块所在包的根目录,将包名第一段返回 10 | 11 | :param mod_filename: 12 | :return: 根包名 or None 13 | """ 14 | 15 | egginfo_pattern = re.compile(r'.*\.egg-info$') 16 | p = os.path.abspath(os.path.join(mod_filename, '..')) 17 | while True: 18 | ls = os.listdir(p) 19 | matched = False 20 | for f in ls: 21 | if f == 'setup.py' or egginfo_pattern.match(f): 22 | matched = True 23 | break 24 | if matched: 25 | break 26 | p_prev = os.path.abspath(os.path.join(p, '..')) 27 | if p == p_prev: 28 | return None 29 | p = p_prev 30 | return p 31 | 32 | 33 | def get_project_root(test_case_filename): 34 | return os.getenv("PROJECT_ROOT") or detect_package_root(test_case_filename) or os.getcwd() 35 | 36 | 37 | def has_override(method, subCls, baseCls): 38 | return getattr(subCls, method) != getattr(baseCls, method) 39 | -------------------------------------------------------------------------------- /pocounit/utils/outcome.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | import contextlib 5 | 6 | from unittest.case import SkipTest 7 | 8 | 9 | class _ShouldStop(Exception): 10 | """ 11 | The test should stop. 12 | """ 13 | 14 | 15 | class Outcome(object): 16 | def __init__(self, result=None): 17 | self.expecting_failure = False 18 | self.result = result 19 | self.result_supports_subtests = hasattr(result, "addSubTest") 20 | self.success = True 21 | self.skipped = [] 22 | self.expectedFailure = None 23 | self.errors = [] 24 | 25 | @contextlib.contextmanager 26 | def testPartExecutor(self, test_case, isTest=False): 27 | old_success = self.success 28 | self.success = True 29 | try: 30 | yield 31 | except KeyboardInterrupt: 32 | raise 33 | except SkipTest as e: 34 | self.success = False 35 | self.skipped.append((test_case, str(e))) 36 | except _ShouldStop: 37 | pass 38 | except: 39 | exc_info = sys.exc_info() 40 | if self.expecting_failure: 41 | self.expectedFailure = exc_info 42 | else: 43 | self.success = False 44 | self.errors.append((test_case, exc_info)) 45 | # explicitly break a reference cycle: 46 | # exc_info -> frame -> exc_info 47 | exc_info = None 48 | else: 49 | if self.result_supports_subtests and self.success: 50 | self.errors.append((test_case, None)) 51 | finally: 52 | self.success = self.success and old_success 53 | -------------------------------------------------------------------------------- /pocounit/utils/trace.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import sys 4 | 5 | 6 | def get_current_lineno_of(filenames, frame=None): 7 | filenames_lower = [f.lower().replace('\\', '/') for f in filenames] 8 | 9 | frame = frame or sys._getframe(0) 10 | while frame: 11 | if frame.f_code.co_filename.lower().replace('\\', '/') in filenames_lower: 12 | break 13 | frame = frame.f_back 14 | 15 | if frame: 16 | return frame.f_lineno, frame.f_code.co_filename.replace('\\', '/') 17 | else: 18 | return None, None 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | airtest 2 | pocoui -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def parse_requirements(filename='requirements.txt'): 7 | """ load requirements from a pip requirements file. (replacing from pip.req import parse_requirements)""" 8 | lineiter = (line.strip() for line in open(filename)) 9 | return [line for line in lineiter if line and not line.startswith("#")] 10 | 11 | 12 | setup( 13 | name='pocounit', 14 | version='1.0.40', 15 | keywords="PocoUnit unittest", 16 | description='Unittest framework for poco and airtest', 17 | packages=find_packages(), 18 | include_package_data=True, 19 | install_requires=parse_requirements(), 20 | license='Apache License 2.0', 21 | ) 22 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/test/__init__.py -------------------------------------------------------------------------------- /test/poco_uiautomation_android/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/PocoUnit/b409928dd904649e559c1cc044c35aa01e97a1c5/test/poco_uiautomation_android/__init__.py -------------------------------------------------------------------------------- /test/poco_uiautomation_android/simple.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.case import PocoTestCase 4 | from pocounit.addons.poco.action_tracking import ActionTracker 5 | 6 | from poco.drivers.android.uiautomation import AndroidUiautomationPoco 7 | 8 | 9 | class AndroidNativeUITestCase(PocoTestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | super(AndroidNativeUITestCase, cls).setUpClass() 13 | 14 | cls.poco = AndroidUiautomationPoco() 15 | 16 | # 启用动作捕捉(action tracker)和游戏运行时日志捕捉插件(runtime logger) 17 | action_tracker = ActionTracker(cls.poco) 18 | cls.register_addon(action_tracker) 19 | 20 | 21 | class T1(AndroidNativeUITestCase): 22 | def runTest(self): 23 | self.poco(text='设置').click() 24 | self.poco(text='WLAN').click() 25 | 26 | 27 | if __name__ == '__main__': 28 | import pocounit 29 | pocounit.main() 30 | -------------------------------------------------------------------------------- /test/test_runner.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | import unittest 5 | 6 | from pocounit.case import PocoTestCase 7 | 8 | 9 | class TestPocoRunner(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | pass 13 | 14 | def test_run(_): 15 | class MyCase(PocoTestCase): 16 | def setUp(self): 17 | print('setup') 18 | self.value = '2333' 19 | 20 | def runTest(self): 21 | print('before') 22 | self.assertEqual(1, 1, 'ok') 23 | self.assertEqual(self.value, '2333', 'setup ok') 24 | print('after') 25 | 26 | def tearDown(self): 27 | print('teardown') 28 | 29 | suite = unittest.TestSuite() 30 | suite.addTest(MyCase()) 31 | 32 | runner = unittest.TextTestRunner() 33 | runner.run(suite) 34 | --------------------------------------------------------------------------------