├── .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 |

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