├── .gitignore
├── .travis.yml
├── MANIFEST.in
├── README.md
├── debug.py
├── dev.sh
├── image
├── fail.png
├── mainpage.png
└── success.png
├── lyrebird_tracking
├── __init__.py
├── context.py
├── data
│ └── base.json
├── report_template
│ ├── components
│ │ ├── filter-tag.js
│ │ └── tracking-detail.js
│ ├── data
│ │ └── report-data.js
│ ├── index.html
│ ├── main.js
│ ├── report.html
│ └── style
│ │ └── report.css
├── server
│ ├── __init__.py
│ ├── base_handler.py
│ ├── data_manager.py
│ ├── search_handler.py
│ └── validator.py
├── static
│ ├── component
│ │ ├── banner.vue
│ │ ├── filter-tag.vue
│ │ ├── main.vue
│ │ ├── tracking-detail.vue
│ │ └── tracking-list.vue
│ └── js
│ │ └── main.js
├── templates
│ └── index.html
├── tracking.py
└── webui.py
├── requirements.txt
├── setup.py
└── tests
├── test_filter_error.py
└── test_select.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### macOS template
3 | *.DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | # Icon must end with two \r
8 | Icon
9 |
10 |
11 | # Thumbnails
12 | ._*
13 |
14 | # Files that might appear in the root of a volume
15 | .DocumentRevisions-V100
16 | .fseventsd
17 | .Spotlight-V100
18 | .TemporaryItems
19 | .Trashes
20 | .VolumeIcon.icns
21 | .com.apple.timemachine.donotpresent
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 | ### Python template
30 | # Byte-compiled / optimized / DLL files
31 | __pycache__/
32 | *.py[cod]
33 | *$py.class
34 |
35 | # C extensions
36 | *.so
37 |
38 | # Distribution / packaging
39 | .Python
40 | env/
41 | build/
42 | develop-eggs/
43 | dist/
44 | downloads/
45 | eggs/
46 | .eggs/
47 | lib/
48 | lib64/
49 | parts/
50 | sdist/
51 | var/
52 | wheels/
53 | *.egg-info/
54 | .installed.cfg
55 | *.egg
56 |
57 | # PyInstaller
58 | # Usually these files are written by a python script from a template
59 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
60 | *.manifest
61 | *.spec
62 |
63 | # Installer logs
64 | pip-log.txt
65 | pip-delete-this-directory.txt
66 |
67 | # Unit test / coverage reports
68 | htmlcov/
69 | .tox/
70 | .coverage
71 | .coverage.*
72 | .cache
73 | nosetests.xml
74 | coverage.xml
75 | *,cover
76 | .hypothesis/
77 |
78 | # Translations
79 | *.mo
80 | *.pot
81 |
82 | # Django stuff:
83 | *.log
84 | local_settings.py
85 |
86 | # Flask stuff:
87 | instance/
88 | .webassets-cache
89 |
90 | # Scrapy stuff:
91 | .scrapy
92 |
93 | # Sphinx documentation
94 | docs/_build/
95 |
96 | # PyBuilder
97 | target/
98 |
99 | # Jupyter Notebook
100 | .ipynb_checkpoints
101 |
102 | # pyenv
103 | .python-version
104 |
105 | # celery beat schedule file
106 | celerybeat-schedule
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # dotenv
112 | .env
113 |
114 | # virtualenv
115 | .venv
116 | venv/
117 | ENV/
118 |
119 | # Spyder project settings
120 | .spyderproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | .idea/
126 | .statistics_result.xls
127 |
128 | .vscode/
129 |
130 | .pytest_cache/
131 |
132 | data/
133 | __pycache__
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | include:
3 | - stage: unit test
4 | language: python
5 | catch: pip
6 | python:
7 | - "3.6"
8 | # command to install dependencies
9 | install:
10 | - pip install -r requirements.txt
11 | - pip install .
12 | # command to run tests
13 | script: pytest
14 |
15 | - stage: release to pypi
16 | if: type = push AND tag IS present
17 | language: python
18 | catch: pip
19 | python:
20 | - "3.6"
21 | script:
22 | - echo "Skipping tests"
23 | deploy:
24 | provider: pypi
25 | user: meituanqa
26 | password:
27 | secure: "B8ua64zQJyydVVVWIMG3lCn3yjca4ZFtxYiThQXKIAh/dkDr40B6Em7zCgjb5n1d6A2MhkQKwBcAaEbhX7W2zEjQv6PAeV42QOYgB/W9pdBGbUh31ZXQSwE0mpQdAH0WCCdLOpB0F0e95JDDtVhjQlhAYbrVSpqYAhIeq79rBtJNvxRRrHjdzHYW+qbwFjbWxlO3c06TEpU2dTwHPdBx9iup4utmEmA51HIQkItDzNdHcRCPLyZvr+P27xp9ikrYgg2He/Usyq/zC6V7NhtMaPga8sf4jMPoYEvW6V/fFyRssvlytpdTAeSyZRAyHRl6cw6fyehqfbiTdhFNzUMRWkg/Oijd0+S4EbpmY5DX4+mVDuM0EU6eizchvT9sjwZcSrlEBK74oyWtQOfLy0yl5+T/Eny3pqLaF1wBGARyMw3Nkviijc1llh/wHgsT7eYUDAoTm7XdEPmtv22FVL7OXCrUbuPRdkyzy0x8WSit9gJfj4IJ0L5kdiNoSBtGw1XASgIyJJUwc3ke+sGp4WmrveYK+Lrp/DCgMH/VtSdSvWkuKAJ1c9Eck8NhD2ZJvgQaKGPgmE3Q+3fMbZcI2U3gJeqkGXgNhSGGDau5694vTFdbs3bbX/dS72ahcCu9hkTPEV+oFWXDmXD6I/q8/hEJ2OhxOmdS7EeV36b47h8pWLA="
28 | on:
29 | tags: true
30 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft lyrebird_tracking
2 | recursive-exclude * *.pyc *.pyo *.swo *.swp *.map *.DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lyrebird-tracking
2 |
3 | ## 简介
4 |
5 | ***Tracking 是基于[Lyrebird](https://github.com/meituan/lyrebird)的插件,为移动端应用提供服务请求的数据分析及验证的功能***
6 |
7 |
8 | ## 环境要求
9 |
10 | * macOS
11 |
12 | * Python3.6及以上
13 |
14 |
15 | ## 安装
16 |
17 | ``` bash
18 | pip3 install lyrebird-tracking
19 | ```
20 |
21 | ## 简单示例
22 |
23 | 请求数据分析的应用场景比较广泛,以其中一种典型应用场景如客户端[埋点](https://baike.baidu.com/item/%E5%9F%8B%E7%82%B9)的分析校验来进行示例
24 |
25 | ### 分析待测对象
26 |
27 | 1. 假设埋点上报的host为 ***abctest.com***
28 | 2. 埋点上报内容如下所述,key2字段为索引值,action与page字段是待校验项
29 |
30 | ``` json
31 | [
32 | {
33 | "key1": "val1",
34 | "property1" : [
35 | {
36 | "key2": "val2",
37 | "action": "view",
38 | "page": "detail_page"
39 | }
40 | ]
41 | }
42 | ]
43 | ```
44 |
45 | ### 编写配置文件
46 |
47 | 1. selector:以JSONPath描述过滤筛选JSON的逻辑,详细语法示例见[附录](#配置文件数据格式)
48 | 2. assert:校验逻辑,校验action与page两个字段,以JSONSchema语法描述预期
49 |
50 | ``` json
51 | {
52 | "target": [
53 | "abctest.com"
54 | ],
55 | "cases": [{
56 | "name": "test case 1st",
57 | "selector": "$[?key1='val1'].property1[?key2='val2']",
58 | "asserts": [{
59 | "field": "action",
60 | "schema": {
61 | "type": "string",
62 | "pattern": "view"
63 | }
64 | },
65 | {
66 | "field": "page",
67 | "schema": {
68 | "type": "string",
69 | "pattern": "detail_page"
70 | }
71 | }
72 | ]
73 | }]
74 | }
75 | ```
76 |
77 | ### 校验功能
78 |
79 | 若移动端发出符合预期的Request Data,如下
80 |
81 | ``` json
82 | [
83 | {
84 | "key1": "val1",
85 | "property1" : [
86 | {
87 | "action": "view",
88 | "key2": "val2",
89 | "lab": {
90 | "good_id": 10001,
91 | "index": 5,
92 | "page_name": "detail_page101"
93 | },
94 | "page": "detail_page"
95 | }
96 | ]
97 | }
98 | ]
99 | ```
100 |
101 | Tracking会自动分析和校验,如图所示
102 |
103 |
104 |
105 | 若移动端发出不符合预期的Request Data,如其中action字段的值不符合预期"view",如下
106 |
107 | ``` json
108 | [
109 | {
110 | "key1": "val1",
111 | "property1" : [
112 | {
113 | "action": "click",
114 | "key2": "val2",
115 | "lab": {
116 | "good_id": 10001,
117 | "index": 5,
118 | "page_name": "detail_page101"
119 | },
120 | "page": "detail_page"
121 | }
122 | ]
123 | }
124 | ]
125 | ```
126 |
127 | Tracking会自动分析和校验,并将错误信息高亮标红展示,如图所示
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | ## 详细功能介绍
136 |
137 |
138 | 如上图所示:
139 |
140 | 1. 工具栏:清空测试缓存
141 |
142 | 2. 分组标签:可以分组筛选查看case的结果
143 |
144 | 3. case列表:以时间倒序排列展示的case记录
145 |
146 | 1. case通过,即为所有校验字段全部通过,展示绿色 Pass Button
147 | 2. case失败,即为只要有一个校验字段失败,展示红色 Fail Button
148 | 3. case没有触发,展示为N/A状态,点击可以查看具体预期值等信息
149 | 4. 单行点击,可以唤起对应的case详情,查看具体校验字段和原数据等信息
150 |
151 | 4. 校验字段详情展示:点击左侧一行,右侧会展示对应的校验详情,展开状态
152 |
153 | 1. Field:校验字段
154 | 2. Expect Schema:校验字段对应的预期JSONSchema
155 | 3. Actual vaule:实际校验字段的值
156 |
157 | 5. 校验字段名和结果展示:校验详情的收起状态, 收起状态的校验结果有三种:
158 |
159 | 1. 字段校验通过,校验字段展示绿色
160 | 2. 字段校验失败,校验字段展示红色
161 | 3. 字段校验逻辑为空,校验字段展示蓝色
162 |
163 | 6. 匹配数据展示区
164 |
165 | 1. 展示匹配查询规则的数据
166 | 2. 字段校验失败,红色高亮提示
167 | 2. 未配置校验/断言逻辑的,蓝色高亮提示
168 | 3. 校验结果错误展示红色高亮提示,鼠标悬停在字段上,会有详细的错误提示
169 |
170 |
171 |
172 | ## 使用流程
173 |
174 | 1. 准备基准数据文件,数据格式见[附录](#JSONPath-简明介绍)
175 |
176 | 2. 将基准数据文件放入指定路径下: ***~.lyrebird/plugins/lyrebird_tracking/base.json***
177 |
178 | 3. 启动[Lyrebird](https://github.com/meituan/lyrebird)工具,[手机链接代理](https://github.com/meituan/lyrebird#%E8%BF%9E%E6%8E%A5%E7%A7%BB%E5%8A%A8%E8%AE%BE%E5%A4%87),操作过程中观测case校验等信息展示
179 |
180 |
181 | ## 开发者指南
182 |
183 | ``` shell
184 | # clone 代码
185 | git clone https://github.com/meituan/lyrebird-tracking.git
186 |
187 | # 进入工程目录
188 | cd lyrebird-tracking
189 |
190 | # 创建虚拟环境
191 | python3 -m venv venv
192 |
193 | # 安装依赖
194 | source venv/bin/activate
195 | pip3 install -r requirements.txt
196 |
197 | # 使用IDE打开工程(推荐Pycharm或vscode)
198 |
199 | # 在IDE中执行debug.py即可开始调试
200 | ```
201 |
202 |
203 | ## 附录
204 |
205 | ### 配置文件数据格式
206 |
207 | #### 字段说明
208 |
209 | * target:待校验的host集合
210 |
211 | * cases:测试用例集合
212 |
213 | * name:测试用例名
214 |
215 | * selector:JSONPath语法描述的查询条件
216 |
217 | * asserts:校验条件
218 |
219 | * field:需要校验的字段
220 |
221 | * schema:JSONSchema语法描述的校验条件,不校验可填为{},前端会高亮显示该字段
222 |
223 | * groupname(可选):case对应分组的组名
224 |
225 | * groupid(可选):case对应分组的组id
226 |
227 |
228 |
229 | ``` json
230 | {
231 | "target": [
232 | "abctest.com"
233 | ],
234 | "cases": [{
235 | "name": "test case 1st",
236 | "selector": "$[?key1='val1'].property1[?key2='val2']",
237 | "asserts": [{
238 | "field": "action",
239 | "schema": {
240 | "type": "string",
241 | "pattern": "view"
242 | }
243 | },
244 | {
245 | "field": "page",
246 | "schema": {
247 | "type": "string",
248 | "pattern": "detail_page"
249 | }
250 | }
251 | ],
252 | "groupname": "group1",
253 | "groupid": 1
254 | }, {
255 | "name": "test case 2nd",
256 | "selector": "$[*].property2[?key3='val3']",
257 | "asserts": [{
258 | "field": "action",
259 | "schema": {
260 | "type": "string",
261 | "pattern": "click"
262 | }
263 | },
264 | {
265 | "field": "page",
266 | "schema": {
267 | "type": "string",
268 | "pattern": "home_page"
269 | }
270 | }
271 | ],
272 | "groupname": "group2",
273 | "groupid": 2
274 | }]
275 | }
276 | ```
277 |
278 | ### JSONPath 简明介绍
279 |
280 | 用于查询逻辑的selector配置基于[JSONPath](https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html)的语法。类似于XPath在xml文档中的定位,JSONPath表达式通常是用来路径检索或设置Json的。目前仅支持一部分JSONPath语法,如下所述。
281 |
282 | #### 支持语法
283 |
284 |
285 | JSONPath | 描述 |
286 | $ | 根对象。例如$name |
287 | [num] | 数组访问,其中num是数字。例如$[0].leader.departments[1].name |
288 | [*] | 数组访问,访问所有数组的元素。例如$[*].leader.departments[2].name |
289 | [key='test'] | 字符串类型对象属性判断相等的过滤,例如$departs[name = 'test'] |
290 | . | 属性访问,例如$name.a.b |
291 |
292 |
293 |
294 |
295 | #### 语法示例
296 |
297 | ``` json
298 | [
299 | {
300 | "name":"king",
301 | "property":123,
302 | "house":120
303 | },
304 | {
305 | "name":"wang",
306 | "property":456,
307 | "house":240
308 | },
309 | {
310 | "name":"king",
311 | "car":"audi",
312 | "house":120
313 | },
314 | {
315 | "name":"king",
316 | "property":123,
317 | "house":789
318 | },
319 | {
320 | "name":"king",
321 | "property":666,
322 | "house":666
323 | }
324 | ]
325 | ```
326 |
327 |
328 | JSONPath | 语义 |
329 | $ | 根对象 |
330 | $[1] | 第1个元素 |
331 | $[*] | 全部元素 |
332 | $[name='king'] | list中name属性为'king'的元素 |
333 | $[name='king'].property | list中name属性为'king'的元素并且取该元素的property属性的值 |
334 |
335 |
336 |
337 | ### JSONSchema 介绍
338 |
339 | 用于校验逻辑的schema配置基于[JSONSchema](https://json-schema.org/understanding-json-schema/basics.html)的语法。JSON Schema 用以标注和验证JSON文档的元数据的文档,可以类比于XML Schema。相对于JSON Schema,一个JSON文档就是JSON Schema的一个instance,可以校验数据结构、数据类型、和详细的判断等。
340 |
341 |
--------------------------------------------------------------------------------
/debug.py:
--------------------------------------------------------------------------------
1 | import lyrebird
2 | import pip
3 |
4 | if __name__ == '__main__':
5 | version_num = pip.__version__[:pip.__version__.find('.')]
6 | if int(version_num) >= 10:
7 | from pip import __main__
8 | __main__._main(['install', '.', '--upgrade'])
9 | else:
10 | pip.main(['install', '.', '--upgrade'])
11 | lyrebird.debug()
--------------------------------------------------------------------------------
/dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "***************************"
4 | echo " Lyrebird-tracking setup start "
5 | echo "***************************"
6 |
7 | # 如果已经有venv目录,删除此目录
8 | if [ -e "./venv/" ]; then
9 | rm -rf ./venv/
10 | fi
11 |
12 | mkdir venv
13 | python3 -m venv ./venv
14 |
15 | # 有些设备上虚拟环境中没有pip,需要通过easy_install安装
16 | if [ ! -e "./venv/bin/pip" ] ;then
17 | echo "pip no exist, install pip with easy_install"
18 | ./venv/bin/easy_install pip
19 | fi
20 |
21 | source ./venv/bin/activate
22 | pip3 install -r ./requirements.txt
23 |
24 |
25 | echo "***************************"
26 | echo " Lyrebird-tracking setup finish "
27 | echo "***************************"
28 |
--------------------------------------------------------------------------------
/image/fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/fail.png
--------------------------------------------------------------------------------
/image/mainpage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/mainpage.png
--------------------------------------------------------------------------------
/image/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/image/success.png
--------------------------------------------------------------------------------
/lyrebird_tracking/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Meituan-Dianping/lyrebird-tracking/81b5aae8762450bb2537cd5bf878d3f5cb82ba21/lyrebird_tracking/__init__.py
--------------------------------------------------------------------------------
/lyrebird_tracking/context.py:
--------------------------------------------------------------------------------
1 | """
2 | 应用上下文类
3 |
4 | """
5 | class Context:
6 | def __init__(self):
7 | # config
8 | self.config = {}
9 | # result list : name and result for list show
10 | self.result_list = []
11 | # content : all data
12 | self.content = []
13 | # error messages list
14 | self.error_list = []
15 | # selected group list
16 | self.select_groups = []
17 |
18 |
19 | # 单例模式
20 | app_context = Context()
21 |
--------------------------------------------------------------------------------
/lyrebird_tracking/data/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "target": [
3 | "abctest.com"
4 | ],
5 | "cases": [{
6 | "name": "test case 1st",
7 | "selector": "$[?key1='val1'].property1[?key2='val2']",
8 | "asserts": [{
9 | "field": "action",
10 | "schema": {
11 | "type": "string",
12 | "pattern": "view"
13 | }
14 | },
15 | {
16 | "field": "page",
17 | "schema": {
18 | "type": "string",
19 | "pattern": "detail_page"
20 | }
21 | },
22 | {
23 | "field": "lab",
24 | "schema": {
25 | "type": "object",
26 | "properties": {
27 | "page_name": {
28 | "type": "string"
29 | },
30 | "good_id": {
31 | "type": "integer"
32 | },
33 | "index": {
34 | "type": "integer"
35 | }
36 | },
37 | "required": [
38 | "page_name",
39 | "good_id",
40 | "index"
41 | ]
42 | }
43 | }
44 | ],
45 | "groupname": "group1",
46 | "groupid": 1
47 | }, {
48 | "name": "test case 2nd",
49 | "selector": "$[*].property2[?key3='val3']",
50 | "asserts": [{
51 | "field": "action",
52 | "schema": {
53 | "type": "string",
54 | "pattern": "click"
55 | }
56 | },
57 | {
58 | "field": "page",
59 | "schema": {
60 | "type": "string",
61 | "pattern": "home_page"
62 | }
63 | }, {
64 | "field": "clickcount",
65 | "schema": {
66 | "type": "number",
67 | "minimum": 0,
68 | "maximum": 100
69 | }
70 | }, {
71 | "field": "goodid_list",
72 | "schema": {
73 | "type": "array",
74 | "minItems": 2,
75 | "maxItems": 3,
76 | "items": {
77 | "type": "number"
78 | }
79 | }
80 | }
81 | ],
82 | "groupname": "group2",
83 | "groupid": 2
84 | }, {
85 | "name": "test case 3rd",
86 | "selector": "$property3[2].property4[?key6='val7'].property5",
87 | "asserts": [{
88 | "field": "email",
89 | "schema": {
90 | "type": "string",
91 | "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$",
92 | "minLength": 2,
93 | "maxLength": 3,
94 | "format": "date-time|email|hostname|ipv4|ipv6|uri"
95 | }
96 | }],
97 | "groupname": "group3",
98 | "groupid": 3
99 | }]
100 | }
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/components/filter-tag.js:
--------------------------------------------------------------------------------
1 | Vue.component(
2 | 'filter-tag', {
3 | template: '#filter-tag',
4 | props: [],
5 | data: function() {
6 | return {
7 | grouplist: [],
8 | showModal: false,
9 | changeGroupCache: [],
10 | allGroup: []
11 | }
12 | },
13 | mounted: function() {
14 | this.tagData();
15 | },
16 | methods: {
17 | tagData: function() {
18 | let filterdata = null;
19 | filterdata = baseData;
20 | for (let i = 0; i < filterdata.cases.length; i++) {
21 | let name = filterdata.cases[i].groupname;
22 | if (typeof name != "undefined") {
23 | // 如果grouplist里面不包含当前groupname返回-1,包含返回index值
24 | if (this.grouplist == 0 || this.grouplist.indexOf(name) == -1) {
25 | this.grouplist.push(name);
26 | }
27 | }
28 | }
29 | // 初始化,展示list赋值展示全部,赋值给AllGroup
30 | this.allGroup = [].concat(this.grouplist);
31 | },
32 | handleClose: function(event, name) {
33 | let index = this.grouplist.indexOf(name);
34 | if (index > -1) {
35 | this.grouplist.splice(index, 1);
36 | }
37 | this.$emit("filterchange", this.grouplist);
38 | },
39 | changeOk: function() {
40 | this.grouplist = this.changeGroupCache;
41 | this.$emit("filterchange", this.grouplist);
42 | this.$Notice.success({
43 | title: "Change Filter Success"
44 | });
45 | },
46 | activatedDataChange: function(val) {
47 | console.log("Selected Groups Change", val);
48 | this.changeGroupCache = val;
49 | }
50 | }
51 |
52 | }
53 | )
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/components/tracking-detail.js:
--------------------------------------------------------------------------------
1 | Vue.component(
2 | 'tracking-detail', {
3 | template: '#tracking-detail-tpl',
4 | props: ["currentcontent", "codedetail"],
5 | computed: {
6 | infoContaint: function() {
7 | infoStr = JSON.stringify(this.currentcontent.content, null, 4);
8 | return '' + infoStr + "
";
9 | }
10 | },
11 | updated: function() {
12 | Prism.highlightAll();
13 | },
14 | mounted: function() {
15 | Prism.highlightAll();
16 | //create monaco editor
17 | editorContainer = this.$el.querySelector("#container");
18 | option = {
19 | value: JSON.stringify(this.codedetail, null, 4),
20 | language: "json",
21 | theme: "vs",
22 | glyphMargin: true,
23 | readOnly: true
24 | };
25 | this.editor = window.monaco.editor.create(editorContainer, option);
26 | //hover register
27 | showhint = this.showhint
28 | monaco.languages.register({ id: 'json' });
29 | monaco.languages.registerHoverProvider('json', {
30 | provideHover: function(model, position) {
31 | return showhint(model, position)
32 | }
33 | });
34 | },
35 | data: function() {
36 | return {
37 | editor: null,
38 | activeNames: ["1"],
39 | match_array: []
40 | };
41 | },
42 | methods: {
43 | jsoninfo: function(content) {
44 | infoStr = JSON.stringify(content, null, 4);
45 | return '' + infoStr + "
";
46 | },
47 | showhint: function(model, position) {
48 | let line = position.lineNumber;
49 | let field = null;
50 | let hint = null;
51 | for (var i = 0; i < this.match_array.length; i++) {
52 | if (this.match_array[i].linenumber === line) {
53 | field = this.match_array[i].linenumber;
54 | hint = JSON.stringify(this.match_array[i].hint);
55 | break;
56 | }
57 | }
58 | if (field) {
59 | return {
60 | range: new monaco.Range(1, 1, line, model.getLineMaxColumn(1)),
61 | contents: [{ value: '**Schema check**' }, { value: hint }]
62 | }
63 | } else {
64 | return {
65 | range: new monaco.Range(0, 0, 0, 0),
66 | contents: []
67 | }
68 | }
69 | }
70 | },
71 | components: {},
72 | watch: {
73 | codedetail: function() {
74 | //每次切换后,都需要清空保存hint提示的array
75 | this.match_array = []
76 | console.log("Code editor: content change");
77 | this.editor.setValue(JSON.stringify(this.codedetail, null, 4));
78 | this.editor.trigger(this.editor.getValue(), "editor.action.formatDocument");
79 |
80 | //如果没有数据展示,不需要做后续的显示处理
81 | if (this.codedetail == null) {
82 | console.log('haha');
83 | return null
84 | }
85 |
86 | for (let i = 0; i < this.currentcontent.asserts.length; i++) {
87 | let matches = this.editor.getModel().findMatches('"' + this.currentcontent.asserts[i].field + '":', false, true, false, false);
88 | if (matches == 0) {
89 | return
90 | }
91 | let match_start_linenumber = matches[0].range.startLineNumber;
92 |
93 | //如果含错误提示,才放入hint提示列表中
94 | if (this.currentcontent.asserts[i].flag === false) {
95 | let match_obj = {
96 | fieldname: this.currentcontent.asserts[i].field,
97 | linenumber: match_start_linenumber,
98 | hint: this.currentcontent.asserts[i].hint
99 | }
100 | this.match_array.push(match_obj);
101 | }
102 |
103 | let fieldname = this.currentcontent.asserts[i].field;
104 | let fvalue = this.codedetail[fieldname];
105 | let fstr = JSON.stringify(fvalue, null, 4);
106 | //获取块大小,涂色用,根据换行符的个数
107 | let detail_length = fstr.split('\n').length;
108 |
109 |
110 | //如果断言的字段有问题,就高亮出来
111 | if (this.currentcontent.asserts[i].flag === false) {
112 | this.editor.deltaDecorations([], [{
113 | range: new monaco.Range(match_start_linenumber, 1, match_start_linenumber + detail_length - 1, 1),
114 | options: { isWholeLine: true, className: "myContentClass" }
115 | }]);
116 | }
117 | // 无断言预期,高亮展示蓝色
118 | else if (this.currentcontent.asserts[i].flag === 2) {
119 | this.editor.deltaDecorations([], [{
120 | range: new monaco.Range(match_start_linenumber, 1, match_start_linenumber + detail_length - 1, 1),
121 | options: { isWholeLine: true, className: "infoContentClass" }
122 | }]);
123 | }
124 |
125 | }
126 | }
127 | }
128 | }
129 |
130 | )
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/data/report-data.js:
--------------------------------------------------------------------------------
1 | var reportCaseData = null
2 | var baseData = null
3 | var detailCollection = null
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tracking Report
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/main.js:
--------------------------------------------------------------------------------
1 | var caseInfo = new Vue({
2 | el: '#caseInfo',
3 | data: {
4 | columns1: [{
5 | title: 'Case',
6 | key: 'name',
7 | className: 'i-table-caseName',
8 | sortable: true
9 | }, {
10 | title: 'Result',
11 | width: 100,
12 | key: 'result',
13 | className: 'i-table-caseName',
14 | sortable: true,
15 | render: (h, params) => {
16 | if (params.row.result === "pass") {
17 | return h(
18 | "i-button", {
19 | props: { size: "small", type: "success" },
20 | style: { width: "50px" }
21 | },
22 | "Pass"
23 | );
24 | } else if (params.row.result === "fail") {
25 | return h(
26 | "i-button", {
27 | props: { size: "small", type: "error" },
28 | style: { width: "50px" }
29 | },
30 | "Fail"
31 | );
32 | } else {
33 | return h(
34 | "i-button", {
35 | props: { size: "small", type: "default" },
36 | style: { width: "50px" }
37 | },
38 | "N/A"
39 | );
40 | }
41 | }
42 | }],
43 | caseInfo: reportCaseData.result,
44 | filter_rules: [],
45 | currentTracking: null,
46 | currentData: null,
47 | codedetail: null
48 | },
49 | methods: {
50 | filterList: function(grouplist) {
51 | this.filter_rules = grouplist;
52 | },
53 | handleRowSelect: function(row, index) {
54 | this.currentTracking = index;
55 | for (let i = 0; i < detailCollection.length; i++) {
56 | let id = detailCollection[i].id
57 | if (row.id == id) {
58 | this.currentData = detailCollection[i];
59 | this.codedetail = detailCollection[i].content;
60 | console.log(id);
61 | console.log(this.codedetail);
62 | console.log(this.currentData);
63 | }
64 | }
65 | }
66 | },
67 | computed: {
68 | displayedData: function() {
69 | // filter出name
70 | let showdata = [];
71 | if (this.filter_rules.length == 0) {
72 | showdata = this.caseInfo;
73 | } else {
74 | for (let i = 0; i < this.filter_rules.length; i++) {
75 | let filter_rule = this.filter_rules[i];
76 | let filtercells = this.caseInfo.filter(function(elem) {
77 | return elem.groupname == filter_rule;
78 | });
79 | showdata = showdata.concat(filtercells);
80 | }
81 | }
82 | return showdata;
83 | }
84 | }
85 | })
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{item}}
28 | Edit Tag
29 |
30 |
31 |
32 | {{item}}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
51 | Check field - {{item.field}}
52 |
53 |
54 |
55 |
56 | Check field - {{item.field}}
57 |
58 |
59 |
60 | Check field - {{item.field}}
61 |
62 |
63 |
Field : {{item.field}}
64 |
Expect schema : No expected value
65 |
66 |
Expect schema :
67 |
68 |
69 |
Actual value :
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | Content
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/lyrebird_tracking/report_template/style/report.css:
--------------------------------------------------------------------------------
1 | .ivu-table {
2 | height: auto;
3 | }
4 |
5 | .i-table-caseName {
6 | font-size: 8px;
7 | }
8 |
9 | .tag-caseName {
10 | font-size: 3px;
11 | }
12 |
13 | .i-table-icon {
14 | font-size: 16px;
15 | text-align: center;
16 | }
17 |
18 | .myContentClass {
19 | background: lightpink;
20 | }
21 |
22 | .infoContentClass {
23 | background: lightblue;
24 | }
25 |
26 | .small {
27 | margin-right: 10px;
28 | font-size: 100%;
29 | }
30 |
31 | .content-header {
32 | margin-bottom: 10px;
33 | }
34 |
35 | #channel {
36 | margin-bottom: 10px;
37 | }
38 |
39 | #screenShot {
40 | background-color: #f5f5f5;
41 | border: 1px solid #ccc;
42 | }
43 |
44 | .picList {
45 | padding: 10px;
46 | float: left;
47 | }
--------------------------------------------------------------------------------
/lyrebird_tracking/server/__init__.py:
--------------------------------------------------------------------------------
1 | from .base_handler import load_base, init_data
2 | from .search_handler import SearchHandler
3 | from .data_manager import new_caseresult
4 | from .validator import Verify
5 | from lyrebird_tracking.context import app_context
6 | from lyrebird import context
7 | import lyrebird
8 |
9 |
10 | def tracking_init():
11 | """
12 | tracking初始化函数
13 | 1. 加载基准文件
14 | 2. 对基准文件进行初始化
15 |
16 | """
17 | load_base()
18 | init_data()
19 |
20 |
21 | def search(jsonnode, jsonpath):
22 | """
23 | 查询函数
24 | :param jsonnode: 待搜索的json数据源
25 | :param jsonpath: 搜索条件
26 | :return: 满足查询搜索条件的结果数组
27 |
28 | """
29 | node = SearchHandler(jsonnode)
30 | targets_list = node.find(jsonpath).data
31 | return targets_list
32 |
33 |
34 | def validate(rule, targets_list):
35 | """
36 | 验证函数
37 | :param rule: 校验规则
38 | :param targets_list: 目标查询的数组
39 |
40 | 校验失败会发消息总线
41 |
42 | """
43 | verify = Verify()
44 | result_list = verify.check(targets_list, rule)
45 | for item in result_list:
46 | # handle data change
47 | new_caseresult(item)
48 | # emit socket io to FE
49 | context.application.socket_io.emit('update', namespace='/tracking-plugin')
50 | if item.get('result') == 'fail':
51 | error_message = dict((k, item[k]) for k in ('name', 'content') if k in item)
52 | error_message['error_msg'] = filter_error_msg(item)
53 | # Bug
54 | # 有埋点错误消息,发事件给消息总线
55 | # pubilsh_error_msg(error_message)
56 |
57 |
58 | def pubilsh_error_msg(msg):
59 | """
60 | 将错误信息通过消息总线发送出去,订阅tracking频道的其他插件会监听到
61 | :param msg: 错误信息详情
62 |
63 | """
64 |
65 | app_context.error_list.append(msg)
66 | lyrebird.publish('tracking.error', msg)
67 | lyrebird.publish('tracking.error', msg, state=True)
68 | lyrebird.publish('tracking.error_list', app_context.error_list)
69 | lyrebird.publish('tracking.error_list', app_context.error_list, state=True)
70 |
71 |
72 | def filter_error_msg(result_dict):
73 | """
74 | 从测试结果中筛选出错误信息,转化为字符串
75 | :param result_dict: dict类型,待处理的结果信息
76 | :return error_str: str类型,最终的错误信息字符串
77 |
78 | """
79 | # 筛选错误信息
80 | error_msg = dict((k, result_dict[k]) for k in ('groupname', 'name') if k in result_dict)
81 | error_list = []
82 | for item in result_dict.get('asserts'):
83 | if item.get('flag') is False:
84 | error_detail = {'field': item.get('field'), 'error detail': item.get('hint')}
85 | error_list.append(error_detail)
86 | error_msg['error message'] = error_list
87 |
88 | # 转换为字符串
89 | error_str = ''
90 | for key in error_msg.keys():
91 | if key == 'error message':
92 | temp_str = key + ':\n'
93 | for item in error_msg.get(key):
94 | temp_str = temp_str + 'field: ' + item.get('field') + '\n' \
95 | + 'error detail: ' + item.get('error detail') + '\n'
96 | else:
97 | temp_str = key + ': ' + str(error_msg.get(key)) + '\n'
98 | error_str = error_str + temp_str
99 |
100 | return error_str
101 |
--------------------------------------------------------------------------------
/lyrebird_tracking/server/base_handler.py:
--------------------------------------------------------------------------------
1 | import lyrebird
2 | import os
3 | import json
4 | import codecs
5 | from lyrebird_tracking.context import app_context
6 | from lyrebird.log import get_logger
7 | import uuid
8 | storage = lyrebird.get_plugin_storage()
9 | BASE_FILE = os.path.abspath(os.path.join(storage, 'base.json'))
10 | DEFAULT_BASE_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', './data/base.json'))
11 |
12 |
13 | def load_base():
14 | """
15 | 加载基准文件(Base文件)
16 | 读取文件路径:~/.lyrbeird/plugin/lyrebird_tracking/base.json
17 | 没有该文件,会加载为默认配置 DEFAULT_BASE_FILE
18 |
19 | """
20 | lyrebird_conf = lyrebird.context.application.conf
21 | # 读取指定base文件,写入到base.json
22 | if lyrebird_conf.get('tracking.base'):
23 | base_path = lyrebird_conf.get('tracking.base')
24 | base = codecs.open(base_path, 'r', 'utf-8').read()
25 | f = codecs.open(BASE_FILE, 'w', 'utf-8')
26 | f.write(base)
27 | f.close()
28 |
29 | # 通过本地默认base文件获取base
30 | elif not os.path.exists(BASE_FILE):
31 | copy_file(BASE_FILE)
32 |
33 | with codecs.open(BASE_FILE, 'r', 'utf-8') as f:
34 | conf_data = json.load(f)
35 |
36 | # base文件内容放置到conf_data中
37 | app_context.config = conf_data
38 |
39 |
40 | def init_data():
41 | """
42 | 初始化基准文件
43 | 初始化解析为应用上下文的变量:
44 | app_context.result_list - case列表数据
45 | app_context.content - 校验结果详情数据
46 |
47 | """
48 | for item in app_context.config.get('cases'):
49 | result_dict = {
50 | 'id': str(uuid.uuid4()),
51 | 'result': 'NA', 'name': item.get('name')}
52 | # 加入分组信息,用于前端筛选
53 | if item.get('groupid'):
54 | result_dict.update({'groupid': item.get('groupid')})
55 |
56 | if item.get('groupname'):
57 | result_dict.update({'groupname': item.get('groupname')})
58 | if not item.get('groupname') in app_context.select_groups:
59 | app_context.select_groups.append(item.get('groupname'))
60 | else:
61 | item.update({'groupname': 'unnamed'})
62 | result_dict.update({'groupname': 'unnamed'})
63 | if not 'unnamed' in app_context.select_groups:
64 | app_context.select_groups.append('unnamed')
65 |
66 | target_dict = {
67 | 'asserts': item.get('asserts'),
68 | 'content': None, 'selector': item.get('selector'),
69 | 'source': None, 'url': None}
70 | target_dict.update(result_dict)
71 | app_context.result_list.append(result_dict)
72 | app_context.content.append(target_dict)
73 |
74 |
75 |
76 | def copy_file(target_path):
77 | """
78 | 复制文件内容,复制默认基准文件到 ~/.lyrbeird/plugin/lyrebird_tracking/base.json
79 | :param target_path: 目标路径
80 |
81 | """
82 | f_from = codecs.open(DEFAULT_BASE_FILE, 'r', 'utf-8')
83 | f_to = codecs.open(target_path, 'w', 'utf-8')
84 | f_to.write(f_from.read())
85 | f_to.close()
86 | f_from.close()
87 |
--------------------------------------------------------------------------------
/lyrebird_tracking/server/data_manager.py:
--------------------------------------------------------------------------------
1 | from lyrebird_tracking.context import app_context
2 |
3 |
4 | def new_caseresult(result_content):
5 | """
6 | 新增case内容,处理断言结果的原始内容,对应用上下文进行更新
7 | :param result_content: 断言结果的原始内容
8 |
9 | """
10 | # 筛选出带目标key的子字典
11 | show_cell = dict((k, result_content[k])
12 | for k in ('result', 'id', 'name', 'groupid', 'groupname') if k in result_content)
13 | # 筛选出未测试的case,标记为NA,name为唯一标识进行筛选
14 | untested_list = list(filter(lambda x: x.get('result') == 'NA' and x.get(
15 | 'name') == result_content.get('name'), app_context.content))
16 | # 如果对应未测试的case存在
17 | if untested_list:
18 | untested_item = untested_list[0]
19 | untested_list_item = list(filter(lambda x: x.get('result') == 'NA' and x.get(
20 | 'name') == result_content.get('name'), app_context.result_list))[0]
21 | # 删除对应未测试case
22 | app_context.content.remove(untested_item)
23 | app_context.result_list.remove(untested_list_item)
24 | # 增加对应测试case,在列表展示数据内插入到列表index=0的位置
25 | app_context.result_list.insert(0, show_cell)
26 | app_context.content.append(result_content)
27 |
--------------------------------------------------------------------------------
/lyrebird_tracking/server/search_handler.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | class SearchHandler:
5 | """
6 | 搜索逻辑类
7 | 主要用于判断数据源是否符合配置规则
8 |
9 | """
10 |
11 | def __init__(self, *args):
12 | self.data = []
13 | self.data += args
14 | self.search_list = []
15 | self.subnodes = []
16 |
17 | def find(self, jsonpath):
18 | """
19 | 查询逻辑的入口函数,支持链式查询方式,如 SearchHandler(data).find(path1).find(path2)
20 | :param jsonpath: 用JSONPath描述的查询逻辑
21 | :return: SearchHandler对象,用于链式查询
22 |
23 | """
24 | self.subnodes = self.data
25 | self.search_list = self.transfer_query(jsonpath)
26 | for query in self.search_list:
27 | self._query_parser(query)
28 | return SearchHandler(*self.subnodes)
29 |
30 | def transfer_query(self, jsonpath):
31 | """
32 | 解析jsonpath语法函数
33 | 主要用正则进行拆分解析查询逻辑
34 | :param jsonpath: 用JSONPath描述的查询逻辑
35 | :return: 按查询顺序进行排列的查询语句数组
36 |
37 | """
38 | if '$' in jsonpath and not jsonpath.startswith('$'):
39 | return
40 | # split $ means root
41 | if jsonpath.startswith('$'):
42 | jsonpath = jsonpath[1:]
43 | # 以[...]为条件进行正则字符串切割
44 | pattern = re.compile(r'(\[.+?\])')
45 | source_list = re.split(pattern, jsonpath)
46 | query_list = []
47 | # 解析正则切割后的string list
48 | for item in source_list:
49 | # 如果是[...],直接append
50 | if item.startswith('['):
51 | query_list.append(item)
52 | # 如果是 property,以.切割成多个属性,extend到querylist里
53 | elif '.' in item:
54 | item_list = item.split('.')
55 | query_list.extend(item_list)
56 | else:
57 | query_list.append(item)
58 | # remove null item
59 | query_list = [x for x in query_list if x != '']
60 | return query_list
61 |
62 | def _query_parser(self, query_str):
63 | """
64 | 根据正则匹配,识别list或property
65 | :param query_str: 查询字符串
66 |
67 | """
68 | # 匹配 [...]
69 | if re.match(r'(\[.+?\])', query_str):
70 | self._query_list(query_str)
71 | else:
72 | self._query_property(query_str)
73 |
74 | def _query_list(self, query_str):
75 | """
76 | 根据正则匹配,识别三种类型的list模式
77 | :param query_str: 查询字符串
78 |
79 | """
80 | node_list = self.subnodes
81 | self.subnodes = []
82 | for obj in node_list:
83 | if not isinstance(obj, list):
84 | continue
85 | # match such as:[2]
86 | if re.match(r'(\[\d+?\])', query_str):
87 | self._query_list_by_index(obj, query_str)
88 | # match such as:[*]
89 | elif query_str == '[*]':
90 | self._query_list_no_index(obj, query_str)
91 | # match such as: [?key=val]
92 | elif re.match(r'(\[\?.+?\=.+?\])', query_str):
93 | self._query_list_kw(obj, query_str)
94 |
95 | def _query_list_by_index(self, node, query_str):
96 | # 从[333]取出index 333
97 | index_num = query_str.split('[')[1].split(']')[0]
98 | if len(node) - 1 >= int(index_num):
99 | self.subnodes.append(node[int(index_num)])
100 |
101 | def _query_list_no_index(self, node, query_str):
102 | # 将整个list extend 至 subnodes里
103 | self.subnodes.extend(node)
104 |
105 | def _query_list_kw(self, node, query_str):
106 | # handle such as :[?key1=val1&key2=val2&key3=val3]
107 | sword = query_str.split('?')[1].split(']')[0]
108 | q_list = sword.split('&')
109 |
110 | for item in node:
111 | if not isinstance(item, dict):
112 | continue
113 |
114 | flag = 1
115 | for q in q_list:
116 | key = q.split('=')[0]
117 | val = q.split('=')[1].strip("'")
118 | # handle value of int type
119 | if val.isnumeric():
120 | val = int(val)
121 | if item.get(key) != val:
122 | flag = 0
123 | break
124 | continue
125 | if flag == 1:
126 | self.subnodes.append(item)
127 |
128 | def _query_property(self, query_str):
129 | """
130 | 处理property模式,默认返回对应property的val
131 | :param query_str: 查询字符串
132 |
133 | """
134 | node_list = self.subnodes
135 | self.subnodes = []
136 | for obj in node_list:
137 | if not isinstance(obj, dict):
138 | continue
139 | if query_str not in obj:
140 | continue
141 | self.subnodes.append(obj[query_str])
142 |
--------------------------------------------------------------------------------
/lyrebird_tracking/server/validator.py:
--------------------------------------------------------------------------------
1 | import jsonschema
2 | from jsonschema import Draft4Validator
3 | import uuid
4 | from lyrebird_tracking.server.search_handler import SearchHandler
5 |
6 |
7 | class Verify:
8 | """
9 | 校验类
10 | 根据配置文件的校验规则进行校验
11 |
12 | """
13 |
14 | def check(self, elements_list, rule):
15 | """
16 | 校验函数
17 | 基于JSONSchea根据校验规则和输入的数据源进行校验,返回的数据会更新应用上下文,校验结果随之会更新在前端
18 | :param elements_list: list类型,待校验的数据列表
19 | :param rule: 校验规则
20 |
21 | """
22 | result_list = []
23 | checker = rule.get('asserts')
24 | for ele in elements_list:
25 | flag = True
26 | check_list = []
27 | for item in checker:
28 | field = item.get('field')
29 | schema = item.get('schema')
30 |
31 | # 判断实际有没有对应的字段
32 | search_list = [field]
33 | node = SearchHandler(ele)
34 | for i in range(len(search_list)):
35 | node = node.find(search_list[i])
36 | result = node.data
37 | if result:
38 | content = result[0]
39 | else:
40 | content = None
41 |
42 | # 错误提示标记hint
43 | hint = None
44 | # 如果schema为空,高亮,不校验,用特殊高亮区别标记出来
45 | if schema:
46 | result = Draft4Validator(schema).is_valid(content)
47 | try:
48 | jsonschema.validate(content, schema)
49 | except jsonschema.ValidationError as e:
50 | hint = e.message
51 | else:
52 | result = 2
53 |
54 | flag = flag * result
55 |
56 | if content:
57 | check_list.append(
58 | {'field': field, 'schema': schema, 'actual': content, 'flag': result, 'hint': hint})
59 | else:
60 | check_list.append({'field': field, 'schema': schema, 'actual': 'error!exists-false',
61 | 'flag': result, 'hint': 'The field is not exists!'})
62 |
63 | result_dict = dict((k, rule[k]) for k in ('name', 'groupid', 'groupname', 'selector') if k in rule)
64 | result_dict['asserts'] = check_list
65 | result_dict['id'] = str(uuid.uuid4())
66 | result_dict['content'] = ele
67 | if flag:
68 | result_dict['result'] = 'pass'
69 | else:
70 | result_dict['result'] = 'fail'
71 |
72 | result_list.append(result_dict)
73 |
74 | return result_list
75 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/component/banner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
41 |
42 |
44 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/component/filter-tag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Case filters : {{grouplist.length}} conditions
5 |
Edit
6 |
7 |
8 |
9 | {{item}}
10 |
11 |
12 |
13 | Clear All
14 | Cancel
15 | OK
16 |
17 |
18 |
19 |
20 |
21 |
112 |
118 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/component/main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
79 |
80 |
94 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/component/tracking-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Detail
5 |
6 |
7 |
8 |
9 |
10 |
11 | Check field - {{item.field}}
12 |
13 |
14 |
15 |
16 | Check field - {{item.field}}
17 |
18 |
19 |
20 | Check field - {{item.field}}
21 |
22 |
23 |
24 |
Field : {{item.field}}
25 |
Expect schema :
26 | No expected value
27 |
28 |
29 |
32 |
33 |
Expect schema :
34 |
35 |
36 |
Actual value :
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
184 |
185 |
197 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/component/tracking-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
136 |
137 |
152 |
--------------------------------------------------------------------------------
/lyrebird_tracking/static/js/main.js:
--------------------------------------------------------------------------------
1 | Vue.config.devtools = true;
2 |
3 | iview.lang('en-US');
4 |
5 | new Vue({
6 | el: '#app',
7 | data: {},
8 | components: {
9 | 'tracking': httpVueLoader('/ui/plugin/tracking/static/component/main.vue')
10 | }
11 | })
--------------------------------------------------------------------------------
/lyrebird_tracking/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lyrebird_tracking/tracking.py:
--------------------------------------------------------------------------------
1 | import gzip
2 | import json
3 | import time
4 |
5 | import lyrebird
6 | from lyrebird import HandlerContext, context
7 | from urllib.parse import urlparse
8 |
9 | from lyrebird_tracking.context import app_context
10 | from lyrebird_tracking.server import search, validate
11 |
12 |
13 | class TrackingHandler:
14 | """
15 | 处理Lyrebird数据流
16 | Trakcing校验数据来源,来自于Lyrebird的请求上下文
17 | """
18 | def handle(self, handler_context: HandlerContext):
19 | """
20 | 数据流处理函数,继承与Lyrebird的HandlerContext,会获取Lyrebird的请求上下文
21 | :param handler_context: 请求上下文,取request data进行筛选校验
22 | """
23 | url = handler_context.get_origin_url()
24 | if url:
25 | hostname = urlparse(url).hostname
26 | else:
27 | hostname = urlparse(handler_context.request.url).hostname
28 |
29 | # 获取配置文件的目标host列表,取自于config中的target
30 | if hostname in app_context.config.get('target'):
31 | # 判断是否为gzip类型,若是进行解压缩处理
32 | if 'Content-Encoding' in handler_context.request.headers and handler_context.request.headers.get(
33 | 'Content-Encoding') == 'gzip':
34 | reqs_data = json.loads(gzip.decompress(handler_context.request.data).decode())
35 | else:
36 | reqs_data = []
37 | # 取出配置文件的cases内容,进行查询和校验
38 | rule_list = app_context.config.get('cases')
39 | for rule in rule_list:
40 | # 根据配置文件的selector选择器配置,进行查询
41 | targets_list = search(reqs_data, rule.get('selector'))
42 | # 如果有匹配的结果,进行进一步的校验
43 | if targets_list:
44 | validate(rule, targets_list)
45 |
--------------------------------------------------------------------------------
/lyrebird_tracking/webui.py:
--------------------------------------------------------------------------------
1 | import json
2 | import lyrebird
3 | import os
4 | import codecs
5 | from flask import request, jsonify, Response, abort
6 | from lyrebird import context
7 | from lyrebird_tracking.server import tracking_init
8 | from lyrebird_tracking.context import app_context
9 | import shutil
10 |
11 |
12 | class Tracking(lyrebird.PluginView):
13 | """
14 | tracking插件视图
15 |
16 | """
17 | def index(self):
18 | """
19 | 插件首页
20 | """
21 | return self.render_template('index.html')
22 |
23 | def get_result(self):
24 | """
25 | 获取case列表API
26 | :return: case列表数据
27 | """
28 | return jsonify({'result': app_context.result_list})
29 |
30 | def get_content(self, id=''):
31 | """
32 | 获取校验详情API
33 | :param id: 对应uuid,每个case的唯一标识,根据id查询校验详情
34 | :return: 对应id的校验详情
35 | """
36 | for item in app_context.content:
37 | if item['id'] == id:
38 | return jsonify(item)
39 | return abort(400, 'Request not found')
40 |
41 | def save_report(self):
42 | """
43 | 保存测试报告API
44 | """
45 | report_data_path = os.path.join(os.path.dirname(__file__), 'report_template/data/report-data.js')
46 | with codecs.open(report_data_path, 'w+', 'utf-8') as f:
47 | f.write('var reportCaseData='+json.dumps({'result': app_context.result_list}, ensure_ascii = False))
48 | f.write('\n')
49 | f.write('var baseData='+json.dumps(app_context.config, ensure_ascii = False))
50 | f.write('\n')
51 | f.write('var detailCollection='+json.dumps(app_context.content, ensure_ascii = False))
52 | f.write('\n')
53 | report_path = os.path.join(os.path.dirname(__file__), 'report_template')
54 | target_path = os.path.abspath(os.path.join(lyrebird.get_plugin_storage(), 'report'))
55 | if os.path.exists(target_path):
56 | shutil.rmtree(target_path)
57 | shutil.copytree(report_path, target_path)
58 |
59 | return context.make_ok_response()
60 |
61 | def clear_result(self):
62 | """
63 | 清空测试缓存API
64 | 需要进行初始化,并且发送socketio消息给前端重新load页面
65 | """
66 | app_context.result_list = []
67 | app_context.content = []
68 | tracking_init()
69 | context.application.socket_io.emit('update', namespace='/tracking-plugin')
70 | return context.make_ok_response()
71 |
72 | def get_base_info(self):
73 | """
74 | 获取基准文件信息API
75 | 主要用于分组筛选
76 | :return: 基准文件原始数据
77 | """
78 | return jsonify(app_context.config)
79 |
80 | def groups(self):
81 | """
82 | 获取选中的case分组;初始返回所有分组
83 | """
84 | return jsonify(app_context.select_groups)
85 |
86 | def select(self):
87 | """
88 | 更新选中的case分组
89 | """
90 | grouplist = request.json.get('group')
91 | app_context.select_groups = grouplist
92 | return context.make_ok_response()
93 |
94 |
95 | def on_create(self):
96 | """
97 | 插件初始化函数
98 | """
99 | # tracking 初始化
100 | tracking_init()
101 | # 设置模板文件目录(可选,设置静态文件目录)
102 | self.set_template_root('lyrebird_tracking')
103 | self.add_url_rule('/', view_func=self.index)
104 | self.add_url_rule('/result', view_func=self.get_result)
105 | self.add_url_rule('/content/', view_func=self.get_content)
106 | self.add_url_rule('/report', view_func=self.save_report)
107 | self.add_url_rule('/clear', view_func=self.clear_result)
108 | self.add_url_rule('/base', view_func=self.get_base_info)
109 | self.add_url_rule('/group', view_func=self.groups)
110 | self.add_url_rule('/select', view_func=self.select, methods=['POST'])
111 |
112 |
113 | def get_icon(self):
114 | """
115 | 设置展示在边栏的图标
116 | :return: 返回图标样式
117 | """
118 | return 'fa fa-fw fa-line-chart'
119 |
120 | def default_conf(self):
121 | """
122 | 设置默认的 conf.json
123 | :return: 返回 conf.json 内容
124 | """
125 | # 读取插件 conf.json 返回
126 | conf_path = os.path.dirname(__file__) + '/conf.json'
127 | with codecs.open(conf_path, 'r', 'utf-8') as f:
128 | return json.load(f)
129 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | lyrebird
2 | jsonschema
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | import os
3 |
4 | here = os.path.abspath(os.path.dirname(__file__))
5 |
6 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
7 | long_description = f.read()
8 |
9 | setup(
10 | name='lyrebird-tracking',
11 | version='0.11.2',
12 | packages=['lyrebird_tracking'],
13 | url='https://github.com/meituan/lyrebird-tracking',
14 | author='HBQA',
15 | long_description=long_description,
16 | long_description_content_type="text/markdown",
17 | include_package_data=True,
18 | zip_safe=False,
19 | classifiers=(
20 | "Programming Language :: Python :: 3 :: Only",
21 | "Programming Language :: Python :: 3.6",
22 | "License :: OSI Approved :: MIT License",
23 | "Operating System :: MacOS",
24 | ),
25 | entry_points={
26 | 'console_scripts': [
27 | ],
28 | 'lyrebird_data_handler': [
29 | 'tracking = lyrebird_tracking.tracking:TrackingHandler'
30 | ],
31 | 'lyrebird_web': [
32 | 'tracking = lyrebird_tracking.webui:Tracking'
33 | ]
34 | },
35 | install_requires=[
36 | 'lyrebird==1.8.7',
37 | 'jsonschema'
38 | ]
39 |
40 | )
41 |
--------------------------------------------------------------------------------
/tests/test_filter_error.py:
--------------------------------------------------------------------------------
1 | from lyrebird_tracking.server.__init__ import filter_error_msg
2 | import operator
3 |
4 | result_dic = {
5 | 'name': 'a',
6 | 'asserts': [{
7 | 'field': 'a',
8 | 'flag': False,
9 | 'hint': "x"
10 | }, {
11 | 'field': 'b',
12 | 'flag': False,
13 | 'hint': 'y'
14 | }, {
15 | 'field': 'c',
16 | 'flag': True,
17 | 'hint': "z"
18 | }],
19 | 'content': {
20 | 'a': 1
21 | },
22 | }
23 |
24 | excepted_error = {
25 | 'name': 'a',
26 | 'content': {
27 | 'a': 1
28 | },
29 | 'error_msg': "name: a\nerror message:\nfield: a\nerror detail: x\nfield: b\n"
30 | "error detail: y\n"
31 | }
32 |
33 |
34 | def test_filter_error():
35 | error_message = dict((k, result_dic[k]) for k in ('name', 'content') if k in result_dic)
36 | error_message['error_msg'] = filter_error_msg(result_dic)
37 | assert operator.eq(excepted_error, error_message) is True
38 |
--------------------------------------------------------------------------------
/tests/test_select.py:
--------------------------------------------------------------------------------
1 | from lyrebird_tracking.server.search_handler import SearchHandler
2 |
3 | data_list = [
4 | {
5 | "a": 3,
6 | "b": [
7 | {
8 | "x": "s",
9 | "y": [{
10 | "n": "test",
11 | "m": "haha"
12 | }, {
13 | "n": "notest",
14 | "m": "heihei"
15 | }]
16 | },
17 | {
18 | "x": "t",
19 | "y": "ooo"
20 | }, {
21 | "x": "st",
22 | "m": 123
23 | }
24 | ]
25 | }, {
26 | "a": 3,
27 | "b": [{
28 | "x": "st",
29 | "y": 456
30 | }, {
31 | "x": "st",
32 | "y": 789
33 | }, {
34 | "x": "s",
35 | "y": 123
36 | }]
37 | }
38 | ]
39 |
40 | raw_data = {
41 | "haha": [
42 | {
43 | 'a': 'p1',
44 | 'b': 'pp',
45 | 'evs':
46 | [
47 | {
48 | 'b': 'p2',
49 | 'x': {
50 | 'y': {
51 | 'z': [
52 | {
53 | 'c': 'p3',
54 | 'core': 'gq'
55 | },
56 | {
57 | 'c': 'p2',
58 | 'core': 'gq1'
59 | }
60 | ]
61 | }
62 | }
63 | },
64 | {
65 | 'b': 'p2',
66 | 'x': {
67 | 'y': {
68 | 'z': [
69 | {
70 | 'c': 'p33',
71 | 'core': 'gq3'
72 | }
73 | ]
74 | }
75 | }
76 | }
77 | ]
78 | },
79 | {'a': 'p1', 'b': 'p2','evsmock': []},
80 | [1, 'a'],
81 | 1,
82 | 'b'
83 | ],
84 | "heihei": [{'a': 'p1', 'evs': []}],
85 | "lala": 123
86 | }
87 |
88 |
89 | def test_transfer_query():
90 | jsonpath_str = "$[?ca='travel'].evs[?vb='yf']"
91 | search_list = SearchHandler().transfer_query(jsonpath_str)
92 | assert search_list == ["[?ca='travel']", "evs", "[?vb='yf']"]
93 |
94 | jsonpath_str = "$haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']"
95 | search_list = SearchHandler().transfer_query(jsonpath_str)
96 | assert search_list == ["haha", "[?a='p1']", "evs", "[?b='p2']", "x", "y", "z", "[?c='p3']"]
97 |
98 |
99 | def test_query1():
100 | jsonpath_str = "$haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']"
101 | node = SearchHandler(raw_data)
102 | result = node.find(jsonpath_str).data
103 | assert len(result) == 1
104 | assert result[0]['core'] == 'gq'
105 |
106 |
107 | def test_query2():
108 | jsonpath_str = "haha[?a='p1'].evs[?b='p2'].x.y.z[?c='p3']"
109 | node = SearchHandler(raw_data)
110 | result = node.find(jsonpath_str).data
111 | assert len(result) == 1
112 | assert result[0]['core'] == 'gq'
113 |
114 |
115 | def test_query3():
116 | jsonpath_str = "$haha[?a='p1'].$evs[?b='p2'].x.y.z[?c='p3']"
117 | node = SearchHandler(raw_data)
118 | result = node.find(jsonpath_str).data
119 | assert len(result) == 0
120 |
121 |
122 | def test_query4():
123 | jsonpath_str = "$[?a=3].b[?x='s']"
124 | node = SearchHandler(data_list)
125 | result = node.find(jsonpath_str).data
126 | assert len(result) == 2
127 | assert result[1]['y'] == 123
128 | assert result[0]['y'][0]['n'] == 'test'
129 |
130 |
131 | def test_query5():
132 | jsonpath_str = "$[*].b[?x='st']"
133 | node = SearchHandler(data_list)
134 | result = node.find(jsonpath_str).data
135 | assert len(result) == 3
136 | assert result[0]['m'] == 123
137 | assert result[1]['y'] == 456
138 | assert result[2]['y'] == 789
139 |
140 |
141 | def test_query6():
142 | jsonpath_str = "$[2].b[?x='st']"
143 | node = SearchHandler(data_list)
144 | result = node.find(jsonpath_str).data
145 | assert len(result) == 0
146 |
147 |
148 | def test_query7():
149 | jsonpath_str = "$[1].b[?x='st']"
150 | node = SearchHandler(data_list)
151 | result = node.find(jsonpath_str).data
152 | assert len(result) == 2
153 | assert result[0]['y'] == 456
154 | assert result[1]['y'] == 789
155 |
156 |
157 | def test_query8():
158 | jsonpath_str = "$haha[?a='p1'].evs[*].x.y.z[?c='p3']"
159 | node = SearchHandler(raw_data)
160 | result = node.find(jsonpath_str).data
161 | assert len(result) == 1
162 | assert result[0]['core'] == 'gq'
163 |
164 |
165 | def test_query9():
166 | jsonpath_str = "$haha[?a='p1'].evs[*].x.y.z"
167 | node = SearchHandler(raw_data)
168 | result = node.find(jsonpath_str).data
169 | assert len(result) == 2
170 | assert result[1][0]['c'] == 'p33'
171 |
172 |
173 | def test_query10():
174 | jsonpath_str = "$haha[?a='p1'&b='pp'].evs[*].x.y.z"
175 | node = SearchHandler(raw_data)
176 | result = node.find(jsonpath_str).data
177 | assert len(result) == 2
178 | assert result[1][0]['c'] == 'p33'
--------------------------------------------------------------------------------