...
 
Commits (83)
    https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/52bea414e7a5b9743e0990f2ec870431f378ba9b Merge pull request #11 from amazingTest/dev 2019-12-29T14:30:58+00:00 Taisite 523314409@qq.com [feat](project)项目内在页面顶部看到项目名称 https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/164eb7d2e1fcc09924242fa0b5b4419a5c104b3c [refact](dist)重新打包dist 2019-12-29T14:40:36+00:00 shaoyuyishiwo 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/4abffbadabfe46ec54549e0364372cdc32b6c186 Update README_CN.md 2020-01-31T10:03:20+00:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/dc17f5452ff1752e5743e945bf05438414a62c56 report-view-color-fix 2020-02-04T15:46:34+08:00 nogit websnaile@qq.com report-view-color-fix https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/80b384e8f4df0aa569c5f6e2515dc9bdb55f710c Merge pull request #1 from nogit/nogit-patch-1-report-view-color-fix 2020-02-04T15:52:48+08:00 nogit websnaile@qq.com report-view-color-fix https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/8a9de41533e80bc4e3488257c2fa708a08c77857 Merge pull request #12 from nogit/master 2020-02-04T09:00:51+00:00 Taisite 523314409@qq.com report-view-color-fix https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/815a535644486e9834072fd3d6976540193c6a02 [refact](dist)重新打包dist 2020-02-04T09:19:25+00:00 shaoyuyishiwo 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/58ed5dc538ceac76f5735213dca6203a08142a4e Update README_CN.md 2020-03-02T09:25:51+00:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/58c724721c771ba6041fe2f33243164a865148d3 Update README_CN.md 2020-04-22T22:07:23+01:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/548c0f7c73a95104e293cda5f4850e9de10c4509 Update README.md 2020-04-22T22:14:21+01:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f51992b8696f1bc31bb7110132806516ad718234 Update README_CN.md 2020-04-23T09:48:03+01:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/7f4704112b0b79c1101eaa8e9cb9fa589d777540 Create FUNDING.yml 2020-06-12T18:42:21+08:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/5e381ee2fb41f25824e791c79b1382dda0f9c0c7 Update FUNDING.yml 2020-06-12T18:43:32+08:00 Taisite 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/49c6f6f5d2125dbfcba56003a820f0652fb74e81 [refact]支持对非json格式返回的正则校验 2020-09-05T11:27:34+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/5936a88863493f79d3011ece134e8d2f0024b340 [fix]修复get请求下全局变量替换参数 2020-09-05T11:29:27+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/1a3b90bc188b3a445b243b07933b60860177aab5 [doc]add ignore 2020-09-12T12:35:26+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/18603ca2a592bbe506eab02e0ceda53fce1ca23e [feat]实际结果&测试结论限制字符长度 2020-09-12T12:38:21+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/c2f056b8e0b6a74554f89e2826e64e57ec25e246 [fix]修复邮箱判断 2020-09-12T12:39:22+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f499f52aed50b43ad91611067c3de8d6f0b909c9 [fix]关闭debug模式 2020-09-12T12:39:47+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/14a7c54770827ac0293561adfeae9c2035b8f848 [feat]正则查询语句支持全局替换 2020-09-12T12:43:39+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/611fb04b10e4bf130d25f989268a7b6c914d687a [tips]预先打包好dist 2020-09-12T13:25:17+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/337343a286751963433ca8edb25518bb28465a0c [extra]为使用者预先打包好dist 2020-09-12T13:26:42+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/942e066d50e37fffb783a75981d956912d6688b5 [fix]修复html正则验证 2020-09-12T16:07:02+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/1abda419e15a97a96011849a2fba89705a24b067 [fix]修复对数组直接进行正则断言 2020-09-20T18:33:23+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/1a67ccf86b6c4468850e116daa733d6d855b275f [fix]修复导出用例未按照用例组排序 2020-09-20T18:34:17+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/a63595533efc38af4eb4ccdcd011b65cfd874d60 [refact]提高性能 2020-09-26T10:27:40+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f11208dfcde9f1cc5043d536c3b3992c4d9daf51 [feat]新增导出报告接口 2020-09-26T10:30:26+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/213925d61544c488a7e7fdfac543e73f8b1f71fa [feat]新增导出报告接口api 2020-09-26T10:32:32+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f8ac3b83fcd38a4758cd4a5fd42c60e7eb28efbc [fix]修复报告请求参数过长问题 2020-09-26T10:34:10+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f3de76bb3607d060f6485d3b53a1ef58b57afb09 [feat]新增报告导出按钮 2020-09-26T10:35:08+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/c6ddca8065e13955ce260f1cffc7018855f4cd7c [feat]报告新增测试总耗时&测试环境&项目名称 2020-09-26T10:38:03+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/979fe67d0ee420c38e9911fabdc2593de19fec44 [feat]新增测试总耗时&测试环境&项目名称字段 2020-09-26T10:39:20+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/65149b56466601d3bf793115cfb7162c27038672 [refact]兼容单个用例下测试总耗时计算 2020-09-26T10:40:42+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/ab84fb72bb37b73d462a10fa1317bfb63db0765c [refact]兼容定时任务下测试总耗时计算 2020-09-26T10:42:01+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/92597b8ca18853e54526bf75faa1008e6cf77262 [extra]为使用者打包好dist 2020-09-26T10:46:10+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/cc5280732d58823d3a204679ba91104b4ac31599 [fix]fix prop 2020-09-26T10:55:42+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/83585cac9de9356c3af09de3a83d669f9d5c39b2 [extra]为使用者打包好最新dist 2020-09-26T10:59:10+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/904f78d636be727bdc0e057f09a90a9b3075caca [extra]为使用者打包好最新dist 2020-09-26T14:27:51+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/508b47a7bfc77ee8b105d53d23389bf04495c74b [fix] fix sender_id 2020-10-31T10:44:22+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/e4d11616200cee220aad01309594e6bd07f22972 [feat]新增定时任务名称记录 2020-10-31T11:07:45+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/ff64f9afc42f8c9dd36eb1a52e27ae36c5257912 [refact] 重构 export_report 接口 2020-10-31T11:08:33+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/9f7135a27265dfc3fa343d9b3b1dbfd3b81c9a80 [feat]新增get_test_report_excel_bytes_io方法 2020-10-31T11:10:15+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/f4aa2d1227292371232cbdd2e31e8919e03ccc1d [feat] 新增定时任务名称记录 2020-10-31T11:11:58+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/2fc4b242adc2a438256129e086e794ec5f05a57a [feat] 新增邮件 & 企业微信发送详细测试报告内容 2020-10-31T11:12:19+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/faae25efd1499f23d81beeef4e3554cb83bab025 [feat] 邮件发送支持传送附件 2020-10-31T11:14:03+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/0c33c357022c21eb470252ca186270cf477c2035 [feat] 邮件发送支持传送附件 2020-10-31T11:14:39+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/64f10c5719d0c84081506fbb494057512f54008c [refact]新增邮箱发件人配置placeholder 2020-10-31T12:19:57+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/37514035df1cf45fdb26ca7aa6a5c0a0cc2d6bfd [extra]打包好最新dist 2020-10-31T12:21:31+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/4619c155ef1bbb0bda40fb8626331dd83d337886 [feat]新增 generate_curl 2020-11-08T11:54:04+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/5359d5ee70b574a2debf8cc6f5cedd65c32800f0 [feat] 新增 helpers 2020-11-08T11:54:33+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/dd5adb384f98dc7bf01c8327548d182f8fa30afe [feat]记录下 curl 2020-11-08T11:55:25+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/6329b1ebcfdd45538c6a47c7dc120c537368885d [feat] 2020-11-08T11:56:18+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/b849580fac6f3729fa7c2beea5443a87b5b8ad97 [feat]定时任务智能告警&恢复提醒功能 2020-11-08T11:58:28+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/393717189d9e843815bf59b60ab4f05abf88f65c [doc]添加项目更新日志 2020-11-10T16:24:30+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/3c20572497a306007b8ee3a879defa6f8063c0f4 [feat]新增 resolve_fake_var 方法 2020-11-13T21:57:04+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/97c5ae35e58e3ba0b26990bb9199e3a40f88e333 [feat]支持在请求参数中调用 faker 开源库 2020-11-13T21:58:27+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/8f220cf52db110bb08f0c11455c7c4d44cf364dc [requirements]新增 Faker==4.14.2 2020-11-13T22:00:01+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/83956c7bf4eb5b10995a6ab97cd0d7d3253aa91c [readme] 2020-11-17T12:16:46+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/91c10ac771025182c98f492fb7bd2c361bbc4bf9 [feat] test_report_detail_map 新增 checkResponseTime 2020-11-21T14:17:38+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/c1b79792d3d24b579b6f2af5147ea35dbce41bab [feat] 新增接口响应耗时断言功能 2020-11-21T14:18:43+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/07c54e229af227b6c29616dbd463687e8c77331c [feat] test_case_map 新增 checkResponseTime 字段 2020-11-21T14:20:29+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/1eefb84066f770a008ab9928c7099974c3bd6153 [feat] 新增 checkResponseTime 字段 2020-11-21T14:22:03+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/8c51277dda77af3795086e83287f47d7f0d4f27d [feat] 前端新增测试耗时断言设置 2020-11-21T14:23:56+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/60cc48f9db52a1af151b427f226c4035fc420b12 [feat]测试报告新增测试耗时 2020-11-21T14:25:28+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/4ec53956a59c0c81be800ef9f131272451134719 [extra]打包好最新dist 2020-11-21T14:29:04+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/d9f0c9f8a5a8e760267b2646589c90e3b5e6d188 [refact]默认不开启nlp 2020-11-21T14:34:29+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/ae5cc0bfba08c4e50af3e688c2270c81088989bc [feat]新增 testDataStorage 2021-01-16T09:51:20+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/547d7913c4fa736cbf39f6b35cc585d6a8681014 testDataStorage.py 2021-01-16T09:54:21+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/5cd42b3847b5fed3b86634ce59607e94cb1630fe add datastorage model 2021-01-16T09:55:33+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/394e2afa211e57f576cf06c0c5a6c9e354f7c61a [faet] start test 新增数据字典参数 2021-01-16T10:00:15+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/0dae07a56f4d59ecb50f4c2e7fee5361e5caadf3 [fix] 修复测试结果乱码 2021-01-16T10:04:42+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/393c9228923aeb24d81e32b570d7054cc1952539 [fix]修复异常情况下 curl 中 headers 的生成 2021-01-16T10:05:34+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/4ea5515b8ed4f0e2cf92dcf4fcbd6dbd252e1f6c [feat]新增 global_vars 传参 2021-01-16T10:07:10+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/fb822ac22a3970202940673899b5cdd82bb56b6c [feat]add storage router 2021-01-16T10:21:16+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/4e2f41f0da7c4840e5d9524d91276c5ee449eec2 [feat]add storage api 2021-01-16T10:22:56+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/fa4181338e8ca6017c9d3fa35e3c9741abbd12f4 [feat]新增前端数据字典测试入口 2021-01-16T10:29:40+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/ef0e43d981bedb943263e465163e4eeeb60d5a39 [feat]新增数据仓库页面 2021-01-16T10:31:07+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/7eb40462236e6c47bb594f7b58c0326db70a5984 [extra]打包好dist 2021-01-16T10:37:06+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/88eaa183646fb1fc6381646caa106baa5bad8e27 [extra]打包好 dist 2021-01-16T10:41:00+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/e7f865184f4facae4fff1b76b6b35acff295b79e [fix]about autor 2021-01-16T10:42:45+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/6fd5611b3c861e6774b7ecd7f3b4d5b56e941f28 [doc] 2021-01-16T10:48:51+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/fea1dfdff774df8c37fb83652459d6b78f62000b add ignore 2021-01-16T10:59:29+08:00 amazingTest 523314409@qq.com https://gitcode.net/weixin_41908648/Taisite-Platform/-/commit/5f11c8c20686c03bee2c3b1d497afe313012d96b [idea] 2021-01-16T11:01:50+08:00 amazingTest 523314409@qq.com
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # https://github.com/amazingTest/Taisite-Platform/blob/master/images/wechatDonation.jpg
.idea/.xml
\ No newline at end of file
.idea/.xml
.DS_Store
venv
.idea
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/frontend/node_modules" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.6 (Taisite-Platform)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="R User Library" level="project" />
<orderEntry type="library" name="R Skeletons" level="application" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="Werkzeug" />
<item index="1" class="java.lang.String" itemvalue="cryptography" />
<item index="2" class="java.lang.String" itemvalue="prompt-toolkit" />
<item index="3" class="java.lang.String" itemvalue="urllib3" />
<item index="4" class="java.lang.String" itemvalue="click" />
<item index="5" class="java.lang.String" itemvalue="Jinja2" />
<item index="6" class="java.lang.String" itemvalue="Flask" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (Taisite-Platform)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BranchesTreeState">
<expand>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="GROUP_NODE:origin" type="e8cecc67:BranchNodeDescriptor" />
</path>
</expand>
<select />
</component>
<component name="ChangeListManager">
<list default="true" id="3eea9fa2-78aa-48ef-980b-e40ac93966bd" name="Default" comment="">
<change beforePath="$PROJECT_DIR$/backend/app/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/app/__init__.py" afterDir="false" />
<list default="true" id="3eea9fa2-78aa-48ef-980b-e40ac93966bd" name="Default" comment="[fix](dist)修复 dist 部分文件缺失问题">
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/Taisite-Platform.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/Taisite-Platform.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="TRACKING_ENABLED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileEditorManager">
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
<file leaf-file-name="tester.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/backend/testframe/interfaceTest/tester.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="330">
<caret line="38" column="12" selection-start-line="38" selection-start-column="12" selection-end-line="38" selection-end-column="12" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="Project.vue" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/frontend/src/views/Project.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-870">
<caret line="1" column="27" selection-start-line="1" selection-start-column="27" selection-end-line="1" selection-end-column="27" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="__init__.py" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/backend/app/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="30">
<caret line="20" column="27" lean-forward="true" selection-end-line="45" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="testReport.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/backend/controllers/testReport.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="240">
<caret line="13" column="15" lean-forward="true" selection-start-line="13" selection-start-column="15" selection-end-line="13" selection-end-column="15" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="common.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/backend/utils/common.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-2700">
<caret line="107" selection-start-line="107" selection-end-line="107" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="CronList.vue" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/frontend/src/views/interfaceTestProject/api/automation/CronList.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="5250">
<caret line="895" column="19" selection-start-line="895" selection-start-column="19" selection-end-line="895" selection-end-column="19" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name=".gitignore" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/.gitignore">
<provider selected="true" editor-type-id="text-editor">
<state>
<caret column="10" selection-start-column="10" selection-end-column="10" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="Dockerfile.backend" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/Dockerfile.backend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="330">
<caret line="38" column="63" selection-start-line="38" selection-start-column="38" selection-end-line="38" selection-end-column="63" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="Header.vue" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/frontend/src/views/common/Header.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-3090">
<caret line="29" column="29" selection-start-line="29" selection-start-column="29" selection-end-line="29" selection-end-column="29" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="Home.vue" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/frontend/src/views/Home.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="60">
<caret line="2" column="5" selection-start-line="2" selection-start-column="5" selection-end-line="2" selection-end-column="5" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="IdeDocumentHistory">
<option name="CHANGED_PATHS">
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="$PROJECT_DIR$/README.md" />
<option value="$PROJECT_DIR$/.gitignore" />
<option value="$PROJECT_DIR$/frontend/src/views/common/Header.vue" />
<option value="$PROJECT_DIR$/backend/testframe/interfaceTest/tester.py" />
<option value="$PROJECT_DIR$/backend/controllers/testReport.py" />
<option value="$PROJECT_DIR$/backend/app/__init__.py" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="ProjectFrameBounds">
<option name="x" value="122" />
<option name="y" value="55" />
<option name="width" value="1741" />
<option name="height" value="926" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectView">
<navigator proportions="" version="1">
<foldersAlwaysOnTop value="true" />
</navigator>
<panes>
<pane id="ProjectPane">
<subPane>
<expand>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
<item name="app" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
<item name="controllers" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
<item name="testframe" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
<item name="testframe" type="462c0819:PsiDirectoryNode" />
<item name="interfaceTest" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="backend" type="462c0819:PsiDirectoryNode" />
<item name="utils" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="frontend" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="frontend" type="462c0819:PsiDirectoryNode" />
<item name="src" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="Taisite-Platform" type="b2602c69:ProjectViewProjectNode" />
<item name="Taisite-Platform" type="462c0819:PsiDirectoryNode" />
<item name="frontend" type="462c0819:PsiDirectoryNode" />
<item name="static" type="462c0819:PsiDirectoryNode" />
</path>
</expand>
<select />
</subPane>
</pane>
<pane id="Scope" />
</panes>
<component name="ProjectId" id="1eqs9PwD6ECqbnVFYx8eAddcJoP" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
<component name="PropertiesComponent">
<property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="TERMINAL_CUSTOM_COMMANDS_GOT_IT" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/frontend/src/views/interfaceTestProject/global" />
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
</component>
<component name="RunManager">
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/frontend/src/views/interfaceTestProject/global" />
<recent name="$PROJECT_DIR$/frontend/src/api" />
<recent name="$PROJECT_DIR$/backend/models" />
<recent name="$PROJECT_DIR$/backend/controllers" />
<recent name="$PROJECT_DIR$/backend/utils" />
</key>
</component>
<component name="RunManager" selected="Python.run">
<configuration name="Unnamed" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/venv/bin/python" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="common" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/backend/utils" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/backend/utils/common.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="config" type="PythonConfigurationType" factoryName="Python" temporary="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
......@@ -228,11 +122,115 @@
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="run" type="PythonConfigurationType" factoryName="Python">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="AUTOTEST_PLATFORM_MONGO_HOST" value="10.0.0.90" />
<env name="AUTOTEST_PLATFORM_MONGO_PORT" value="27017" />
<env name="AUTOTEST_PLATFORM_MONGO_USERNAME" value="osr_190403" />
<env name="AUTOTEST_PLATFORM_MONGO_PASSWORD" value="7xd36KH8Y21j42j" />
<env name="AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME" value="btp_taisite_test" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/backend" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/backend/run.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="t" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/backend/testframe/interfaceTest" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/backend/testframe/interfaceTest/t.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/backend/testframe" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/backend/testframe/test.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="tester" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="Taisite-Platform" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/backend/testframe/interfaceTest" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/backend/testframe/interfaceTest/tester.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<list>
<item itemvalue="Python.Unnamed" />
<item itemvalue="Python.run" />
<item itemvalue="Python.config" />
<item itemvalue="Python.t" />
<item itemvalue="Python.tester" />
<item itemvalue="Python.test" />
<item itemvalue="Python.common" />
</list>
<recent_temporary>
<list>
<item itemvalue="Python.test" />
<item itemvalue="Python.common" />
<item itemvalue="Python.config" />
<item itemvalue="Python.config" />
<item itemvalue="Python.tester" />
<item itemvalue="Python.t" />
</list>
</recent_temporary>
</component>
......@@ -247,438 +245,117 @@
<option name="presentableId" value="Default" />
<updated>1566002854371</updated>
</task>
<task id="LOCAL-00001" summary="add gitignore">
<created>1577626125510</created>
<task id="LOCAL-00001" summary="[fix](dist)修复 dist 部分文件缺失问题">
<created>1574687777309</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1577626125510</updated>
</task>
<task id="LOCAL-00002" summary="untrack">
<created>1577627205616</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1577627205616</updated>
<updated>1574687777309</updated>
</task>
<task id="LOCAL-00003" summary="Merge remote-tracking branch 'remotes/origin/master' into dev&#10;&#10;# Conflicts:&#10;#&#9;.idea/misc.xml&#10;#&#9;.idea/workspace.xml&#10;#&#9;dist/index.html&#10;#&#9;dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map&#10;#&#9;frontend/package.json&#10;#&#9;frontend/src/views/Home.vue&#10;#&#9;frontend/src/views/Project.vue">
<created>1577627970938</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1577627970938</updated>
</task>
<task id="LOCAL-00004" summary="Merge remote-tracking branch 'remotes/origin/master' into dev&#10;&#10;# Conflicts:&#10;#&#9;.idea/misc.xml&#10;#&#9;.idea/workspace.xml&#10;#&#9;dist/index.html&#10;#&#9;dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map&#10;#&#9;frontend/package.json&#10;#&#9;frontend/src/views/Home.vue&#10;#&#9;frontend/src/views/Project.vue">
<created>1577628667894</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1577628667894</updated>
</task>
<option name="localTasksCounter" value="5" />
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="ToolWindowManager">
<frame x="122" y="55" width="1741" height="926" extended-state="0" />
<editor active="true" />
<layout>
<window_info anchor="bottom" id="TODO" order="6" />
<window_info anchor="bottom" id="Event Log" order="7" sideWeight="0.3930435" side_tool="true" visible="true" weight="0.42469135" />
<window_info anchor="bottom" id="Run" order="2" weight="0.38395062" />
<window_info anchor="bottom" id="Version Control" order="7" sideWeight="0.49971014" weight="0.32962963" />
<window_info anchor="bottom" id="Python Console" order="7" />
<window_info anchor="right" id="PTest View" order="3" />
<window_info anchor="bottom" id="Terminal" order="7" sideWeight="0.60695654" visible="true" weight="0.42469135" />
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.17101449" />
<window_info anchor="right" id="R Packages" order="3" />
<window_info anchor="right" id="R Graphics" order="3" />
<window_info id="Structure" order="1" side_tool="true" weight="0.25" />
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
<window_info id="Favorites" order="2" side_tool="true" />
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
<window_info anchor="bottom" id="Run" order="2" weight="0.32938075" />
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.17101449" />
<window_info id="Structure" order="1" side_tool="true" weight="0.25" />
<window_info anchor="right" id="Commander" order="0" weight="0.4" />
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
<window_info anchor="bottom" id="TODO" order="6" />
<window_info anchor="bottom" id="Version Control" order="7" />
<window_info anchor="right" id="R Packages" order="3" />
<window_info anchor="right" id="R Graphics" order="3" />
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
<window_info anchor="bottom" id="Message" order="0" />
<window_info anchor="bottom" id="Event Log" order="7" side_tool="true" />
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
<window_info id="Favorites" order="2" side_tool="true" />
<window_info anchor="bottom" id="Find" order="1" />
<window_info anchor="bottom" id="Terminal" order="7" visible="true" weight="0.29753086" />
<window_info anchor="right" id="PTest View" order="3" />
<window_info anchor="bottom" id="Python Console" order="7" />
</layout>
</component>
<component name="VcsContentAnnotationSettings">
<option name="myLimit" value="2678400000" />
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="add gitignore" />
<MESSAGE value="untrack" />
<MESSAGE value="Merge remote-tracking branch 'remotes/origin/master' into dev&#10;&#10;# Conflicts:&#10;#&#9;.idea/misc.xml&#10;#&#9;.idea/workspace.xml&#10;#&#9;dist/index.html&#10;#&#9;dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map&#10;#&#9;frontend/package.json&#10;#&#9;frontend/src/views/Home.vue&#10;#&#9;frontend/src/views/Project.vue" />
<option name="LAST_COMMIT_MESSAGE" value="Merge remote-tracking branch 'remotes/origin/master' into dev&#10;&#10;# Conflicts:&#10;#&#9;.idea/misc.xml&#10;#&#9;.idea/workspace.xml&#10;#&#9;dist/index.html&#10;#&#9;dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map&#10;#&#9;frontend/package.json&#10;#&#9;frontend/src/views/Home.vue&#10;#&#9;frontend/src/views/Project.vue" />
<MESSAGE value="[fix](dist)修复 dist 部分文件缺失问题" />
<option name="LAST_COMMIT_MESSAGE" value="[fix](dist)修复 dist 部分文件缺失问题" />
</component>
<component name="editorHistoryManager">
<entry file="file://$PROJECT_DIR$/frontend/src/views/Home.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="60">
<caret line="2" column="5" selection-start-line="2" selection-start-column="5" selection-end-line="2" selection-end-column="16" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/Project.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-780">
<caret line="4" column="36" selection-start-line="4" selection-start-column="36" selection-end-line="4" selection-end-column="36" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/utils/cookie.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-1350" />
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/api/testCase.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-1440" />
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/utils/request.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-180" />
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.frontend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="420">
<caret line="14" lean-forward="true" selection-start-line="14" selection-end-line="14" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.backend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="570">
<caret line="19" column="40" selection-start-line="19" selection-start-column="40" selection-end-line="19" selection-end-column="40" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/config.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1822">
<caret line="61" column="49" selection-start-line="61" selection-start-column="49" selection-end-line="61" selection-end-column="49" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.frontend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="300">
<caret line="10" column="69" lean-forward="true" selection-start-line="10" selection-start-column="7" selection-end-line="10" selection-end-column="69" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/app/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-62">
<caret line="17" column="51" lean-forward="true" selection-start-line="17" selection-start-column="51" selection-end-line="17" selection-end-column="51" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/AppData/Local/Programs/Python/Python36-32/Lib/re.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="6810">
<caret line="230" column="4" selection-start-line="230" selection-start-column="4" selection-end-line="230" selection-end-column="4" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.backend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1170">
<caret line="39" selection-start-line="39" selection-end-line="39" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README_CN.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="2910">
<caret line="97" column="18" selection-start-line="97" selection-start-column="18" selection-end-line="97" selection-end-column="18" />
</first_editor>
<second_editor />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="37">
<caret line="280" selection-start-line="280" selection-end-line="280" />
</first_editor>
<second_editor />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/controllers/user.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="180">
<caret line="12" selection-start-line="12" selection-end-line="12" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.frontend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="300">
<caret line="10" column="69" lean-forward="true" selection-start-line="10" selection-start-column="7" selection-end-line="10" selection-end-column="69" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/app/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="30">
<caret line="14" column="33" selection-start-line="14" selection-start-column="33" selection-end-line="14" selection-end-column="33" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/controllers/caseSuite.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="270">
<caret line="16" column="114" selection-start-line="16" selection-start-column="91" selection-end-line="16" selection-end-column="114" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/controllers/project.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="300">
<caret line="16" selection-start-line="16" selection-end-line="16" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/controllers/webhook.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="150">
<caret line="11" column="23" selection-start-line="11" selection-start-column="23" selection-end-line="11" selection-end-column="23" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/run.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="120">
<caret line="4" column="50" selection-start-line="4" selection-start-column="50" selection-end-line="4" selection-end-column="50" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.backend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="900">
<caret line="30" column="49" selection-end-line="47" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/router/index.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="720">
<caret line="24" column="19" selection-start-line="24" selection-start-column="19" selection-end-line="24" selection-end-column="19" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/vuex/store.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="660">
<caret line="37" selection-start-line="37" selection-end-line="37" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="485">
<caret line="280" selection-start-line="280" selection-end-line="280" />
</first_editor>
<second_editor />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/deploy">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-239">
<caret line="3" selection-start-line="3" selection-end-line="3" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/dist/index.html">
<provider selected="true" editor-type-id="text-editor">
<state>
<caret column="41" selection-start-column="41" selection-end-column="41" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/config.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="511">
<caret line="61" column="49" selection-start-line="61" selection-start-column="49" selection-end-line="61" selection-end-column="49" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/requirements.txt">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="210">
<caret line="7" column="16" selection-start-line="7" selection-start-column="16" selection-end-line="7" selection-end-column="16" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/createAdminUser.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="123">
<caret line="16" selection-start-line="16" selection-end-line="16" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/app/__init1__.py" />
<entry file="file://$PROJECT_DIR$/backend/config.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1822">
<caret line="61" column="49" selection-start-line="61" selection-start-column="49" selection-end-line="61" selection-end-column="49" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.frontend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="300">
<caret line="10" column="69" lean-forward="true" selection-start-line="10" selection-start-column="7" selection-end-line="10" selection-end-column="69" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;markdown-preview-editor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="240">
<caret line="96" lean-forward="true" selection-start-line="96" selection-end-line="96" />
</first_editor>
<second_editor />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/utils/cron/interfaceTestCron.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="5520">
<caret line="193" column="51" selection-start-line="193" selection-start-column="51" selection-end-line="193" selection-end-column="51" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/package.json">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="360">
<caret line="12" column="4" selection-start-line="12" selection-start-column="4" selection-end-line="12" selection-end-column="4" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/interfaceTestProject/ProjectReport.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8970">
<caret line="299" column="20" selection-start-line="299" selection-start-column="20" selection-end-line="299" selection-end-column="20" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/About.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="720">
<caret line="24" column="24" selection-start-line="24" selection-start-column="24" selection-end-line="24" selection-end-column="24" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/Login.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="210">
<caret line="7" column="21" selection-start-line="7" selection-start-column="21" selection-end-line="7" selection-end-column="21" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/main.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="337">
<caret line="26" selection-start-line="26" selection-end-line="26" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/Home.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="60">
<caret line="2" column="5" selection-start-line="2" selection-start-column="5" selection-end-line="2" selection-end-column="5" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/Project.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-870">
<caret line="1" column="27" selection-start-line="1" selection-start-column="27" selection-end-line="1" selection-end-column="27" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/Dockerfile.backend">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="330">
<caret line="38" column="63" selection-start-line="38" selection-start-column="38" selection-end-line="38" selection-end-column="63" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/.gitignore">
<provider selected="true" editor-type-id="text-editor">
<state>
<caret column="10" selection-start-column="10" selection-end-column="10" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/App.vue">
<provider selected="true" editor-type-id="text-editor" />
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/interfaceTestProject/api/automation/CronList.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="5250">
<caret line="895" column="19" selection-start-line="895" selection-start-column="19" selection-end-line="895" selection-end-column="19" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/frontend/src/views/common/Header.vue">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-3090">
<caret line="29" column="29" selection-start-line="29" selection-start-column="29" selection-end-line="29" selection-end-column="29" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/utils/common.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-2700">
<caret line="107" selection-start-line="107" selection-end-line="107" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/testframe/interfaceTest/tester.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="330">
<caret line="38" column="12" selection-start-line="38" selection-start-column="12" selection-end-line="38" selection-end-column="12" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/controllers/testReport.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="240">
<caret line="13" column="15" lean-forward="true" selection-start-line="13" selection-start-column="15" selection-end-line="13" selection-end-column="15" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/backend/app/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="30">
<caret line="20" column="27" lean-forward="true" selection-end-line="45" />
</state>
</provider>
</entry>
<component name="WindowStateProjectService">
<state x="369" y="186" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1605940068834">
<screen x="0" y="23" width="1440" height="841" />
</state>
<state x="369" y="45" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.23.1440.838@0.23.1440.838" timestamp="1604041545744" />
<state x="369" y="186" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.23.1440.839@0.23.1440.839" timestamp="1604114854345" />
<state x="369" y="186" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.23.1440.841@0.23.1440.841" timestamp="1605940068834" />
<state x="698" y="114" key="EnvironmentVariablesDialog" timestamp="1605940066703">
<screen x="0" y="23" width="1440" height="841" />
</state>
<state x="759" y="120" key="EnvironmentVariablesDialog/0.23.1440.838@0.23.1440.838" timestamp="1604041544822" />
<state x="698" y="114" key="EnvironmentVariablesDialog/0.23.1440.839@0.23.1440.839" timestamp="1604114850744" />
<state x="698" y="114" key="EnvironmentVariablesDialog/0.23.1440.841@0.23.1440.841" timestamp="1605940066703" />
<state width="1301" height="188" key="GridCell.Tab.0.bottom" timestamp="1610765541309">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.816@0.23.1440.816" timestamp="1603097927708" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.827@0.23.1440.827" timestamp="1603163077827" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.828@0.23.1440.828" timestamp="1603246990685" />
<state width="1301" height="189" key="GridCell.Tab.0.bottom/0.23.1440.831@0.23.1440.831" timestamp="1610764737868" />
<state width="1301" height="188" key="GridCell.Tab.0.bottom/0.23.1440.832@0.23.1440.832" timestamp="1610765541309" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.837@0.23.1440.837" timestamp="1602559098236" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.838@0.23.1440.838" timestamp="1603960165263" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.839@0.23.1440.839" timestamp="1604117454665" />
<state width="1229" height="225" key="GridCell.Tab.0.bottom/0.23.1440.840@0.23.1440.840" timestamp="1604117737903" />
<state width="1301" height="158" key="GridCell.Tab.0.bottom/0.23.1440.841@0.23.1440.841" timestamp="1605940115567" />
<state width="1301" height="158" key="GridCell.Tab.0.bottom/0.23.1440.842@0.23.1440.842" timestamp="1606298681687" />
<state width="1301" height="188" key="GridCell.Tab.0.center" timestamp="1610765541308">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.816@0.23.1440.816" timestamp="1603097927707" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.827@0.23.1440.827" timestamp="1603163077826" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.828@0.23.1440.828" timestamp="1603246990684" />
<state width="1301" height="189" key="GridCell.Tab.0.center/0.23.1440.831@0.23.1440.831" timestamp="1610764737866" />
<state width="1301" height="188" key="GridCell.Tab.0.center/0.23.1440.832@0.23.1440.832" timestamp="1610765541308" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.837@0.23.1440.837" timestamp="1602559098236" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.838@0.23.1440.838" timestamp="1603960165261" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.839@0.23.1440.839" timestamp="1604117454664" />
<state width="1229" height="225" key="GridCell.Tab.0.center/0.23.1440.840@0.23.1440.840" timestamp="1604117737901" />
<state width="1301" height="158" key="GridCell.Tab.0.center/0.23.1440.841@0.23.1440.841" timestamp="1605940115565" />
<state width="1301" height="158" key="GridCell.Tab.0.center/0.23.1440.842@0.23.1440.842" timestamp="1606298681686" />
<state width="1301" height="188" key="GridCell.Tab.0.left" timestamp="1610765541307">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.816@0.23.1440.816" timestamp="1603097927706" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.827@0.23.1440.827" timestamp="1603163077826" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.828@0.23.1440.828" timestamp="1603246990683" />
<state width="1301" height="189" key="GridCell.Tab.0.left/0.23.1440.831@0.23.1440.831" timestamp="1610764737865" />
<state width="1301" height="188" key="GridCell.Tab.0.left/0.23.1440.832@0.23.1440.832" timestamp="1610765541307" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.837@0.23.1440.837" timestamp="1602559098235" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.838@0.23.1440.838" timestamp="1603960165260" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.839@0.23.1440.839" timestamp="1604117454663" />
<state width="1229" height="225" key="GridCell.Tab.0.left/0.23.1440.840@0.23.1440.840" timestamp="1604117737900" />
<state width="1301" height="158" key="GridCell.Tab.0.left/0.23.1440.841@0.23.1440.841" timestamp="1605940115564" />
<state width="1301" height="158" key="GridCell.Tab.0.left/0.23.1440.842@0.23.1440.842" timestamp="1606298681685" />
<state width="1301" height="188" key="GridCell.Tab.0.right" timestamp="1610765541308">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.816@0.23.1440.816" timestamp="1603097927707" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.827@0.23.1440.827" timestamp="1603163077827" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.828@0.23.1440.828" timestamp="1603246990684" />
<state width="1301" height="189" key="GridCell.Tab.0.right/0.23.1440.831@0.23.1440.831" timestamp="1610764737867" />
<state width="1301" height="188" key="GridCell.Tab.0.right/0.23.1440.832@0.23.1440.832" timestamp="1610765541308" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.837@0.23.1440.837" timestamp="1602559098236" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.838@0.23.1440.838" timestamp="1603960165262" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.839@0.23.1440.839" timestamp="1604117454664" />
<state width="1229" height="225" key="GridCell.Tab.0.right/0.23.1440.840@0.23.1440.840" timestamp="1604117737902" />
<state width="1301" height="158" key="GridCell.Tab.0.right/0.23.1440.841@0.23.1440.841" timestamp="1605940115566" />
<state width="1301" height="158" key="GridCell.Tab.0.right/0.23.1440.842@0.23.1440.842" timestamp="1606298681686" />
<state x="277" y="183" key="IDE.errors.dialog" timestamp="1610765603864">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state x="277" y="183" key="IDE.errors.dialog/0.23.1440.832@0.23.1440.832" timestamp="1610765603864" />
<state x="475" y="182" width="598" height="552" key="find.popup" timestamp="1608003957300">
<screen x="0" y="23" width="1440" height="847" />
</state>
<state x="475" y="180" width="598" height="548" key="find.popup/0.23.1440.838@0.23.1440.838" timestamp="1604041606615" />
<state x="475" y="180" width="598" height="549" key="find.popup/0.23.1440.839@0.23.1440.839" timestamp="1604117397720" />
<state x="475" y="180" width="598" height="550" key="find.popup/0.23.1440.840@0.23.1440.840" timestamp="1604385576121" />
<state x="475" y="181" width="598" height="548" key="find.popup/0.23.1440.841@0.23.1440.841" timestamp="1607322781291" />
<state x="475" y="181" width="598" height="548" key="find.popup/0.23.1440.842@0.23.1440.842" timestamp="1606991959575" />
<state x="475" y="182" width="598" height="552" key="find.popup/0.23.1440.847@0.23.1440.847" timestamp="1608003957300" />
<state x="428" y="237" key="git4idea.merge.GitMergeDialog" timestamp="1610765884408">
<screen x="0" y="23" width="1440" height="832" />
</state>
<state x="428" y="237" key="git4idea.merge.GitMergeDialog/0.23.1440.832@0.23.1440.832" timestamp="1610765884408" />
</component>
</project>
\ No newline at end of file
......@@ -72,218 +72,13 @@ The platform follows the idea of "separate development frontend and backend". Th
![泰斯特平台结构图_V1.0](https://github.com/amazingTest/Taisite-Platform/blob/master/images/泰斯特平台结构图_V1.0.png)
## IV . Deploy
## deploy
### Deploy under windows
[click me](https://mp.weixin.qq.com/s/bLyDWHCAPCshF8vmbSHtWw)
#### 0. Clone
## how to use
git clone https://github.com/amazingTest/Taisite-Platform.git
#### 1. Install python 3 env
#### 2. deploy NLP model
[Download model](https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip)
2.2 Extract the compression package
2.3 Install python dependent-packages
pip install tensorflow==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install bert-serving-server==1.9.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
2.4 Start the model
// Execute after the current directory is switched to the model folder directory
bert-serving-start -model_dir ./chinese_L-12_H-768_A-12/ -num_worker=1
After the startup is successful, the output is as follows:
![NLP模型启动成功输出](https://github.com/amazingTest/Taisite-Platform/blob/master/images/NLP模型启动成功输出.png)
#### 3. Deploy Mongodb database
#### 4. Set system environment variables
AUTOTEST_PLATFORM_ENV=production
AUTOTEST_PLATFORM_NLP_SERVER_HOST=127.0.0.1
AUTOTEST_PLATFORM_MONGO_HOST=${MONGO_HOST}
AUTOTEST_PLATFORM_MONGO_PORT=${MONGO_PORT}
AUTOTEST_PLATFORM_MONGO_USERNAME=${USERNAME}
AUTOTEST_PLATFORM_MONGO_PASSWORD=${PASSWORD}
AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME=taisite
Where AUTOTEST_PLATFORM_ENV defaults to production (required)
AUTOTEST_PLATFORM_MONGO_HOST and AUTOTEST_PLATFORM_MONGO_PORT indicate the address and port of the database (required)
AUTOTEST_PLATFORM_MONGO_USERNAME and AUTOTEST_PLATFORM_MONGO_PASSWORD represent the account password of the database (if not required)
AUTOTEST_PLATFORM_NLP_SERVER_HOST (Natural Language Model Service) defaults to native boot (not required)
AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME is the default data table name (required)
After the setting is completed, you can test it with the following commands (CMD switches to the project root directory)
python ./backend/config.py
If the configuration is successful, you can see the input configuration data.
#### 5. Package the front-end dist file (I have done this for you, skip it if you don't need secondary development)
5.1 Install the Vue environment, download node.js and configure the environment, download the npm package manager
5.2 Cmd into the frontend directory, configure cnpm:
npm install -g cnpm --registry=https://registry.npm.taobao.org
5.3 Execute the install dependency package command:
cnpm install
5.4 Execute the package command:
cnpm run build
If successfully packaged, the dist folder will be generated in the project root directory.
#### 6. Start backend
// Switch to the project root directory to execute
pip install -r ./backend/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
// Start backend (default 5050 port)
python ./backend/run.py
// Create a platform administrator account password
python ./backend/createAdminUser.py
#### 7. Access project
You can now log in using http://127.0.0.1:5050/#/login using the created administrator account password.
![平台登录界面2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/平台登录界面2.png)
### Docker containerized deployment in Linux environment
#### 0. Clone
git clone https://github.com/amazingTest/Taisite-Platform.git
#### 1. Natural language model deployment
sudo -i
docker pull shaoyuyishiwo/bertserver
docker run --name autotest-platform-bertserver -d shaoyuyishiwo/bertserver
#### 2. Mongo database deployment (skip this step if an existing database is available)
2.1 Start database & data mount to host
sudo -i
docker pull mongo
docker run --name autotest-platform-mongo -p 27017:27017 -v /data/db:/data/db -v /data/configdb:/data/configdb ``-d mongo
2.2 Create a database account
docker exec -it autotest-platform-mongo /bin/bash
mongo
> use admin
switched to db admin
> db.createUser({user:"${USERNAME}",pwd:"${PASSWORD}",roles:["root"]})
Successfully added user: { "user" : "admin", "roles" : [ "root" ] }
2.3 Database memory expansion (recommended)
> db.adminCommand({setParameter:1, internalQueryExecMaxBlockingSortBytes:335544320})
{ "was" : 33554432, "ok" : 1 }
#### 3. Environment variable configuration
// Edit /etc/profile file
sudo -i
vi /etc/profile
If there is a warning, select (E)dit anyway (enter E)
3.1 Insert the following data at the end of the text (enter i to get into insert status)
export AUTOTEST_PLATFORM_ENV=production
export AUTOTEST_PLATFORM_NLP_SERVER_HOST=${BERT_IPADRESS}
export AUTOTEST_PLATFORM_MONGO_HOST=${MONGO_HOST}
export AUTOTEST_PLATFORM_MONGO_PORT=${MONGO_PORT}
export AUTOTEST_PLATFORM_MONGO_USERNAME=${USERNAME}
export AUTOTEST_PLATFORM_MONGO_PASSWORD=${PASSWORD}
export AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME=${DBNAME}
The variable is a dynamic value. The deployer can input it according to the actual situation.
The DBNAME value can be arbitrarily customized (database table name). The BERT_IPADRESS and
MONGO_HOST values can be queried by the following commands:
docker inspect autotest-platform-bertserver
docker inspect autotest-platform-mongo // If you used the above steps to deploy the database
The output is shown below:
![控制台输出1.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/控制台输出1.png)
3.2 After inserting, click the ESC button, type :wq and click Enter to save.
3.3 Environment variables take effect immediately after executing the following command
source /etc/profile
#### 4. Start the project
Before you start the project, you need to change the timezone info by modifying the RUN script in **Dockerfile.backend** which stay
in first-level directory of the project. The default timezone is Asia/Shanghai.
// Execute the deployment file in the project root directory
sh deploy ${PORT}
The ${PORT} variable fills in the project access port, and the administrator account password is also created when the
project starts, as shown in the following figure:
![控制台输出2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/控制台输出2.png)
#### 5. Access project
The browser can access the ${PORT} port of the deployment server address.
![平台登录界面.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/平台登录界面.png)
#### EXTRA. FQA
The following output represents the NLP model startup failure
![NLP部署失败.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/NLP部署失败.png)
Solution steps:
1. Remove the code from ./backend/app/init.py:
![不使用NLP模型方法指南1.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/不使用NLP模型方法指南1.png)
2. Modify the following code in ./backend/testframe/interfaceTest/tester.py to pass:
![不使用NLP模型方法指南2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/不使用NLP模型方法指南2.png)
When you start the project after you finish, you will not depend on the natural language model~
[click me](https://shimo.im/docs/8TqxG3Ttjvj9yT8T)
## V . Contact me
......
......@@ -4,6 +4,10 @@
![泰斯特平台LOGO.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/泰斯特平台LOGO.png)
## 更新记录
[完整更新日记](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MzIwODY0NA==&action=getalbum&album_id=1515122445446397953&scene=173&subscene=&sessionid=undefined&enterid=1608004196&from_msgid=2247485048&from_itemidx=1&count=3#wechat_redirect)
## 开源申明
**这是一个受限制的自由软件!您不能在任何未经允许的前提下对程序代码进行修改和用于商业用途;也不允许对程序代码修改后以任何形式任何目的的再发布。**
......@@ -70,247 +74,14 @@
![泰斯特平台结构图_V1.0](https://github.com/amazingTest/Taisite-Platform/blob/master/images/泰斯特平台结构图_V1.0.png)
## IV . 泰斯特平台部署
### windows 环境下部署
#### 0. 克隆项目
git clone https://github.com/amazingTest/Taisite-Platform.git
#### 1. 安装 python 3 环境
[点击进入教程](https://www.runoob.com/python3/python3-install.html)
#### 2. 部署自然语言模型
[点击下载模型](https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip)
2.2 解压压缩包
2.3 安装 python 依赖包
pip install tensorflow==1.14.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install bert-serving-server==1.9.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
2.4 启动模型
// 当前目录切换至模型文件夹目录后执行
bert-serving-start -model_dir ./chinese_L-12_H-768_A-12/ -num_worker=1
启动成功后输出如下:
![NLP模型启动成功输出](https://github.com/amazingTest/Taisite-Platform/blob/master/images/NLP模型启动成功输出.png)
#### 3. 部署 Mongodb 数据库
[点击进入教程](https://www.runoob.com/mongodb/mongodb-window-install.html)
#### 4. 设置系统环境变量
AUTOTEST_PLATFORM_ENV=production
AUTOTEST_PLATFORM_NLP_SERVER_HOST=127.0.0.1
AUTOTEST_PLATFORM_MONGO_HOST=${MONGO_HOST}
AUTOTEST_PLATFORM_MONGO_PORT=${MONGO_PORT}
AUTOTEST_PLATFORM_MONGO_USERNAME=${USERNAME}
AUTOTEST_PLATFORM_MONGO_PASSWORD=${PASSWORD}
AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME=taisite
其中 AUTOTEST_PLATFORM_ENV 默认为 production (必填)
AUTOTEST_PLATFORM_MONGO_HOST和 AUTOTEST_PLATFORM_MONGO_PORT 分别表示数据库的地址和端口(必填)
AUTOTEST_PLATFORM_MONGO_USERNAME和 AUTOTEST_PLATFORM_MONGO_PASSWORD 分别表示数据库的帐号密码(若无可不填)
AUTOTEST_PLATFORM_NLP_SERVER_HOST(自然语言模型服务)默认为本机启动 (非必填)
AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME 为默认的数据表名(必填)
设置完成后可通过下列命令进行测试(CMD切换至项目根目录下)
python ./backend/config.py
若配置成功则可看见输入的配置数据
#### 5. 打包前端 dist 文件 (这一步我已为你们做好,若不需二次开发可跳过)
5.1 安装 Vue 环境,下载 node.js 并配置环境,下载 npm 包管理器
5.2 cmd 进入 frontend 目录下,配置 cnpm :
npm install -g cnpm --registry=https://registry.npm.taobao.org
5.3 执行安装依赖包命令:
cnpm install
5.4 执行打包命令:
cnpm run build
若成功打包则会在项目根目录下生成 dist 文件夹
#### 6. 启动后端
// 切换至项目根目录下执行
pip install -r ./backend/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
// 启动后端 ( 默认5050端口 )
python ./backend/run.py
// 创建平台管理员帐号密码
python ./backend/createAdminUser.py
#### 7. 访问项目
现在就可以访问 http://127.0.0.1:5050/#/login 使用创建的管理员帐号密码进行登录
![平台登录界面2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/平台登录界面2.png)
### Linux 环境下 Docker 容器化部署
[点击进入 Docker 教程地址](https://www.runoob.com/docker/ubuntu-docker-install.html)
#### 0. 克隆项目
git clone https://github.com/amazingTest/Taisite-Platform.git
#### 1. 自然语言模型部署
sudo -i
docker pull shaoyuyishiwo/bertserver
docker run --name autotest-platform-bertserver -d shaoyuyishiwo/bertserver
#### 2. Mongo 数据库部署 (若已有现成数据库可用则可跳过此步)
2.1 启动数据库 & 数据挂载至宿主机
sudo -i
docker pull mongo
docker run --name autotest-platform-mongo -p 27017:27017 -v /data/db:/data/db -v /data/configdb:/data/configdb ``-d mongo
2.2 创建数据库帐号
docker exec -it autotest-platform-mongo /bin/bash
mongo
> use admin
switched to db admin
> db.createUser({user:"${USERNAME}",pwd:"${PASSWORD}",roles:["root"]})
Successfully added user: { "user" : "admin", "roles" : [ "root" ] }
2.3 数据库内存扩容(建议)
> db.adminCommand({setParameter:1, internalQueryExecMaxBlockingSortBytes:335544320})
{ "was" : 33554432, "ok" : 1 }
#### 3. 环境变量配置
// 编辑 /etc/profile 文件
sudo -i
vi /etc/profile
若出现警告则选择 (E)dit anyway (输入 E)
3.1 文本末端插入下列数据 (输入 i 则变为 insert 状态)
export AUTOTEST_PLATFORM_ENV=production
export AUTOTEST_PLATFORM_NLP_SERVER_HOST=${BERT_IPADRESS}
export AUTOTEST_PLATFORM_MONGO_HOST=${MONGO_HOST}
export AUTOTEST_PLATFORM_MONGO_PORT=${MONGO_PORT}
export AUTOTEST_PLATFORM_MONGO_USERNAME=${USERNAME}
export AUTOTEST_PLATFORM_MONGO_PASSWORD=${PASSWORD}
export AUTOTEST_PLATFORM_MONGO_DEFAULT_DBNAME=${DBNAME}
变量为动态值,部署者自行根据实际情况输入,DBNAME 值可任意自定义(数据库表名),其中 BERT_IPADRESS 和 MONGO_HOST 值可通过下列命令查询:
docker inspect autotest-platform-bertserver
docker inspect autotest-platform-mongo // 若使用了上面的步骤部署数据库
输出如下图所示:
![控制台输出1.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/控制台输出1.png)
3.2 插入完毕后点击 ESC 按钮、输入 :wq 后单击回车保存
3.3 执行下列命令后环境变量立即生效
source /etc/profile
#### 4. 启动项目
//在项目根目录下执行部署文件
sh deploy ${PORT}
其中 ${PORT} 变量填写项目访问端口即可,项目启动的同时也创建了管理员帐号密码,如下图所示:
![控制台输出2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/控制台输出2.png)
#### 5. 访问项目
浏览器访问部署服务器地址的 ${PORT}端口即可
![平台登录界面.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/平台登录界面.png)
#### EXTRA. 常见问题
下列输出代表 NLP模型 启动失败
![NLP部署失败.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/NLP部署失败.png)
解决步骤:
1.删除 ./backend/app/init.py 中的这段代码:
![不使用NLP模型方法指南1.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/不使用NLP模型方法指南1.png)
2.将 ./backend/testframe/interfaceTest/tester.py 中的下列代码修改成 pass:
![不使用NLP模型方法指南2.png](https://github.com/amazingTest/Taisite-Platform/blob/master/images/不使用NLP模型方法指南2.png)
完成后再启动项目时,就不会依赖于自然语言模型了~
## V . 泰斯特平台使用教程
平台主流程使用可参考[本篇博文中的正文部分](https://juejin.im/post/5cd0117be51d456e537ef3bd)
若想 **完整教程** 可关注下方微信号,回复 **优质教程**
![2D-Code](https://github.com/amazingTest/Taisite-Platform/blob/master/images/微信公众号.jpg)
**QQ 交流群号:728314402**
## VI . 泰斯特平台零距离体验(建议先看教程哦~)
因体验服务器配置问题,体验需关注下方公众号后回复 「体验地址」 获得:
![2D-Code](https://github.com/amazingTest/Taisite-Platform/blob/master/images/微信公众号.jpg)
## Ⅶ . 泰斯特带你成长
## 平台部署
如果你想:
[点我进入平台部署](https://mp.weixin.qq.com/s/bLyDWHCAPCshF8vmbSHtWw)
+ 熟悉平台的每一个最新改动
+ 了解人工智能与测试结合的最新动态
+ 打破测试水平的瓶颈
+ 泰斯特带你一起成长
## 平台教程
那么我欢迎你来加入(扫描下方海报中二维码) **我的星球** 一起问道技术巅峰
[点我进入平台教程](https://shimo.im/docs/8TqxG3Ttjvj9yT8T)
![我的星球](https://github.com/amazingTest/Taisite-Platform/blob/master/images/知识星球二维码.jpg)
## Ⅷ . 捐赠
......
......@@ -32,14 +32,14 @@ from utils.cron.cronManager import CronManager
cron_manager = CronManager()
cron_manager.start()
from utils.nlp.Nlper import Nlper
bert_ip = _config.get_nlp_server_host() if _config.get_nlp_server_host() else '127.0.0.1'
bert_client = BertClient(ip=bert_ip, timeout=10000)
nlper = Nlper(bert_client)
# from utils.nlp.Nlper import Nlper
# bert_ip = _config.get_nlp_server_host() if _config.get_nlp_server_host() else '127.0.0.1'
# bert_client = BertClient(ip=bert_ip, timeout=10000)
# nlper = Nlper(bert_client)
from models import project, host, caseSuite, testingCase, testReport, cronTab, mail, mailSender
from models import project, host, caseSuite, testingCase, testReport, cronTab, mail, mailSender, testDataStorage
from controllers import user
from controllers import project, host, caseSuite, testingCase, testReport, cronTab, mail, mailSender, webhook
from controllers import project, host, caseSuite, testingCase, testReport, cronTab, mail, mailSender, webhook, testDataStorage
......@@ -55,7 +55,8 @@ def add_cron(project_id):
trigger_type=filtered_data.get('triggerType'),
test_case_id_list=filtered_data.get('testCaseIdList'),
is_execute_forbiddened_case=filtered_data.get('isExecuteForbiddenedCase'),
run_date=filtered_data.get('runDate'))
run_date=filtered_data.get('runDate'),
cron_name=filtered_data.get('name'))
else:
cron = Cron(test_case_suite_id_list=filtered_data.get('testCaseSuiteIdList'),
test_domain=filtered_data.get('testDomain'),
......@@ -69,7 +70,9 @@ def add_cron(project_id):
trigger_type=filtered_data.get('triggerType'),
test_case_id_list=filtered_data.get('testCaseIdList'),
is_execute_forbiddened_case=filtered_data.get('isExecuteForbiddenedCase'),
seconds=filtered_data.get('interval'))
seconds=filtered_data.get('interval'),
cron_name=filtered_data.get('name'))
cron_id = cron_manager.add_cron(cron)
for key, value in filtered_data.items():
......
......@@ -34,13 +34,13 @@ def add_mail_sender(project_id):
@app.route('/api/project/<project_id>/mailSenderList/<sender_id>/updateMailSender', methods=['POST'])
@login_required
def update_mail_sender(project_id, host_id):
def update_mail_sender(project_id, sender_id):
try:
filtered_data = MailSender.filter_field(request.get_json())
for key, value in filtered_data.items():
MailSender.update({"_id": ObjectId(host_id)},
MailSender.update({"_id": ObjectId(sender_id)},
{'$set': {key: value}})
update_response = MailSender.update({"_id": ObjectId(host_id)},
update_response = MailSender.update({"_id": ObjectId(sender_id)},
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}})
if update_response["n"] == 0:
return jsonify({'status': 'failed', 'data': '未找到相应更新数据!'})
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import app
from flask import Flask, jsonify, request, abort, send_file
from models.testDataStorage import TestDataStorage
from bson import ObjectId
from utils import common
from flask_login import login_required
import datetime
import ast
@app.route('/api/project/<project_id>/testDataStorageList', methods=['GET', 'POST'])
@login_required
def test_data_storage_list(project_id):
# TODO 性能优化
total_num, storages = common.get_total_num_and_arranged_data(TestDataStorage, request.args, fuzzy_fields=['name'])
return jsonify({'status': 'ok', 'data': {'totalNum': total_num, 'rows': storages}})
@app.route('/api/project/<project_id>/testDataStorageList/<storage_id>', methods=['GET'])
@login_required
def storage_detail(project_id, storage_id):
storage = TestDataStorage.find_one({'_id': ObjectId(storage_id)})
storage = common.format_response_in_dic(storage)
return jsonify({'status': 'ok', 'data': storage}) if storage else \
jsonify({'status': 'failed', 'data': '未找到测试数据仓库详情'})
@app.route('/api/project/<project_id>/addTestDataStorage', methods=['POST'])
@login_required
def add_storage(project_id):
request_data = request.get_json()
request_data["status"] = True
request_data["projectId"] = ObjectId(project_id)
request_data["createAt"] = datetime.datetime.utcnow()
request_data["lastUpdateTime"] = datetime.datetime.utcnow()
if 'dataMap' in request_data:
request_data['dataMap'] = ast.literal_eval(request_data['dataMap'])
if not isinstance(request_data['dataMap'], dict):
return jsonify({'status': 'failed', 'data': '数据字典必须为字典格式!'})
filtered_data = TestDataStorage.filter_field(request_data, use_set_default=True)
try:
TestDataStorage.insert(filtered_data)
return jsonify({'status': 'ok', 'data': '添加成功'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '添加失败 %s' % e})
@app.route('/api/project/<project_id>/testDataStorageList/<storage_id>/updateStorage', methods=['POST'])
@login_required
def update_storage(project_id, storage_id):
json_data = request.get_json()
if 'dataMap' in json_data:
json_data['dataMap'] = ast.literal_eval(json_data['dataMap'])
if not isinstance(json_data['dataMap'], dict):
return jsonify({'status': 'failed', 'data': '数据字典必须为字典格式!'})
try:
filtered_data = TestDataStorage.filter_field(json_data)
for key, value in filtered_data.items():
TestDataStorage.update({"_id": ObjectId(storage_id)},
{'$set': {key: value}})
update_response = TestDataStorage.update({"_id": ObjectId(storage_id)},
{'$set': {'lastUpdateTime': datetime.datetime.utcnow()}})
if update_response["n"] == 0:
return jsonify({'status': 'failed', 'data': '未找到相应更新数据!'})
return jsonify({'status': 'ok', 'data': '更新成功'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '更新失败: %s' % e})
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import app
from flask import Flask, jsonify, request, abort
from flask import Flask, jsonify, request, abort, send_file
from models.testReport import TestReport
from bson import ObjectId
from utils import common
......@@ -13,13 +13,22 @@ from flask_login import login_required
def reports_list(project_id):
# TODO 性能优化
total_num, test_reports = common.get_total_num_and_arranged_data(TestReport, request.args)
for test_report in test_reports:
del test_report['testDetail']
return jsonify({'status': 'ok', 'data': {'totalNum': total_num, 'rows': test_reports}})
@app.route('/api/project/<project_id>/reportsList/<report_id>', methods=['GET'])
@login_required
def report_detail(project_id, report_id):
test_report = list(TestReport.find({'_id': ObjectId(report_id)}))
test_report = common.format_response_in_dic(test_report[0])
test_report = TestReport.find_one({'_id': ObjectId(report_id)})
test_report = common.format_response_in_dic(test_report)
return jsonify({'status': 'ok', 'data': test_report}) if test_report else \
jsonify({'status': 'failed', 'data': '未找到报告详情'})
@app.route('/api/project/<project_id>/reportsList/<report_id>/export', methods=['POST'])
@login_required
def export_report(project_id, report_id):
bytes_io = TestReport.get_test_report_excel_bytes_io(report_id)
return send_file(bytes_io, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
\ No newline at end of file
......@@ -10,6 +10,8 @@ from io import BytesIO
from models.testingCase import TestingCase
from models.caseSuite import CaseSuite
from models.testReport import TestReport
from models.project import Project
from models.testDataStorage import TestDataStorage
from bson import ObjectId
from utils import common
import pymongo
......@@ -35,6 +37,12 @@ def add_case(project_id, case_suite_id):
request_data["testCaseType"] = 'interfaceTest'
request_data["createAt"] = datetime.datetime.utcnow()
request_data["lastUpdateTime"] = datetime.datetime.utcnow()
if "checkResponseTime" in request_data:
request_data["checkResponseTime"] = float(request_data["checkResponseTime"])\
if request_data["checkResponseTime"] else None
if "checkHttpCode" in request_data:
request_data["checkHttpCode"] = str(request_data["checkHttpCode"]) \
if request_data["checkHttpCode"] else ""
filtered_data = TestingCase.filter_field(request_data, use_set_default=True)
try:
TestingCase.insert(filtered_data)
......@@ -92,7 +100,14 @@ def update_case(project_id, case_suite_id, case_id):
return jsonify({'status': 'failed', 'data': '请求参数数据格式不正确!: %s' % e})
try:
filtered_data = TestingCase.filter_field(request.get_json())
json_data = request.get_json()
if "checkResponseTime" in json_data:
json_data["checkResponseTime"] = float(json_data["checkResponseTime"]) \
if json_data["checkResponseTime"] else None
if "checkHttpCode" in json_data:
json_data["checkHttpCode"] = str(json_data["checkHttpCode"]) \
if json_data["checkHttpCode"] else ""
filtered_data = TestingCase.filter_field(json_data)
for key, value in filtered_data.items():
TestingCase.update({"_id": ObjectId(case_id)},
{'$set': {key: value}})
......@@ -132,6 +147,13 @@ def start_test():
else:
domain = request_data["domain"]
# 获取 global_vars_id
global_vars_id = request_data["globalVarsId"] if request_data.get('globalVarsId') else None
# 查找数据字典
global_vars = TestDataStorage.find_one({'_id': ObjectId(global_vars_id)}).get('dataMap', {})\
if global_vars_id else {}
if 'caseSuiteIdList' in request_data:
case_suite_id_list = request_data["caseSuiteIdList"]
......@@ -171,6 +193,7 @@ def start_test():
if len(testing_case_list) > 0:
project_id = testing_case_list[0]["projectId"]
project_name = Project.find_one({'_id': ObjectId(project_id)})['name']
else:
return jsonify({'status': 'failed', 'data': '请先「启用」接口测试用例'})
......@@ -190,11 +213,11 @@ def start_test():
if 'caseSuiteIdList' not in request_data and len(testing_case_list) == 1:
is_single_test = True
tester = tester(test_case_list=testing_case_list, domain=domain)
tester = tester(test_case_list=testing_case_list, domain=domain, global_vars=global_vars)
if not is_single_test:
try:
tester.execute_all_test_and_send_report(TestingCase, TestReport, project_id, executor_nick_name, execution_mode)
tester.execute_all_test_and_send_report(TestingCase, TestReport, project_id, executor_nick_name, execution_mode, project_name)
return jsonify({'status': 'ok', 'data': '测试已启动,稍后请留意自动化测试报告'})
except BaseException as e:
return jsonify({'status': 'failed', 'data': '测试启动失败: %s' % e})
......@@ -221,12 +244,15 @@ def start_test():
raw_data = {
"projectId": ObjectId(project_id),
"projectName": project_name,
"testCount": test_count,
"passCount": passed_count,
"failedCount": failed_count,
"passRate": passed_rate,
"comeFrom": execution_mode,
"executorNickName": executor_nick_name,
"totalTestSpendingTimeInSec": test_result_list[0]['spendingTimeInSec'],
"testDomain": domain,
"testDetail": test_result_list,
"createAt": datetime.datetime.utcnow()
}
......@@ -264,6 +290,7 @@ test_case_map = {
'headers': '请求头部',
'presendParams': '请求参数',
'checkHttpCode': '状态码校验',
'checkResponseTime': '耗时校验/s',
'checkResponseData': '正则校验',
'checkResponseSimilarity': '文本相似度校验',
'checkResponseNumber': '数值校验',
......@@ -481,7 +508,9 @@ def export_test_cases():
print(e)
return _case_info
export_testing_cases = map(export_case_format, map(add_case_suite_name, TestingCase.find(query)))
export_testing_cases = map(export_case_format, map(add_case_suite_name,
TestingCase.find(query).sort([('caseSuiteId', pymongo.ASCENDING),
('createAt', pymongo.ASCENDING)])))
bytes_io = BytesIO()
workbook = xlsxwriter.Workbook(bytes_io, {'in_memory': True})
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from app import db
from utils.mango import *
# 类名定义 collection
class TestDataStorage(Model):
class Meta:
database = db
collection = 'testDataStorage'
# 字段
name = StringField()
description = StringField()
# dataMap = ArrayField(field_name='dataMap', default=[{'key': '', 'value': '', '__value_type': ''}],
# expected_structure={'expectedTypeRange': [list],
# 'expectedValueRange': [{
# 'expectedTypeRange': [dict],
# 'expectedDict': {
# 'key': {'expectedTypeRange': [str]},
# 'value': {'expectedTypeRange': [str, dict, list, int, float, bool]},
# '__value_type': {'expectedTypeRange': [str]},
# }
# }]
# })
dataMap = DictField()
_id = ObjectIdField()
isDeleted = BooleanField(field_name='isDeleted', default=False)
projectId = ObjectIdField()
createAt = DateField()
status = BooleanField(field_name='status', default=True)
creatorNickName = StringField()
lastUpdateTime = DateField()
lastUpdatorNickName = StringField()
if __name__ == '__main__':
pass
\ No newline at end of file
......@@ -3,6 +3,47 @@
from app import db
from utils.mango import *
from utils import common
from utils.helpers import ExcelHelper
import xlsxwriter
from io import BytesIO
import ast
import datetime
from bson import ObjectId
test_report_summary_map = {
'projectName': '测试项目',
'testDomain': '测试环境',
'testCount': '用例总数',
'passCount': '通过数',
'failedCount': '失败数',
'passRate': '通过率',
'comeFrom': '报告来源',
'executorNickName': '执行人',
'createAt': '生成时间',
'totalTestSpendingTimeInSec': '总耗时/s'
}
# 使用 dic_get 定位数据
test_report_detail_map = {
"['testBaseInfo', 'name']": '用例名称',
"['testBaseInfo', 'requestMethod']": '请求方法',
"['testBaseInfo', 'url']": '请求地址',
"['testBaseInfo', 'headers']": '请求头',
"['testBaseInfo', 'cookies']": '请求Cookie',
"['testBaseInfo', 'presendParams']": '请求参数',
"['testBaseInfo', 'curl']": '复现 curl',
"['testBaseInfo', 'checkHttpCode']": '状态码校验',
"['responseHttpStatusCode']": '实际状态码',
"['testBaseInfo', 'checkResponseData']": '数据校验',
"['testBaseInfo', 'checkResponseNumber']": '数值校验',
"['testBaseInfo', 'checkResponseSimilarity']": '相似度校验',
"['responseData']": '实际数据',
"['testConclusion']": '测试结论',
"['testStartTime']": '测试开始时间',
"['testBaseInfo', 'checkResponseTime']": '耗时校验/s',
"['spendingTimeInSec']": '测试耗时/s',
}
# 类名定义 collection
......@@ -17,8 +58,11 @@ class TestReport(Model):
_id = ObjectIdField()
isDeleted = BooleanField(field_name='isDeleted', default=False)
projectId = ObjectIdField()
projectName = StringField()
testDomain = StringField()
createAt = DateField()
lastUpdateTime = DateField()
totalTestSpendingTimeInSec = FloatField()
testCount = IntField()
passCount = IntField()
failedCount = IntField()
......@@ -28,6 +72,97 @@ class TestReport(Model):
executorNickName = StringField()
cronId = StringField()
@classmethod
def get_test_report_excel_bytes_io(cls, report_id):
test_report = cls.find_one({'_id': ObjectId(report_id)})
test_report = common.format_response_in_dic(test_report)
bytes_io = BytesIO()
workbook = xlsxwriter.Workbook(bytes_io, {'in_memory': True})
summary_sheet = workbook.add_worksheet(u'测试报告概览')
summary_sheet.freeze_panes(1, 0)
detail_sheet = workbook.add_worksheet(u'测试报告详情')
detail_sheet.freeze_panes(1, 0)
# 设置测试报告表头 format
header_style = workbook.add_format()
header_style.set_bg_color("#00CCFF")
header_style.set_color("#FFFFFF")
header_style.set_bold()
header_style.set_border()
# 测试报告概览表头
for index, value in enumerate(test_report_summary_map.values()):
summary_sheet.write(0, index, value, header_style)
# 设置测试报告概览每列宽度
[ExcelHelper.ExcelSheetHelperFunctions.set_column_auto_width(summary_sheet, i)
for i in range(len(test_report_summary_map.values()))]
# 测试报告概览数据
for index, value in enumerate(test_report_summary_map.keys()):
summary_sheet.write(1, index, str(test_report.get(value, '(暂无此数据)')))
test_details = test_report['testDetail']
# 测试报告详情表头
for index, value in enumerate(test_report_detail_map.values()):
detail_sheet.write(0, index, value, header_style)
# 设置测试报告详情每列宽度
[ExcelHelper.ExcelSheetHelperFunctions.set_column_auto_width(detail_sheet, i)
for i in range(len(test_report_detail_map.values()))]
test_result_pass_style = workbook.add_format()
test_result_pass_style.set_bg_color("#00ff44")
test_result_pass_style.set_color("#FFFFFF")
test_result_pass_style.set_bold()
# test_result_pass_style.set_border()
test_result_failed_style = workbook.add_format()
test_result_failed_style.set_bg_color("#ff0026")
test_result_failed_style.set_color("#FFFFFF")
test_result_failed_style.set_bold()
# test_result_failed_style.set_border()
failed_curl_style = workbook.add_format()
failed_curl_style.set_bg_color("#ffee00")
# failed_curl_style.set_color("#FFFFFF")
failed_curl_style.set_bold()
# failed_curl_style.set_border()
# 测试报告详情数据
for index, locator in enumerate(test_report_detail_map.keys()):
locator = ast.literal_eval(locator)
for col_index, detail in enumerate(test_details):
if 'testConclusion' in str(locator):
test_result = str(common.dict_get(detail, locator))
if '测试通过' in test_result:
detail_sheet.write(col_index + 1, index,
test_result, test_result_pass_style)
else:
detail_sheet.write(col_index + 1, index,
test_result, test_result_failed_style)
elif 'curl' in str(locator):
test_result_status = str(common.dict_get(detail, ['status']))
if test_result_status == 'failed':
detail_sheet.write(col_index + 1, index,
str(common.dict_get(detail, locator)), failed_curl_style)
else:
detail_sheet.write(col_index + 1, index, str(common.dict_get(detail, locator)))
else:
pre_write_value = str(common.dict_get(detail, locator))
pre_write_value = '(空)' if pre_write_value == 'None' else pre_write_value
detail_sheet.write(col_index + 1, index, pre_write_value)
workbook.close()
bytes_io.seek(0)
return bytes_io
def __str__(self):
return "createAt:{}"\
.format(self.createAt)
......
......@@ -70,6 +70,7 @@ class TestingCase(Model):
status = BooleanField(field_name='status', default=False)
isClearCookie = BooleanField(field_name='isClearCookie', default=False)
checkHttpCode = StringField()
checkResponseTime = FloatField()
checkResponseData = ArrayField(field_name='checkResponseData', default=[{'regex': '', 'query': []}],
expected_structure={'expectedTypeRange': [list, type(None)],
'expectedValueRange': [{
......
......@@ -10,3 +10,4 @@ python-dateutil==2.7.3
xlrd==1.1.0
xlsxwriter==1.1.8
github_webhook==1.0.2
Faker==4.14.2
\ No newline at end of file
......@@ -2,4 +2,4 @@
from app import app
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5050)
\ No newline at end of file
app.run(debug=False, host='0.0.0.0', port=5050)
\ No newline at end of file
......@@ -7,8 +7,9 @@ from utils import common
import ast
from bson import ObjectId
from threading import Thread
import copy
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
......@@ -36,7 +37,8 @@ class tester:
from app import nlper
self.nlper = nlper
except ImportError as e:
raise ImportError('nlp模型导入失败!<%s>' % e)
pass
# raise ImportError('nlp模型导入失败!<%s>' % e)
self.test_case_list = test_case_list
self.domain = domain
......@@ -50,53 +52,68 @@ class tester:
self.test_result_list = test_result_list
if global_vars is None:
self.global_vars = {}
self._origin_global_vars = global_vars if global_vars else {}
self.global_vars = copy.deepcopy(self._origin_global_vars)
# 异步方便返回测试启动是否成功的提示给前端
@async_test
def execute_all_test_and_send_report(self, testing_case_model, test_report_model,
project_id, executor_nick_name, execution_mode):
project_id, executor_nick_name, execution_mode, project_name):
test_results = []
for test_case in self.test_case_list:
test_start_time = time.time()
total_test_start_time = time.time()
for index, test_case in enumerate(self.test_case_list):
print(f'{executor_nick_name} 正在运行 {project_name} 测试项目,进度: {index+1} / {len(self.test_case_list)}')
backup_test_start = time.time()
test_start_datetime = datetime.datetime.utcnow()
test_result = self.execute_single_test(test_case)
test_end_time = time.time()
backup_test_end = time.time()
backup_test_spending_time = round(backup_test_end - backup_test_start, 3)
if 'lastManualTestResult' in test_case:
test_case.pop('lastManualTestResult')
domain = test_case["domain"] if 'domain' in test_case and isinstance(test_case["domain"], str) and \
not test_case["domain"].strip() == '' else self.domain
not test_case["domain"].strip() == '' else self.domain
if 'requestProtocol' in test_case and 'route' in test_case:
url = '%s://%s%s' % (test_case['requestProtocol'].lower(), domain, test_case['route'])
test_case["url"] = url
test_case['curl'] = common.generate_curl(method=test_case["requestMethod"],
url=test_case["url"],
headers=test_case["headers"],
data=test_case['presendParams'])
test_result["spendingTimeInSec"] = test_case.pop(
'spendingTimeInSec') if 'spendingTimeInSec' in test_case else backup_test_spending_time
test_result["testBaseInfo"] = test_case
test_result["testStartTime"] = test_start_datetime
test_result["spendingTimeInSec"] = round(test_end_time - test_start_time, 3)
test_results.append(test_result)
total_test_end_time = time.time()
total_test_spending_time = round(total_test_end_time - total_test_start_time, 3)
self.test_result_list = test_results
self.update_case_info(testing_case_model)
self.send_report(test_report_model, project_id, executor_nick_name, execution_mode)
self.send_report(test_report_model, project_id, executor_nick_name, execution_mode, total_test_spending_time, project_name)
# TODO 方便单个接口调试时同步返回结果,需重构
def execute_all_test_for_cron_and_single_test(self):
test_results = []
for test_case in self.test_case_list:
test_start_time = time.time()
backup_test_start = time.time()
test_start_datetime = datetime.datetime.utcnow()
test_result = self.execute_single_test(test_case)
test_end_time = time.time()
backup_test_end = time.time()
backup_test_spending_time = round(backup_test_end - backup_test_start, 3)
if 'lastManualTestResult' in test_case:
test_case.pop('lastManualTestResult')
domain = test_case["domain"] if 'domain' in test_case and isinstance(test_case["domain"], str) and \
not test_case["domain"].strip() == '' else self.domain
not test_case["domain"].strip() == '' else self.domain
if 'requestProtocol' in test_case and 'route' in test_case:
url = '%s://%s%s' % (test_case['requestProtocol'].lower(), domain, test_case['route'])
test_case["url"] = url
test_case['curl'] = common.generate_curl(method=test_case["requestMethod"],
url=test_case["url"],
headers=test_case["headers"],
data=test_case['presendParams'])
test_result["spendingTimeInSec"] = test_case.pop('spendingTimeInSec')\
if 'spendingTimeInSec' in test_case else backup_test_spending_time
test_result["testBaseInfo"] = test_case
test_result["testStartTime"] = test_start_datetime
test_result["spendingTimeInSec"] = round(test_end_time - test_start_time, 3)
test_results.append(test_result)
return test_results
......@@ -128,88 +145,112 @@ class tester:
json_data = None
headers = dict()
check_http_code = None
check_response_time = None
check_response_data = None
check_response_number = None
check_response_similarity = None
set_global_vars = None # for example {'status': ['status']}
domain = test_case["domain"] if 'domain' in test_case and isinstance(test_case["domain"], str) and \
not test_case["domain"].strip() == '' else self.domain
if 'requestProtocol' in test_case and 'route' in test_case:
test_case['route'] = \
common.resolve_global_var(pre_resolve_var=test_case['route'], global_var_dic=self.global_vars) \
if isinstance(test_case['route'], str) else test_case['route']
url = '%s://%s%s' % (test_case['requestProtocol'].lower(), domain, test_case['route'])
if 'requestMethod' in test_case:
method = test_case['requestMethod']
if 'requestMethod' in test_case and 'presendParams' in test_case \
and test_case['requestMethod'].lower() == 'get':
url += '?'
for key, value in test_case['presendParams'].items():
if value is not None:
get_method_params_value = common.resolve_global_var(pre_resolve_var=value,
global_var_dic=self.global_vars) \
if isinstance(value, str) else value
url += '%s=%s&' % (key, get_method_params_value)
url = url[0:(len(url) - 1)]
elif 'presendParams' in test_case and isinstance(test_case['presendParams'], dict):
# dict 先转 str,方便全局变量替换
test_case['presendParams'] = str(test_case['presendParams'])
# 全局替换
test_case['presendParams'] = common.resolve_global_var(pre_resolve_var=test_case['presendParams'],
global_var_dic=self.global_vars)
# 转回 dict
test_case['presendParams'] = ast.literal_eval(test_case['presendParams'])
json_data = test_case['presendParams']
if 'headers' in test_case and not test_case['headers'] in ["", None, {}, {'': ''}]:
if isinstance(test_case['headers'], list):
for header in test_case['headers']:
if not header['name'].strip() == '':
headers[header['name']] = \
common.resolve_global_var(pre_resolve_var=header['value'], global_var_dic=self.global_vars)\
if isinstance(header['value'], str) else headers[header['name']]
else:
raise TypeError('headers must be list!')
not test_case["domain"].strip() == '' else self.domain
try:
if 'requestProtocol' in test_case and 'route' in test_case:
test_case['route'] = \
common.resolve_global_var(pre_resolve_var=test_case['route'], global_var_dic=self.global_vars) \
if isinstance(test_case['route'], str) else test_case['route']
url = '%s://%s%s' % (test_case['requestProtocol'].lower(), domain, test_case['route'])
if 'requestMethod' in test_case:
method = test_case['requestMethod']
if 'setGlobalVars' in test_case and not test_case['setGlobalVars'] in [[], {}, "", None]:
set_global_vars = test_case['setGlobalVars']
if 'presendParams' in test_case and isinstance(test_case['presendParams'], dict):
# dict 先转 str,方便全局变量替换
test_case['presendParams'] = str(test_case['presendParams'])
headers = None if headers == {} else headers
# 转换 fake 数据
test_case['presendParams'] = common.resolve_fake_var(pre_resolve_var=test_case['presendParams'])
test_case['cookies'] = []
for key, value in session.cookies.items():
cookie_dic = dict()
cookie_dic['name'] = key
cookie_dic['value'] = value
test_case['cookies'].append(cookie_dic)
# 全局替换
test_case['presendParams'] = common.resolve_global_var(pre_resolve_var=test_case['presendParams'],
global_var_dic=self.global_vars)
# 转回 dict
test_case['presendParams'] = ast.literal_eval(test_case['presendParams'])
json_data = test_case['presendParams']
if 'headers' in test_case and not test_case['headers'] in ["", None, {}, {'': ''}]:
if isinstance(test_case['headers'], list):
for header in test_case['headers']:
if not header['name'].strip() == '':
headers[header['name']] = \
common.resolve_global_var(pre_resolve_var=header['value'],
global_var_dic=self.global_vars) \
if isinstance(header['value'], str) else headers[header['name']]
else:
raise TypeError('headers must be list!')
# print(headers)
if 'setGlobalVars' in test_case and not test_case['setGlobalVars'] in [[], {}, "", None]:
set_global_vars = test_case['setGlobalVars']
headers = None if headers == {} else headers
test_case['cookies'] = []
for key, value in session.cookies.items():
cookie_dic = dict()
cookie_dic['name'] = key
cookie_dic['value'] = value
test_case['cookies'].append(cookie_dic)
except BaseException as e:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('测试前置准备失败, 错误信息: <%s> ' % e)
return returned_data
try:
use_json_data = len(list(filter(lambda x: str(x).lower() == 'content-type' and 'json'
in headers[x], headers.keys() if headers else {}))) > 0
response = session.request(url=url, method=method, json=json_data, headers=headers, verify=False) if use_json_data\
else session.request(url=url, method=method, data=json_data, headers=headers, verify=False)
test_start = time.time()
except BaseException as e:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('请求失败, 错误信息: <%s> ' % e)
return returned_data
test_case['headers'] = headers # 重新赋值生成报告时用
test_case['dataMap'] = self._origin_global_vars # 记录下通过数据仓库引入的变量
test_case['headers'] = headers # 重新赋值生成报告时用
if test_case['requestMethod'].lower() == 'get':
response = session.request(url=url, method=method, params=json_data, headers=headers, verify=False)
else:
response = session.request(url=url, method=method, json=json_data, headers=headers,
verify=False) if use_json_data \
else session.request(url=url, method=method, data=json_data, headers=headers, verify=False)
test_end = time.time()
test_spending_time = round(test_end - test_start, 3)
test_case['spendingTimeInSec'] = test_spending_time
# response.encoding = 'utf-8'
# print(response.headers) TODO 请求头断言
except BaseException as e:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('请求失败, 错误信息: <%s> ' % e)
return returned_data
response_status_code = response.status_code
returned_data["responseHttpStatusCode"] = response_status_code
returned_data["responseData"] = response.text
try:
returned_data["responseData"] = response.text.encode('latin-1').decode('unicode-escape')
except BaseException:
returned_data["responseData"] = response.text
try:
response_json = json.loads(response.text) if isinstance(response.text, str) \
and response.text.strip() else {}
except BaseException as e:
if set_global_vars and isinstance(set_global_vars, list):
......@@ -222,30 +263,78 @@ class tester:
if 'checkHttpCode' in test_case and not test_case['checkHttpCode'] in ["", None]:
check_http_code = test_case['checkHttpCode']
if check_http_code and not str(response_status_code) == str(check_http_code):
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('响应状态码错误, 期待值: <%s>, 实际值: <%s>。\t'
% (check_http_code, response_status_code))
return returned_data
is_check_res_data_valid = isinstance(test_case.get('checkResponseData'), list) and\
if 'checkResponseTime' in test_case and test_case['checkResponseTime']:
check_response_time = test_case['checkResponseTime']
if check_response_time and float(test_spending_time) > float(check_response_time):
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('响应时间过长, 期待值: <%s s>, 实际值: <%s s>。\t'
% (check_response_time, test_spending_time))
return returned_data
is_check_res_data_valid = isinstance(test_case.get('checkResponseData'), list) and \
len(list(filter(lambda x: str(x.get('regex')).strip() == '',
test_case.get('checkResponseData')))) < 1
is_check_res_similarity_valid = isinstance(test_case.get('checkResponseSimilarity'), list) and\
is_check_res_similarity_valid = isinstance(test_case.get('checkResponseSimilarity'), list) and \
len(list(filter(lambda x: isinstance(x.get('targetSimilarity'), type(None)),
test_case.get('checkResponseSimilarity')))) < 1
is_check_res_number_valid = isinstance(test_case.get('checkResponseNumber'), list) and\
is_check_res_number_valid = isinstance(test_case.get('checkResponseNumber'), list) and \
len(list(filter(lambda x: str(x.get('expressions').get('expectResult')).strip()
== '', test_case.get('checkResponseNumber')))) < 1
if is_check_res_data_valid:
if 'checkResponseData' in test_case and not test_case['checkResponseData'] in [[], {}, "", None]:
if not isinstance(test_case['checkResponseData'], list):
raise TypeError('checkResponseData must be list!')
for index, crd in enumerate(test_case['checkResponseData']):
if not isinstance(crd, dict) or 'regex' not in crd or 'query' not in crd or \
not isinstance(crd['regex'], str) or not isinstance(crd['query'], list):
raise TypeError('checkResponseData is not valid!')
# TODO 可开启/关闭 全局替换
test_case['checkResponseData'][index]['regex'] = \
common.resolve_global_var(pre_resolve_var=crd['regex'], global_var_dic=self.global_vars) if \
crd.get('regex') and isinstance(crd.get('regex'), str) else '' # 警告!python判断空字符串为False
check_response_data = test_case['checkResponseData']
if check_response_data:
try:
for crd in check_response_data:
regex = crd['regex']
if regex.strip() == '':
continue
query = crd['query']
# query 支持全局变量替换
for index, single_query in enumerate(query):
query[index] = common.resolve_global_var(pre_resolve_var=single_query,
global_var_dic=self.global_vars)
result = re.search(regex, str(response.text)) # python 将regex字符串取了r''(原生字符串)
if not result:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('判断响应值错误(查询语句为: %s), 响应值应满足正则: <%s>,\
实际值: <%s> (%s)。(正则匹配时会将数据转化成string)\t'
% (
query, regex, response.text, type(response.text)))
except BaseException as e:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('判断响应值时报错, 错误信息: <%s>。\t' % e)
# TODO 目前默认当 is_check_res_similarity_valid 和 is_check_res_number_valid 为真时,返回格式必须可转 json ,可优化
is_test_failed = is_check_res_data_valid or is_check_res_number_valid or is_check_res_similarity_valid
is_test_failed = is_check_res_number_valid or is_check_res_similarity_valid
returned_data['status'] = 'failed' if is_test_failed else 'ok'
returned_data["testConclusion"].append('服务器返回格式不是json, 错误信息: %s, 服务器返回为: %s '
% (e, response.text)) if returned_data.get('status') and \
returned_data.get('status') == 'failed' else None
returned_data.get(
'status') == 'failed' else None
if returned_data['status'] == 'ok':
returned_data["testConclusion"].append('测试通过')
......@@ -262,6 +351,9 @@ class tester:
if 'checkHttpCode' in test_case and not test_case['checkHttpCode'] in ["", None]:
check_http_code = test_case['checkHttpCode']
if 'checkResponseTime' in test_case and test_case['checkResponseTime']:
check_response_time = test_case['checkResponseTime']
if 'checkResponseData' in test_case and not test_case['checkResponseData'] in [[], {}, "", None]:
if not isinstance(test_case['checkResponseData'], list):
raise TypeError('checkResponseData must be list!')
......@@ -283,7 +375,7 @@ class tester:
for index, crs in enumerate(test_case['checkResponseSimilarity']):
if not isinstance(crs, dict) or 'baseText' not in crs or 'targetSimilarity' not in crs \
or 'compairedText' not in crs or not isinstance(crs['baseText'], str) \
or not isinstance(crs['compairedText'], str):
or not isinstance(crs['compairedText'], str):
raise TypeError('checkResponseSimilarity is not valid!')
test_case['checkResponseSimilarity'][index]['baseText'] = \
common.resolve_global_var(pre_resolve_var=crs['baseText'], global_var_dic=self.global_vars) if \
......@@ -324,6 +416,12 @@ class tester:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('响应状态码错误, 期待值: <%s>, 实际值: <%s>。\t'
% (check_http_code, response_status_code))
if check_response_time and float(test_spending_time) > float(check_response_time):
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('响应时间过长, 期待值: <%s s>, 实际值: <%s s>。\t'
% (check_response_time, test_spending_time))
if check_response_data:
try:
for crd in check_response_data:
......@@ -331,6 +429,10 @@ class tester:
if regex.strip() == '':
continue
query = crd['query']
# query 支持全局变量替换
for index, single_query in enumerate(query):
query[index] = common.resolve_global_var(pre_resolve_var=single_query,
global_var_dic=self.global_vars)
real_value = common.dict_get(response_json, query)
if real_value is None:
returned_data["status"] = 'failed'
......@@ -377,7 +479,8 @@ class tester:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('相似度校验未达标!已对比字符串: 「%s」、「%s」, 实际相似度: 「%s」 '
'预期相似度: 「%s」。\t ' % (base_text, compaired_text,
actual_similarity, target_similarity))
actual_similarity,
target_similarity))
except BaseException as e:
returned_data["status"] = 'failed'
returned_data["testConclusion"].append('判断相似度时报错, 模型服务器可能已宕机/断网。具体错误信息: <%s>。\t' % e)
......@@ -396,9 +499,9 @@ class tester:
test_result = common.format_response_in_dic(test_result)
self.test_result_list[index] = test_result
testing_case_model.update({"_id": ObjectId(test_case_id)},
{'$set': {'lastManualTestResult': test_result}})
{'$set': {'lastManualTestResult': test_result}})
def send_report(self, test_report_model, project_id, executor_nick_name, execution_mode):
def send_report(self, test_report_model, project_id, executor_nick_name, execution_mode, total_test_spending_time, project_name):
test_count = len(self.test_result_list)
passed_count = len(
list(filter(lambda x: x == 'ok', [test_result["status"] for test_result in self.test_result_list])))
......@@ -413,13 +516,16 @@ class tester:
raw_data = {
"projectId": ObjectId(project_id),
"projectName": project_name,
"testCount": test_count,
"passCount": passed_count,
"failedCount": failed_count,
"passRate": passed_rate,
"comeFrom": execution_mode,
"testDomain": self.domain,
"executorNickName": executor_nick_name,
"testDetail": self.test_result_list,
"totalTestSpendingTimeInSec": total_test_spending_time,
"createAt": datetime.datetime.utcnow()
}
filtered_data = test_report_model.filter_field(raw_data, use_set_default=True)
......
......@@ -11,6 +11,33 @@ from utils.sendReportEmail import send_report_email
from tzlocal import get_localzone
import string
from faker import Faker
def generate_curl(url, method='POST', headers=None, data=None):
curl_method = f' -X {method.upper()}'
curl_headers = ''
if isinstance(headers, dict): # {'Accept': 'application/json', 'Content-Type': 'application/json'}
for k, v in headers.items():
curl_headers += f" -H '{k}: {v}'"
elif isinstance(headers, list): # [{'name': 'Accept', 'value': 'application/json'}, {'name': 'Content-Type', 'value': 'application/json'}]
for header in headers:
curl_headers += f" -H '{header.get('name', '')}: {header.get('value', '')}'"
data = str(data).replace("'", '"') if data else None
curl_data = f" --data-binary '{data}'" if data else ''
curl = f"curl '{url}'" \
f"{curl_method}" \
f"{curl_headers}" \
f"{curl_data} "
return curl
def get_offset_between_local_and_utc():
ts = time.time()
......@@ -149,6 +176,7 @@ def format_js_dic_to_python_dic(query_dic):
def get_total_num_and_arranged_data(raw_model, query_dic, fuzzy_fields=None):
query_dic = query_dic.to_dict() if query_dic.to_dict() else {}
if fuzzy_fields is not None:
if not isinstance(fuzzy_fields, list):
......@@ -161,13 +189,7 @@ def get_total_num_and_arranged_data(raw_model, query_dic, fuzzy_fields=None):
query_dic[fuzzy_field] = re.compile(pre_compiled_str)
query_dic = format_js_dic_to_python_dic(query_dic)
raw_model_copy = copy.deepcopy(raw_model)
raw_model_data_copy = []
if not isinstance(raw_model_copy.find(), list):
try:
raw_model_data_copy = list(raw_model_copy.find({'isDeleted': {"$ne": True}}))
except BaseException as e:
raise TypeError('raw_data cannot convert to list: %s' % e)
if not isinstance(query_dic, dict):
raise TypeError('query_dic must be dict')
......@@ -183,9 +205,10 @@ def get_total_num_and_arranged_data(raw_model, query_dic, fuzzy_fields=None):
if not query_dic == {}:
query_dic['isDeleted'] = {"$ne": True}
total_num = len(list(raw_model_copy.find(query_dic)))
# total_num = len(list(raw_model_copy.find(query_dic)))
total_num = raw_model_copy.find(query_dic).count()
else:
total_num = len(raw_model_data_copy)
total_num = raw_model_copy.find(query_dic).count()
if sort_by and order and format_order(order):
sort_query = [(sort_by, format_order(order))]
else:
......@@ -203,6 +226,7 @@ def get_total_num_and_arranged_data(raw_model, query_dic, fuzzy_fields=None):
else:
arranged_data = raw_model_copy.find(query_dic).skip(skip).limit(size)
# TODO 性能优化
return total_num, list(map(format_response_in_dic, map(raw_model_copy.filter_field, arranged_data)))
......@@ -219,12 +243,15 @@ def dict_get(dic, locators, default=None):
'''
if not isinstance(dic, dict):
if isinstance(dic, str) and len(locators) == 1 and is_slice_expression(locators[0]):
slice_indexes = locators[0].split(':')
start_index = int(slice_indexes[0]) if slice_indexes[0] else None
end_index = int(slice_indexes[-1]) if slice_indexes[-1] else None
value = dic[start_index:end_index]
return value
if can_convert_to_str(dic):
dic = str(dic)
if len(locators) == 1 and is_slice_expression(locators[0]):
slice_indexes = locators[0].split(':')
start_index = int(slice_indexes[0]) if slice_indexes[0] else None
end_index = int(slice_indexes[-1]) if slice_indexes[-1] else None
value = dic[start_index:end_index]
return value
return dic
return default
if dic == {} or len(locators) < 1:
......@@ -306,6 +333,34 @@ def is_slice_expression(expression):
return False
def resolve_fake_var(pre_resolve_var, fake_var_regex='\${faker\..+\(.*\)}', locale='zh-CN'):
# usage:
# print(resolve_fake_var('这是随机出来的地址: 【${faker.address()}】 我厉害吧!'))
re_global_var = re.compile(fake_var_regex)
faker = Faker(locale)
def fake_var_repl(match_obj):
attribute_start_index = match_obj.group().index('.') + 1
attribute_end_index = match_obj.group().index('(')
_attribute = match_obj.group()[attribute_start_index: attribute_end_index]
_str_params = match_obj.group()[attribute_end_index + 1: -2]
_dict_params = str_params_2_dict(_str_params) if '=' in _str_params else {}
nonlocal faker
match_value = getattr(faker, _attribute)(**_dict_params)
# 将一些数字类型转成str,否则re.sub会报错, match_value可能是0!
match_value = str(match_value) if match_value is not None else match_value
return match_value if match_value else match_obj.group()
resolved_var = re.sub(pattern=re_global_var, string=pre_resolve_var, repl=fake_var_repl)
return resolved_var
def resolve_global_var(pre_resolve_var, global_var_dic, global_var_regex='\${.*?}',
match2key_sub_string_start_index=2, match2key_sub_string_end_index=-1):
......@@ -399,8 +454,7 @@ def frontend_date_str2datetime(input_str, timedelta=None):
def is_valid_email(email):
re_email = re.compile(r'^[a-zA-Z0-9\.]+@[a-zA-Z0-9]+\.[a-zA-Z]{3}$')
if re_email.match(email):
if re.match('^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$', email):
return True
else:
return False
......@@ -503,7 +557,7 @@ def validate_and_pre_process_import_test_case(case_suite_model, testing_case_mod
if is_transfer_ele2dict:
# TODO 判断优化: (默认值可能不是都存在)
_case_info[key] = case_attribute.default if not _case_info[key] else\
list(map(lambda x: ast.literal_eval(x.replace('\'', '\"')),
list(map(lambda x: ast.literal_eval(x.replace('"', r'\"').replace('\'', '\"')),
str(_case_info[key]).strip().split(';')))
elif attribute_type is dict:
_case_info[key] = ast.literal_eval(str(_case_info[key]).strip()) \
......@@ -572,8 +626,11 @@ def send_email(model, project_id, send_data):
mail_list = send_data.get('mail_list')
mail_title = send_data.get('mail_title')
mail_content = send_data.get('mail_content')
attachment_name = send_data.get('attachment_name', 'attachment')
attachment_content = send_data.get('attachment_content')
if send_report_email(user_name, pass_word, mail_list, mail_title, mail_content):
if send_report_email(user_name, pass_word, mail_list, mail_title, mail_content,
attachment_name, attachment_content):
return {'status': 'ok', 'data': '邮件发送成功'}
else:
return {'status': 'failed', 'data': '邮件发送失败'}
......@@ -586,6 +643,18 @@ def get_random_key(digit_num=16):
return keys
# TODO 暂时没有处理非字符串类型的值
def str_params_2_dict(str_params: str) -> dict:
str_params = str_params.replace(' ', '')
params = str_params.split(',')
dic = {}
for param in params:
equal_sign_index = param.index('=')
key = param[:equal_sign_index]
value = param[equal_sign_index + 1:]
dic[key] = value
return dic
if __name__ == '__main__':
pass
......@@ -98,6 +98,7 @@ class CronManager:
is_enterprise_wechat_notify = cron_info.get('isEnterpriseWechatNotify')
enterprise_wechat_access_token = cron_info.get('enterpriseWechatAccessToken')
enterprise_wechat_notify_strategy = cron_info.get('enterpriseWechatNotifyStrategy')
cron_name = cron_info.get('name')
try:
if trigger_type == 'interval' and int(interval) > 0:
......@@ -120,7 +121,8 @@ class CronManager:
enterprise_wechat_notify_strategy=enterprise_wechat_notify_strategy,
trigger_type=trigger_type, # 更新定时器时,此参数并没有真正起到作用, 仅修改展示字段
test_case_id_list=test_case_id_list,
run_date=run_date) # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
run_date=run_date,
cron_name=cron_name) # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
else:
cron = Cron(test_case_suite_id_list=test_case_suite_id_list,
is_execute_forbiddened_case=is_execute_forbiddened_case,
......@@ -134,7 +136,8 @@ class CronManager:
enterprise_wechat_notify_strategy=enterprise_wechat_notify_strategy,
trigger_type=trigger_type, # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
test_case_id_list=test_case_id_list,
seconds=interval) # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
seconds=interval, # 更新定时器时,此参数并没有起到作用, 仅修改展示字段
cron_name=cron_name)
# 玄学,更改job的时候必须改args,不能改func
self.scheduler.modify_job(job_id=cron_id, coalesce=True, args=[cron])
......
......@@ -4,17 +4,26 @@ from models.testingCase import TestingCase
from models.mailSender import MailSender
from testframe.interfaceTest.tester import tester
from models.testReport import TestReport
from models.project import Project
import pymongo
from bson import ObjectId
import datetime
import requests
import time
import copy
class Cron:
def __init__(self, test_case_suite_id_list, test_domain, trigger_type, is_execute_forbiddened_case=False,
stop_alert_and_wait_until_resume = {}
recorded_first_failed_time = {}
recorded_first_failed_report_id = {}
def __init__(self, cron_name, test_case_suite_id_list, test_domain, trigger_type, is_execute_forbiddened_case=False,
test_case_id_list=None, alarm_mail_list=None, is_ding_ding_notify=False, ding_ding_access_token=None,
ding_ding_notify_strategy=None, is_enterprise_wechat_notify=False, enterprise_wechat_access_token=None,
enterprise_wechat_notify_strategy=None, is_web_hook=False, **trigger_args):
enterprise_wechat_notify_strategy=None, is_web_hook=False, retry_limit=3, retry_interval=60, global_vars=None,
**trigger_args):
if test_case_id_list is None:
test_case_id_list = []
......@@ -67,6 +76,13 @@ class Cron:
self.report_created_time = None # 告警时发送测试报告生成时间
self.failed_count = 0 # 用于判断是否邮件发送告警
self.cron_name = cron_name
self.current_retry_count = 0 # 记录当前定时任务尝试次数
self.retry_limit = retry_limit # 定时任务报错后重试次数限制
self.retry_interval = retry_interval # 定时任务报错后重试时间间隔
self.global_vars = global_vars if global_vars else {}
def get_cron_test_cases_list(self):
if not self.is_execute_forbiddened_case:
for case_suite_id in self.test_case_suite_id_list:
......@@ -101,22 +117,24 @@ class Cron:
def get_id(self):
return self._id
def generate_test_report(self, project_id, cron_id, test_result_list):
def generate_test_report(self, project_id, cron_id, test_result_list, total_test_spending_time, project_name):
test_count = len(test_result_list)
passed_count = len(
list(filter(lambda x: x == 'ok', [test_result["status"] for test_result in test_result_list])))
failed_count = len(
list(filter(lambda x: x == 'failed', [test_result["status"] for test_result in test_result_list])))
# failed count 已在生成报告前进行计算
# failed_count = len(
# list(filter(lambda x: x == 'failed', [test_result["status"] for test_result in test_result_list])))
passed_rate = '%d' % round((passed_count / test_count) * 100, 2) + '%'
self.report_created_time = datetime.datetime.now()
self.failed_count = failed_count
failed_count = self.failed_count
execute_from = "WebHook" if hasattr(self, 'is_web_hook') and self.is_web_hook else "定时任务"
execute_from = "WebHook" if hasattr(self, 'is_web_hook') and self.is_web_hook else f"定时任务 - {self.cron_name}"
raw_data = {
"projectId": ObjectId(project_id),
"projectName": project_name,
"testCount": test_count,
"passCount": passed_count,
"failedCount": failed_count,
......@@ -124,6 +142,8 @@ class Cron:
"comeFrom": execute_from,
"executorNickName": "定时机器人",
"cronId": cron_id,
"totalTestSpendingTimeInSec": total_test_spending_time,
"testDomain": self.test_domain,
"testDetail": test_result_list,
"createAt": datetime.datetime.utcnow() # 存入库时什么datetime都当utc使
}
......@@ -141,20 +161,47 @@ class Cron:
res = requests.post(url=hook_url, json=data, headers=headers)
return res
def send_enterprise_wechat_notify(self, title, content, headers=None):
def send_enterprise_wechat_notify(self, title, content, headers=None, send_report_file=True):
if headers is None:
headers = {'Content-Type': 'application/json'}
hook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}".format(self.enterprise_wechat_access_token)
data = {"msgtype": "markdown", "markdown": {"content": "{} \n >{}".format(title, content)}}
res = requests.post(url=hook_url, json=data, headers=headers)
return res
text_notify_res = requests.post(url=hook_url, json=data, headers=headers)
if send_report_file:
file_notify_res = self.send_enterprise_wechat_file(
file_content=TestReport.get_test_report_excel_bytes_io(self.report_id).read())
if not file_notify_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(file_notify_res.text))
return text_notify_res
def send_enterprise_wechat_file(self, file_content, file_name='test-report.xlsx'):
post_file_url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?" \
f"key={self.enterprise_wechat_access_token}&type=file"
files = {'file': (file_name, file_content, 'application/octet-stream')}
post_file_res = requests.post(url=post_file_url, files=files,
headers={'Content-Type': 'multipart/form-data'}).json()
media_id = post_file_res.get('media_id', '')
json_data = {
"msgtype": "file",
"file": {
"media_id": media_id
}
}
# TODO 发送报告具体链接至邮箱。 如interfaceTestProject/5ccfa182b144f831b04d7ca5/projectReport
def send_report_to_staff(self, project_id, mail_list, mail_title, mail_content):
hook_url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.enterprise_wechat_access_token}'
hook_res = requests.post(url=hook_url, json=json_data, headers={'Content-Type': 'application/json'})
return hook_res # {'errcode': 0, 'errmsg': 'ok'}
def send_report_to_staff(self, project_id, mail_list, mail_title, mail_content,
attachment_name, attachment_content):
if not isinstance(mail_list, list):
raise TypeError("mail_list must be list!")
if self.failed_count < 1:
raise TypeError('测试全通过,不需要发送告警报告!')
# if self.failed_count < 1:
# raise TypeError('测试全通过,不需要发送告警报告!')
if not self.report_created_time:
raise TypeError('无测试报告生成时间,报告发送失败!')
......@@ -162,77 +209,185 @@ class Cron:
json_data['mail_list'] = mail_list
json_data['mail_title'] = mail_title
json_data['mail_content'] = mail_content
json_data['attachment_name'] = attachment_name
json_data['attachment_content'] = attachment_content
result = common.send_email(MailSender, project_id, json_data)
return result
def cron_mission(self):
# print(self.stop_alert_and_wait_until_resume )
cron_test_cases_list = self.get_cron_test_cases_list()
if len(cron_test_cases_list) > 0:
project_id = cron_test_cases_list[0]["projectId"]
project_name = Project.find_one({'_id': ObjectId(project_id)})['name']
else:
raise TypeError('定时任务执行中未找到任何可执行用例!')
_global_vars = self.global_vars if hasattr(self, 'global_vars') else {}
tester_for_cron = tester(test_case_list=cron_test_cases_list,
domain=self.test_domain)
domain=self.test_domain,
global_vars=_global_vars)
total_test_start_time = time.time()
test_result_list = tester_for_cron.execute_all_test_for_cron_and_single_test()
total_test_end_time = time.time()
total_test_spending_time = round(total_test_end_time - total_test_start_time, 3)
for index, test_result in enumerate(test_result_list):
test_result = common.format_response_in_dic(test_result)
test_result_list[index] = test_result
if len(test_result_list) > 0:
self.generate_test_report(project_id, self.get_id(), test_result_list)
is_send_mail = self.failed_count > 0 and isinstance(self.alarm_mail_list, list)\
and len(self.alarm_mail_list) > 0
if not len(test_result_list):
return
self.failed_count = len(
list(filter(lambda x: x == 'failed', [test_result["status"] for test_result in test_result_list])))
self.current_retry_count += 1 if self.failed_count > 0 else -self.current_retry_count
generate_retry_cron = self.failed_count > 0 and self.current_retry_count < self.retry_limit
self.generate_test_report(project_id, self.get_id(), test_result_list, total_test_spending_time, project_name)
if generate_retry_cron:
print(f'当前失败用例个数:{self.failed_count}')
print(f'正在重试第 {self.current_retry_count} 次')
time.sleep(self.retry_interval)
self.cron_mission()
else:
is_send_mail = self.failed_count > 0 and isinstance(self.alarm_mail_list, list) \
and len(self.alarm_mail_list) > 0
is_send_ding_ding = self.ding_ding_access_token if hasattr(self, 'ding_ding_access_token') else False
is_send_enterprise_wechat = self.enterprise_wechat_access_token if hasattr(self, 'enterprise_wechat_access_token')\
is_send_enterprise_wechat = self.enterprise_wechat_access_token if hasattr(self,
'enterprise_wechat_access_token') \
else False
if is_send_enterprise_wechat:
enterprise_wechat_title = '智能测试平台企业微信服务'
enterprise_wechat_content = '泰斯特平台 \n >⛔ 测试失败 \n > 生成报告id: {}'.format(self.report_id) \
if self.failed_count > 0 else '泰斯特平台 \n >👍️️️️ 测试通过 \n > 生成报告id: {}' \
.format(self.report_id)
if hasattr(self, 'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get('fail') \
and self.failed_count > 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title, content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if hasattr(self, 'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get('success') \
and self.failed_count <= 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title, content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if is_send_ding_ding:
dingding_title = '智能测试平台钉钉服务'
dingding_content = '### ⛔️ 泰斯特平台 \n >⛔ 测试失败 \n > 生成报告id: {}'.format(self.report_id)\
if self.failed_count > 0 else '### ✅️ 泰斯特平台 \n >👍️️️️ 测试通过 \n > 生成报告id: {}'\
.format(self.report_id)
if hasattr(self, 'ding_ding_notify_strategy') and self.ding_ding_notify_strategy.get('fail')\
and self.failed_count > 0:
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
if hasattr(self, 'ding_ding_notify_strategy') and self.ding_ding_notify_strategy.get('success')\
and self.failed_count <= 0:
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
if is_send_mail:
mesg_title = '测试平台告警'
mesg_content = "Dears: \n\n 定时测试中存在用例未通过!,请登录平台查看详情 !\n\n 报告编号为:" \
" {} \n\n 报告生成时间为: {}"\
.format(self.report_id, self.report_created_time.strftime('%Y-%m-%d %H:%M:%S'))
result_json = self.send_report_to_staff(project_id, self.alarm_mail_list, mesg_title, mesg_content)
finally_passed_and_send_resume_notify = not self.failed_count and self.stop_alert_and_wait_until_resume.get(self.cron_name)
failed_again_but_wait_for_resume = self.failed_count and self.stop_alert_and_wait_until_resume.get(self.cron_name)
if finally_passed_and_send_resume_notify:
print(f'finally_passed_and_send_resume_notify... report id: {self.report_id}')
if is_send_enterprise_wechat:
enterprise_wechat_title = '### 接口测试平台企业微信服务'
enterprise_wechat_content = f' ✅️ {project_name} 项目 \n\n > 👍️️️️ {self.cron_name} 测试通过 \n\n ' \
f'> 😄 于 {self.recorded_first_failed_time[self.cron_name]} 发生的告警已恢复~ \n\n ' \
f'> 过往报错报告id: {self.recorded_first_failed_report_id[self.cron_name]} \n\n' \
f'> 最新生成报告id: {self.report_id} \n\n > ⬇️ 此时下方应有最新报告详情 '
if hasattr(self, 'enterprise_wechat_notify_strategy'):
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title,
content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if is_send_ding_ding:
dingding_title = '### 接口测试平台钉钉服务'
dingding_content = f' ✅️ {project_name} 项目 \n\n > 👍️️️️ {self.cron_name} 测试通过 \n\n ' \
f'> 😄 于 {self.recorded_first_failed_time[self.cron_name]} 发生的告警已恢复~ \n\n ' \
f'> 过往报错报告id: {self.recorded_first_failed_report_id[self.cron_name]} \n\n' \
f'> 最新生成报告id: {self.report_id}'
if hasattr(self, 'ding_ding_notify_strategy'):
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
mesg_title = '接口测试平台告警恢复提醒 :)'
mesg_content = "Dears: \n\n 于 【{}】 【{}】 项目下 【{}】 测试任务 (报告 id: {}) 中报错测试用例已全部恢复通过~ 最新测试报告详情内容请查阅附件 ~ \n\n 最新报告 id 为:" \
" {} \n\n 最新报告生成时间为: {}" \
.format(self.recorded_first_failed_time[self.cron_name], project_name, self.cron_name,
self.recorded_first_failed_report_id[self.cron_name], self.report_id,
self.report_created_time.strftime('%Y-%m-%d %H:%M:%S'))
mesg_attachment_name = f'接口测试报告_{self.report_created_time.strftime("%Y-%m-%d %H:%M:%S")}.xlsx'
mesg_attachment_content = TestReport.get_test_report_excel_bytes_io(self.report_id).read()
result_json = self.send_report_to_staff(project_id, self.alarm_mail_list, mesg_title, mesg_content,
mesg_attachment_name, mesg_attachment_content)
if result_json.get('status') == 'failed':
raise BaseException('邮件发送异常: {}'.format(result_json.get('data')))
else:
raise TypeError('无任何测试结果!')
self.stop_alert_and_wait_until_resume[self.cron_name] = False
elif failed_again_but_wait_for_resume:
# 在等待中且有失败的情况,暂时不做任何操作,防止使用者被不断的定时任务提醒轰炸
print(f'failed_again_but_wait_for_resume, report_id: {self.report_id}')
elif not self.stop_alert_and_wait_until_resume.get(self.cron_name):
if self.failed_count > 0:
self.recorded_first_failed_report_id[self.cron_name] = copy.deepcopy(self.report_id)
date_now = str(datetime.datetime.now())
dot_index = date_now.rindex('.')
self.recorded_first_failed_time[self.cron_name] = date_now[:dot_index]
self.stop_alert_and_wait_until_resume[self.cron_name] = True if self.failed_count else False
if is_send_enterprise_wechat:
enterprise_wechat_title = '### 接口测试平台企业微信服务'
enterprise_wechat_content = f' ⛔ {project_name} 项目 \n\n > 🚑 {self.cron_name} 测试失败 \n\n' \
f' > 生成报告id: {self.report_id} \n\n > ⬇️ 此时下方应有报告详情 ' \
if self.failed_count > 0 else f' ✅️ {project_name} 项目 \n\n > 👍️️️️ {self.cron_name} 测试通过 \n\n ' \
f'> 生成报告id: {self.report_id} \n\n > ⬇️ 此时下方应有报告详情 '
if hasattr(self,
'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get(
'fail') \
and self.failed_count > 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title,
content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if hasattr(self,
'enterprise_wechat_notify_strategy') and self.enterprise_wechat_notify_strategy.get(
'success') \
and self.failed_count <= 0:
enterprise_wechat_res = self.send_enterprise_wechat_notify(title=enterprise_wechat_title,
content=enterprise_wechat_content)
if not enterprise_wechat_res.status_code == 200:
raise BaseException('企业微信发送异常: {}'.format(enterprise_wechat_res.text))
if is_send_ding_ding:
dingding_title = '### 接口测试平台钉钉服务'
dingding_content = f' ⛔ {project_name} 项目 \n\n > 🚑 {self.cron_name} 测试失败 \n\n' \
f' > 生成报告id: {self.report_id}' \
if self.failed_count > 0 else f' ✅️ {project_name} 项目 \n\n > 👍️️️️ {self.cron_name} 测试通过 \n\n ' \
f'> 生成报告id: {self.report_id}'
if hasattr(self, 'ding_ding_notify_strategy') and self.ding_ding_notify_strategy.get('fail') \
and self.failed_count > 0:
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
if hasattr(self, 'ding_ding_notify_strategy') and self.ding_ding_notify_strategy.get('success') \
and self.failed_count <= 0:
dingding_res = self.send_ding_ding_notify(title=dingding_title, content=dingding_content)
if not dingding_res.status_code == 200:
raise BaseException('钉钉发送异常: {}'.format(dingding_res.text))
if is_send_mail:
mesg_title = '接口测试平台告警 :('
mesg_content = "Dears: \n\n 【{}】 项目下 【{}】 测试任务中存在未通过的测试用例!测试报告详情内容请查阅附件 ~ \n\n 报告 id 为:" \
" {} \n\n 报告生成时间为: {}" \
.format(project_name, self.cron_name, self.report_id,
self.report_created_time.strftime('%Y-%m-%d %H:%M:%S'))
mesg_attachment_name = f'接口测试报告_{self.report_created_time.strftime("%Y-%m-%d %H:%M:%S")}.xlsx'
mesg_attachment_content = TestReport.get_test_report_excel_bytes_io(self.report_id).read()
result_json = self.send_report_to_staff(project_id, self.alarm_mail_list, mesg_title, mesg_content,
mesg_attachment_name, mesg_attachment_content)
if result_json.get('status') == 'failed':
raise BaseException('邮件发送异常: {}'.format(result_json.get('data')))
else:
pass
if __name__ == '__main__':
......
from xlsxwriter.worksheet import (
Worksheet, cell_number_tuple, cell_string_tuple)
from typing import Optional
class ExcelSheetHelperFunctions:
def __init__(self):
pass
@staticmethod
def set_column_auto_width(worksheet: Worksheet, column: int):
"""
Set the width automatically on a column in the `Worksheet`.
!!! Make sure you run this function AFTER having all cells filled in
the worksheet!
"""
max_width = ExcelSheetHelperFunctions.get_column_width(worksheet=worksheet, column=column)
if max_width is None:
return
elif max_width > 45:
max_width = 45
worksheet.set_column(first_col=column, last_col=column, width=max_width)
@staticmethod
def get_column_width(worksheet: Worksheet, column: int) -> Optional[int]:
"""Get the max column width in a `Worksheet` column."""
strings = getattr(worksheet, '_ts_all_strings', None)
if strings is None:
strings = worksheet._ts_all_strings = sorted(
worksheet.str_table.string_table,
key=worksheet.str_table.string_table.__getitem__)
lengths = set()
for row_id, columns_dict in worksheet.table.items(): # type: int, dict
data = columns_dict.get(column)
if not data:
continue
if type(data) is cell_string_tuple:
iter_length = len(strings[data.string])
if not iter_length:
continue
lengths.add(iter_length)
continue
if type(data) is cell_number_tuple:
iter_length = len(str(data.number))
if not iter_length:
continue
lengths.add(iter_length)
if not lengths:
return None
return int(max(lengths) * 2.5) # 中文给他加长一波
......@@ -7,12 +7,8 @@ from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
#发送邮件
#title:标题
#conen:内容
def send_report_email(username, password, mail_namelist, title, content, attachment=None):
def send_report_email(username, password, mail_namelist, title, content, attachment_name='attachment',
attachment_content=None):
try:
msg = MIMEMultipart()
msg['from'] = username
......@@ -21,11 +17,12 @@ def send_report_email(username, password, mail_namelist, title, content, attachm
txt = MIMEText(content, 'html', 'utf-8')
msg.attach(txt)
if attachment:
if attachment_name and attachment_content:
# 添加附件
part = MIMEApplication(open(attachment, 'rb').read())
part = MIMEApplication(attachment_content)
# part = MIMEApplication(open(attachment, 'rb').read())
part.add_header('Content-Disposition', 'attachment', filename=
attachment)
attachment_name)
msg.attach(part)
# 设置服务器、端口
......@@ -49,3 +46,5 @@ if __name__ == '__main__':
pass
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>autotest-platform</title><link href=/static/css/app.b9f22872b933349a211edcec5f288a6a.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.2ae2e69a05c33dfc65f8.js></script><script type=text/javascript src=/static/js/vendor.5dbfeeda77126c757cd8.js></script><script type=text/javascript src=/static/js/app.fe9e4e63df40224d9918.js></script></body></html>
\ No newline at end of file
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>autotest-platform</title><link href=/static/css/app.884423d85178d863bbbe128e72a976e0.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/static/js/manifest.2ae2e69a05c33dfc65f8.js></script><script type=text/javascript src=/static/js/vendor.95947f6a8c21de282d4f.js></script><script type=text/javascript src=/static/js/app.a95e515db29f31eabd46.js></script></body></html>
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
{"version":3,"sources":["webpack:///webpack/bootstrap 36d5576ff2b654e1f4bf"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,IAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"static/js/manifest.2ae2e69a05c33dfc65f8.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 36d5576ff2b654e1f4bf"],"sourceRoot":""}
\ No newline at end of file
{"version":3,"sources":["webpack:///webpack/bootstrap 11c2ede9fd2d57da8016"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,IAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"static/js/manifest.2ae2e69a05c33dfc65f8.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 11c2ede9fd2d57da8016"],"sourceRoot":""}
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
import request from '@/utils/request'
export function getTestDataStorageList (project_id, params, header) {
return request({
url: `/api/project/${project_id}/testDataStorageList`,
headers: header,
params: params,
method: 'GET'
})
}
export function getTestDataStorageDetail (project_id, storage_id, params, header) {
return request({
url: `/api/project/${project_id}/testDataStorageList/${storage_id}`,
headers: header,
params: params,
method: 'GET'
})
}
export function addTestDataStorage (project_id, params, header) {
return request({
url: `/api/project/${project_id}/addTestDataStorage`,
headers: header,
method: 'POST',
data: params
})
}
export function updateTestDataStorage (project_id, storage_id, params, header) {
return request({
url: `/api/project/${project_id}/testDataStorageList/${storage_id}/updateStorage`,
method: 'POST',
headers: header,
data: params
})
}
......@@ -17,3 +17,14 @@ export function getReportDetail (project_id, report_id, header) {
method: 'GET'
})
}
export function exportReportDetail (project_id, report_id, header) {
return request({
url: `/api/project/${project_id}/reportsList/${report_id}/export`,
headers: header,
responseType: 'blob',
method: 'POST',
data: null
})
}
......@@ -28,7 +28,7 @@ Vue.component("header-view", Header)
router.beforeEach(async (to, from, next) => {
if (to.matched.length === 0) { //匹配前往的路由不存在
next('/aboutAuthor')
next('/interfaceProjectList')
return
}
......@@ -37,7 +37,7 @@ router.beforeEach(async (to, from, next) => {
//TODO 判断太草率
nickName !== '' ?
to.path.trim() === '/' ?
next('/aboutAuthor') :
next('/interfaceProjectList') :
next() :
next('/login')
} else {
......
......@@ -13,6 +13,7 @@ import addCaseApi from '@/views/interfaceTestProject/api/automation/AddCaseApi'
import updateCaseApi from '@/views/interfaceTestProject/api/automation/UpdateCaseApi'
import globalHost from '@/views/interfaceTestProject/global/Globalhost'
import globalMail from '@/views/interfaceTestProject/global/GlobalMail'
import globalTestDataStorage from '@/views/interfaceTestProject/global/GlobalTestDataStorage'
import projectReport from '@/views/interfaceTestProject/ProjectReport'
Vue.use(Router)
......@@ -65,6 +66,12 @@ export default new Router({
name: '邮箱配置',
leaf: true
},
{
path: '/interfaceTestProject/:project_id/GlobalTestDataStorage',
component: globalTestDataStorage,
name: '数据仓库',
leaf: true
},
{
path: '/interfaceTestProject/:project_id/CronList',
component: cronList,
......
<template>
<div style="margin:35px">
<section style="margin:35px">
<!--工具条-->
<el-col :span="24" class="toolbar" style="padding-bottom: 0px;">
......@@ -24,23 +24,24 @@
</el-table-column>
<el-table-column sortable='custom' prop="_id" label="报告编号" min-width="17%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="testCount" label="测试接口总数" min-width="8%" show-overflow-tooltip>
<el-table-column sortable='custom' prop="testCount" label="用例总数" min-width="8%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="passCount" label="通过的接口总数" min-width="8%" show-overflow-tooltip>
<el-table-column sortable='custom' prop="passCount" label="通过数" min-width="8%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="failedCount" label="失败的接口总数" min-width="8%" show-overflow-tooltip>
<el-table-column sortable='custom' prop="failedCount" label="失败数" min-width="8%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="passRate" label="通过率" min-width="8%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="comeFrom" label="执行方式" min-width="10%" show-overflow-tooltip>
<el-table-column sortable='custom' prop="comeFrom" label="报告来源" min-width="10%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="executorNickName" label="执行人" min-width="10%" show-overflow-tooltip>
</el-table-column>
<el-table-column sortable='custom' prop="createAt" label="报告生成时间" min-width="15%" show-overflow-tooltip>
</el-table-column>
<el-table-column label="操作" min-width="15%">
<el-table-column label="操作" min-width="20%">
<template slot-scope="scope">
<el-button size="small" class="el-icon-document" type="primary" @click="showReportDetail(scope.$index, scope.row)"> 查看详情</el-button>
<el-button size="small" class="el-icon-download" :loading="exportLoading" type="primary" @click="exportReportDetail(scope.$index, scope.row)"> 导出</el-button>
<!--<el-button type="danger" size="small" @click="handleDel(scope.$index, scope.row)">删除</el-button>-->
</template>
</el-table-column>
......@@ -63,8 +64,8 @@
<!--报告详情-->
<el-dialog title="报告详情" width="97%" v-loading="detailLoading" :visible.sync="isReportDetailShow" :close-on-click-modal="false">
<div style="height:700px;overflow:auto;overflow-x:hidden;border: 1px solid #e6e6e6">
<el-table height="700" :data="testReportDetail" :row-class-name="reportsTableRow" :header-cell-style="reportHeaderColor" :row-style="reportRowStyle" v-loading="listLoading" style="width: 100%;">
<el-table-column prop="testBaseInfo.name" label="用例名称" min-width="30%" sortable show-overflow-tooltip>
<el-table height="700" :data="testReportDetail" :row-class-name="reportsTableRow" :header-cell-style="reportHeaderColor" v-loading="listLoading" style="width: 100%;">
<el-table-column prop="testBaseInfo.name" label="用例名称" min-width="25%" sortable show-overflow-tooltip>
</el-table-column>
<el-table-column prop="testBaseInfo.requestMethod" label="请求方法" min-width="15%" sortable show-overflow-tooltip>
</el-table-column>
......@@ -92,21 +93,35 @@
</el-table-column>
<el-table-column prop="testStartTime" label="测试开始时间" min-width="25%" sortable show-overflow-tooltip>
</el-table-column>
<el-table-column prop="spendingTimeInSec" label="测试耗时/s" min-width="18%" sortable show-overflow-tooltip>
<el-table-column prop="testBaseInfo.checkResponseTime" label="耗时校验/s" min-width="17%" sortable show-overflow-tooltip>
</el-table-column>
<el-table-column prop="spendingTimeInSec" label="测试耗时/s" min-width="17%" sortable show-overflow-tooltip>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
<a
class="js-download-doc"
:href="downloadLink"
:download="downloadName"
v-show="false"
/>
</section>
</template>
<script>
import {getReportList, getReportDetail} from "../../api/testReport";
import {getReportList, getReportDetail, exportReportDetail} from "../../api/testReport";
import moment from "moment";
export default {
data () {
return {
downloadLink: '',
downloadName: '',
listLoading: false,
detailLoading: false,
exportLoading: false,
isReportDetailShow: false,
testReports: [],
testReportDetail: [],
......@@ -159,6 +174,37 @@
self.listLoading = false;
})
},
// 导出报告详情
async exportReportDetail(index, row){
let self = this;
self.exportLoading = true;
let project_id = row.projectId
let report_id = row._id
let header = {
"Content-Type": "application/json"
};
exportReportDetail(project_id, report_id, header).then((res) => {
const blob = new Blob([res])
self.downloadLink = window.URL.createObjectURL(blob)
self.downloadName = `接口测试报告_${moment().format('YYYY-MM-DD-HH-mm-ss')}.xlsx`
self.$nextTick(() => {
self.$el.querySelector('.js-download-doc').click()
window.URL.revokeObjectURL(this.downloadLink)
self.exportLoading = false;
self.$message.success({
message: '报告导出成功',
center: true,
});
})
}).catch((error) => {
console.log(error)
self.$message.error({
message: '报告导出失败,请稍后重试哦~',
center: true,
});
self.exportLoading = false;
})
},
showReportDetail(index, row){
let self = this;
self.isReportDetailShow = true;
......@@ -175,6 +221,10 @@
item["testBaseInfo"]["cookies"] = JSON.stringify(item["testBaseInfo"]["cookies"]) || '';
item["testBaseInfo"]["presendParams"] = JSON.stringify(item["testBaseInfo"]["presendParams"]) || '';
if(item["testBaseInfo"]["presendParams"].length > 5000){
item["testBaseInfo"]["presendParams"] = item["testBaseInfo"]["presendParams"].substr(0, 5000) + '......(长度已超出限制,完整请求参数请前往数据库查看)'
}
// TODO 判断可优化
item["responseData"] ?
......@@ -183,6 +233,13 @@
item["responseData"] = item["responseData"]:
item["responseData"] = '(无任何数据)'
if(item["responseData"].length > 5000){
item["responseData"] = item["responseData"].substr(0, 5000) + '......(长度已超出限制,完整响应请前往数据库查看)'
}
if(item["testConclusion"].length > 5000){
item["testConclusion"] = item["testConclusion"].substr(0, 5000) + '......(长度已超出限制,完整响应请前往数据库查看)'
}
item["responseHttpStatusCode"] ?
item["responseHttpStatusCode"].toString().trim() === '' ?
......@@ -221,6 +278,9 @@
if (item["testBaseInfo"]["checkHttpCode"] === null || item["testBaseInfo"]["checkHttpCode"] === undefined){
item["testBaseInfo"]["checkHttpCode"] = '(无任何校验)'
}
if (item["testBaseInfo"]["checkResponseTime"] === null || item["testBaseInfo"]["checkResponseTime"] === undefined){
item["testBaseInfo"]["checkResponseTime"] = '(无任何校验)'
}
});
}
self.testReportDetail = testDetails
......@@ -294,16 +354,13 @@
})
},
// 修改table tr行的背景色
reportRowStyle({ row, rowIndex }){
reportsTableRow({ row, rowIndex }){
if (row.status.toString() === 'ok')
return 'background-color: #33CC00;color: #fff;font-weight: 500;'
return 'bg1-row reportsTableRow'
else {
return 'background-color: #FF3333;color: #fff;font-weight: 500;'
return 'bg2-row reportsTableRow'
}
},
reportsTableRow({ row, rowIndex }){
return 'reportsTableRow';
},
// 修改table header的背景色
reportHeaderColor({ row, column, rowIndex, columnIndex }) {
if (rowIndex === 0) {
......@@ -358,4 +415,14 @@
.el-table .el-table__body .reportsTableRow:hover>td {
background-color: deepskyblue;
}
.el-table .bg1-row{
background-color: #33CC00;
color: #fff;
font-weight: 500;
}
.el-table .bg2-row{
background-color: #FF3333;
color: #fff;
font-weight: 500;
}
</style>
......@@ -76,6 +76,14 @@
</el-tooltip>
</el-form-item>
<el-form-item style="float: right; margin-right: 116px">
<el-select v-model="testDataId" @visible-change='checkActiveStorage' clearable placeholder="测试数据" >
<el-option
v-for="(item,index) in TestDataStorage"
:key="index+''"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
<el-select v-model="url" @visible-change='checkActiveEnv' clearable placeholder="测试环境" >
<el-option
v-for="(item,index) in Host"
......@@ -219,6 +227,7 @@
<script>
import {getCaseList, updateCase, copyCase, importTestCases, exportTestCases, addCase, getLastSingleTestResult} from '../../../../api/testCase';
import {getHosts} from '../../../../api/host';
import {getTestDataStorageList} from '../../../../api/testDataStorage';
import {startInterfaceTest} from "../../../../api/common";
import {getCookie} from '@/utils/cookie';
import moment from "moment";
......@@ -282,6 +291,8 @@
totalNum: 0,
url: '',
Host: [],
testDataId: '',
TestDataStorage: [],
apiListLoading: false,
sels: [],//列表选中列
TestResult: false,
......@@ -535,7 +546,8 @@
caseIdList: [row._id],
domain: self.url,
executorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')),
executionMode: '单个用例手动执行'
executionMode: '单个用例手动执行',
globalVarsId : self.testDataId
};
startInterfaceTest(params, header).then((res) => {
self.testLoading = false;
......@@ -793,6 +805,28 @@
self.listLoading = false;
})
},
getTestDataStorage(){
let self = this;
let header = {};
let params = {status: true, projectId: self.$route.params.project_id};
getTestDataStorageList(self.$route.params.project_id, params, header).then((res) => {
let {status, data} = res
if (status === 'ok'){
self.TestDataStorage = data.rows
}
else{
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '暂时无法获取 TestDataStorage,请稍后刷新重试~',
center: true,
});
})
},
getHost() {
let self = this;
let header = {};
......@@ -810,7 +844,7 @@
}
}).catch((error) => {
self.$message.error({
message: '暂时无法获取HOST,请稍后刷新重试~',
message: '暂时无法获取 HOST,请稍后刷新重试~',
center: true,
});
})
......@@ -824,6 +858,15 @@
})
}
},
checkActiveStorage: function(){
let self = this;
if (self.TestDataStorage.length < 1){
self.$message.warning({
message: '未找到「启用的数据字典」哦, 请前往「数据仓库」进行设置',
center: true,
})
}
},
//显示新增界面
handleAdd: function () {
this.addFormVisible = true;
......@@ -862,6 +905,7 @@
mounted() {
this.getCaseApiList();
this.getHost();
this.getTestDataStorage();
this.warmPrompt();
},
computed: {
......
......@@ -13,6 +13,16 @@
<el-form-item style="margin-left: 5px">
<el-button type="primary" class="el-icon-caret-right" :disabled="!hasSels" @click="executeTest"> 执行测试</el-button>
</el-form-item>
<el-select v-model="testDataId" @visible-change='checkActiveStorage' clearable placeholder="测试数据" >
<el-option
v-for="(item,index) in TestDataStorage"
:key="index+''"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
<el-select v-model="testUrl" @visible-change="checkActiveEnv" clearable placeholder="测试环境" style="margin-left: 5px">
<el-option v-for="(item,index) in Host" :key="index+''" :label="item.name" :value="item.host"></el-option>
</el-select>
......@@ -155,6 +165,7 @@
import {getCaseSuiteList, addCaseSuite, updateCaseSuite, copyCaseSuite} from '../../../../api/caseSuite';
import {exportTestCases} from '../../../../api/testCase';
import {getHosts} from "../../../../api/host";
import {getTestDataStorageList} from '../../../../api/testDataStorage';
import {getCrons, addCron, pauseCron, resumeCron, delCron} from "../../../../api/cron";
import {startInterfaceTest} from "../../../../api/common";
import {getCookie} from "@/utils/cookie";
......@@ -189,6 +200,7 @@
currentPage: 1,
totalNum: 0,
testUrl: '',
testDataId: '',
listLoading: false,
copyLoading: false,
exportLoading: false,
......@@ -199,6 +211,7 @@
disDel: true,
TestStatus: false,
Host: [],
TestDataStorage: [],
hasSels: false,
editFormVisible: false,//编辑界面是否显示
editLoading: false,
......@@ -257,8 +270,10 @@
"domain": self.testUrl,
"caseSuiteIdList": ids,
"executorNickName": unescape(getCookie('nickName').replace(/\\u/g, '%u')),
"executionMode" : "用例组手动执行"
"executionMode" : "用例组手动执行",
"globalVarsId" : self.testDataId
});
startInterfaceTest(params, header).then((res) => {
self.listLoading = false;
self.update = false;
......@@ -301,6 +316,28 @@
}
});
},
getTestDataStorage(){
let self = this;
let header = {};
let params = {status: true, projectId: self.$route.params.project_id};
getTestDataStorageList(self.$route.params.project_id, params, header).then((res) => {
let {status, data} = res
if (status === 'ok'){
self.TestDataStorage = data.rows
}
else{
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '暂时无法获取 TestDataStorage,请稍后刷新重试~',
center: true,
});
})
},
getHost() {
let self = this;
let header = {};
......@@ -726,6 +763,15 @@
})
}
},
checkActiveStorage: function(){
let self = this;
if (self.TestDataStorage.length < 1){
self.$message.warning({
message: '未找到「启用的数据字典」哦, 请前往「数据仓库」进行设置',
center: true,
})
}
},
// 修改table tr行的背景色
reportRowStyle({ row, rowIndex }){
if (!(row.status === true))
......@@ -741,6 +787,7 @@
mounted() {
this.getCaseSuites();
this.getHost();
this.getTestDataStorage();
},
computed: {
......
......@@ -181,6 +181,7 @@
<el-radio-group v-model="form.check">
<el-radio-button label="noCheck"><div>不校验</div></el-radio-button>
<el-radio-button label="checkHttpStatusCode"><div>HTTP状态校验</div></el-radio-button>
<el-radio-button label="checkResponseTime"><div>接口耗时校验</div></el-radio-button>
<el-radio-button label="checkJsonRegex"><div>JSON正则校验</div></el-radio-button>
<el-radio-button label="checkNumber"><div>数值校验</div></el-radio-button>
<el-radio-button label="checkSimilarity"><div>智能相似度校验</div></el-radio-button>
......@@ -192,6 +193,14 @@
<el-option v-for="(item,index) in httpCode" :key="index+''" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
<div v-show="showResponseTimeCheck">
<el-input
v-model="form.checkResponseTime"
placeholder="接口期望耗时/s(以内)"
type="number"
style="max-width:20%">
</el-input>
</div>
<div v-show="showJsonRegexCheck">
<el-collapse-item title="JSON正则校验" name="4">
<el-table :data="form.checkRegex" highlight-current-row>
......@@ -421,6 +430,7 @@
apiResponseLoading: false,
saveCorrelation: false,
showHttpCodeCheck: false,
showResponseTimeCheck: false,
showJsonRegexCheck: false,
showNumberCheck: false,
showSimilarityCheck: false,
......@@ -446,6 +456,7 @@
check: "checkSimilarity",
RegularParam: "",
checkHttp: "",
checkResponseTime: null,
},
FormRules: {
name : [{ required: true, message: '请输入名称', trigger: 'blur' }],
......@@ -511,11 +522,17 @@
lastUpdatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户'
};
if (self.form.checkHttp){
params["checkHttpCode"] = self.form.checkHttp
}
params["checkHttpCode"] = self.form.checkHttp
params["checkResponseTime"] = parseFloat(self.form.checkResponseTime)
if (self.form.check === 'noCheck'){
params["checkHttpCode"] = null
params["checkResponseTime"] = null
params["checkResponseData"] = null
params["checkResponseNumber"] = null
params["checkResponseSimilarity"] = null
......@@ -649,7 +666,7 @@
});
}
self.form.checkHttp = data.checkHttpCode;
self.form.checkResponseTime = data.checkResponseTime;
if (data.checkResponseData === null || data.checkResponseData === undefined){
self.form.checkRegex = [{regex: "", query: []}]
}
......@@ -711,6 +728,7 @@
//注意:当观察的数据为对象或数组时,curVal和oldVal是相等的,因为这两个形参指向的是同一个数据对象
handler(curVal,oldVal){
if (curVal.check === 'noCheck') {
this.showResponseTimeCheck = false
this.showHttpCodeCheck = false
this.showJsonRegexCheck = false
this.showNumberCheck = false
......@@ -720,21 +738,31 @@
this.showJsonRegexCheck = false
this.showNumberCheck = false
this.showSimilarityCheck = false
this.showResponseTimeCheck = false
} else if (curVal.check === 'checkJsonRegex'){
this.showHttpCodeCheck = false
this.showJsonRegexCheck = true
this.showNumberCheck = false
this.showSimilarityCheck = false
this.showResponseTimeCheck = false
} else if (curVal.check === 'checkNumber'){
this.showHttpCodeCheck = false
this.showJsonRegexCheck = false
this.showNumberCheck = true
this.showSimilarityCheck = false
this.showResponseTimeCheck = false
} else if (curVal.check === 'checkSimilarity'){
this.showHttpCodeCheck = false
this.showJsonRegexCheck = false
this.showNumberCheck = false
this.showSimilarityCheck = true
this.showResponseTimeCheck = false
} else if (curVal.check === 'checkResponseTime'){
this.showHttpCodeCheck = false
this.showJsonRegexCheck = false
this.showNumberCheck = false
this.showSimilarityCheck = false
this.showResponseTimeCheck = true
}
},
deep:true
......
......@@ -117,10 +117,10 @@
<el-dialog title="发件人配置" :visible.sync="ConfigFormVisible" :close-on-click-modal="false" style="width: 60%; left: 20%">
<el-form :inline="true" :model="ConfigForm" label-width="100px" :rules="ConfigFormRules" ref="ConfigForm">
<el-form-item label="发件人邮箱" prop="username">
<el-input v-model.trim="ConfigForm.username" auto-complete="off"></el-input>
<el-input placeholder="目前仅支持 QQ 邮箱哦~" v-model.trim="ConfigForm.username" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="邮箱授权码" prop='password'>
<el-input type="password" v-model.trim="ConfigForm.password" auto-complete="off"></el-input>
<el-input placeholder="目前仅支持 QQ 邮箱哦~" type="password" v-model.trim="ConfigForm.password" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button :disabled="isMailSenderChecked" type="info" @click.native="testMailSender" :loading="testMailSenderLoading">请先验证</el-button>
......
<template>
<div style="margin:35px">
<!--工具条-->
<el-col :span="24" class="toolbar" style="padding-bottom: 0px;">
<el-form :inline="true" :model="filters" @submit.native.prevent>
<router-link :to="{ name: '接口测试'}" style='text-decoration: none;color: aliceblue;'>
<el-button class="return-list"><i class="el-icon-d-arrow-left"return-list style="margin-right: 5px"></i> 回首页</el-button>
</router-link>
<el-form-item style="margin-left: 35px">
<el-button class="el-icon-plus" type="primary" @click="handleAdd"> 新增数据字典</el-button>
</el-form-item>
<div style="float: right; margin-right: 95px">
<el-form-item>
<el-input v-model.trim="filters.name" placeholder="名称" @keyup.enter.native="getTestDataStorageList"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="el-icon-search" @click="getTestDataStorageList"> 查询</el-button>
</el-form-item>
</div>
</el-form>
</el-col>
<!--列表-->
<el-table @sort-change='sortChange' :data="project" :row-style="reportRowStyle" :row-class-name="ReportTableRow" highlight-current-row v-loading="listLoading" @selection-change="selsChange" style="width: 100%;">
<el-table-column type="selection" min-width="5%">
</el-table-column>
<el-table-column prop="name" label="名称" min-width="30%" sortable='custom' show-overflow-tooltip>
</el-table-column>
<el-table-column prop="dataMap" label="数据字典" min-width="30%" sortable='custom' show-overflow-tooltip>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="35%" sortable='custom' show-overflow-tooltip>
</el-table-column>
<el-table-column prop="createAt" label="创建时间" min-width="25%" sortable='custom' show-overflow-tooltip>
</el-table-column>
<el-table-column prop="creatorNickName" label="创建者" min-width="18%" sortable='custom'>
</el-table-column>
<el-table-column prop="lastUpdateTime" label="最后更新时间" min-width="25%" sortable='custom' show-overflow-tooltip>
</el-table-column>
<el-table-column prop="lastUpdatorNickName" label="最后更新人" min-width="18%" sortable='custom'>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="10%" sortable='custom'>
<template slot-scope="scope">
<img v-show="scope.row.status" src="../../../assets/imgs/icon-yes.svg"/>
<img v-show="!scope.row.status" src="../../../assets/imgs/icon-no.svg"/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="50%">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDel(scope.$index, scope.row)">删除</el-button>
<el-button
type="info"
size="small"
:loading="statusChangeLoading"
@click="handleChangeStatus(scope.$index, scope.row)">
{{scope.row.status===false?'启用':'禁用'}}
</el-button>
</template>
</el-table-column>
</el-table>
<!--工具条-->
<el-col :span="24" class="toolbar">
<!--<el-button type="danger" @click="batchRemove" :disabled="this.sels.length===0">批量删除</el-button>-->
<el-pagination
style="float: right"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:page-sizes="[10, 20, 40]"
:page-size="size"
layout="total, sizes, prev, pager, next, jumper"
:total="totalNum">
</el-pagination>
</el-col>
<!--编辑界面-->
<el-dialog title="编辑" :visible.sync="editFormVisible" :close-on-click-modal="false" style="width: 60%; left: 20%">
<el-form :model="editForm" :rules="editFormRules" ref="editForm" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input placeholder="请输入名称" v-model.trim="editForm.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="数据字典" prop='dataMap'>
<el-input placeholder="请输入数据字典...(如 {'user_id': '123456'})" type="textarea" :rows="5" v-model.trim="editForm.dataMap"></el-input>
</el-form-item>
<el-form-item label="描述" prop='description'>
<el-input placeholder="请输入描述..." type="textarea" :rows="5" v-model.trim="editForm.description"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click.native="editFormVisible = false">取消</el-button>
<el-button type="primary" @click.native="editSubmit" :loading="editLoading">提交</el-button>
</div>
</el-dialog>
<!--新增界面-->
<el-dialog title="新增" :visible.sync="addFormVisible" :close-on-click-modal="false" style="width: 60%; left: 20%">
<el-form :model="addForm" label-width="80px" :rules="addFormRules" ref="addForm">
<el-form-item label="名称" prop="name">
<el-input placeholder="请输入名称" v-model.trim="addForm.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="数据字典" prop='dataMap'>
<el-input placeholder="请输入数据字典...(如 {'user_id': '123456'})" type="textarea" :rows="5" v-model.trim="addForm.dataMap"></el-input>
</el-form-item>
<el-form-item label="描述" prop='description'>
<el-input placeholder="请输入描述..." type="textarea" :rows="5" v-model.trim="addForm.description"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click.native="addFormVisible = false">取消</el-button>
<el-button type="primary" @click.native="addSubmit" :loading="addLoading">提交</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {getTestDataStorageList, addTestDataStorage, updateTestDataStorage} from '../../../api/testDataStorage';
import {getCookie} from '@/utils/cookie';
export default {
data() {
let checkJson = (rule, value, callback) => {
if (value !== "" && value !== null){
value = value.replace(/'/g, "\"")
try{
value = JSON.parse(value)
callback()
}catch (e) {
callback(new Error('参数格式不正确!'))
this.$message.warning({
message: '参数格式不正确!',
center: true,
});
}
}else{
callback()
}
};
return {
filters: {
name: ''
},
project: [],
size: 10,
skip: 0,
sortBy: 'createAt',
order: 'descending',
pageNum: 1,
totalNum: 0,
listLoading: false,
statusChangeLoading: false,
sels: [],//列表选中列
editFormVisible: false,//编辑界面是否显示
editLoading: false,
editFormRules: {
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
dataMap: [
{ required: true, message: '请输入数据字典', trigger: 'blur' },
{ validator: checkJson, trigger: 'blur' }
],
description: [
{ required: false, message: '请输入描述', trigger: 'blur' },
{ max: 1024, message: '不能超过1024个字符', trigger: 'blur' }
]
},
//编辑界面数据
editForm: {
name: '',
dataMap: '',
description: ''
},
addFormVisible: false,//新增界面是否显示
addLoading: false,
addFormRules: {
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
dataMap: [
{ required: true, message: '请输入数据字典', trigger: 'blur' },
{ validator: checkJson, trigger: 'blur' }
],
description: [
{ required: false, message: '请输入描述', trigger: 'blur' },
{ max: 1024, message: '不能超过1024个字符', trigger: 'blur' }
]
},
//新增界面数据
addForm: {
name: '',
dataMap: '',
description: ''
}
}
},
methods: {
// 获取数据仓库列表
getTestDataStorageList() {
this.listLoading = true;
let self = this;
let params = {size: self.size, skip: self.skip, sortBy: self.sortBy, order: self.order,
projectId: self.$route.params.project_id};
if (self.filters.name.trim() !== ''){
params['name'] = self.filters.name.trim()
};
let header = {};
getTestDataStorageList(this.$route.params.project_id, params, header).then((res) => {
let { status, data } = res;
self.listLoading = false;
if (status === 'ok') {
self.totalNum = data.totalNum;
data.rows.forEach(el => {
el.dataMap = el.dataMap ? JSON.stringify(el.dataMap) : '(空)'
})
self.project = data.rows
}
else {
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '数据仓库列表获取失败,请稍后刷新重试哦~',
center: true,
});
self.listLoading = false;
});
},
handleSizeChange(val){
let self = this;
self.size = val;
self.listLoading = true;
let params = {size: self.size, skip: self.skip, sortBy: self.sortBy, order: self.order,
projectId: self.$route.params.project_id};
let header = {};
getTestDataStorageList(this.$route.params.project_id, header).then((res) => {
let { status, data } = res;
self.listLoading = false;
if (status === 'ok') {
self.totalNum = data.totalNum;
data.rows.forEach(el => {
el.dataMap = el.dataMap ? JSON.stringify(el.dataMap) : '(空)'
})
self.project = data.rows
}
else {
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '数据仓库列表获取失败,请稍后刷新重试哦~',
center: true,
});
self.listLoading = false;
});
},
handleCurrentChange(val){
let self = this;
self.skip = (val - 1 ) * self.size;
self.listLoading = true;
let params = {size: self.size, skip: self.skip, sortBy: self.sortBy, order: self.order,
projectId: self.$route.params.project_id};
let header = {};
getTestDataStorageList(this.$route.params.project_id, params, header).then((res) => {
let { status, data } = res;
self.listLoading = false;
if (status === 'ok') {
self.totalNum = data.totalNum;
data.rows.forEach(el => {
el.dataMap = el.dataMap ? JSON.stringify(el.dataMap) : '(空)'
})
self.project = data.rows
}
else {
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '数据仓库列表获取失败,请稍后刷新重试哦~',
center: true,
});
self.listLoading = false;
});
},
//排序
sortChange (column){
let self = this;
self.listLoading = true;
self.sortBy = column.prop;
self.order = column.order;
let params = {size: self.size, skip: self.skip, sortBy: self.sortBy, order: self.order,
projectId: self.$route.params.project_id};
let header = {};
getTestDataStorageList(this.$route.params.project_id, params, header).then((res) => {
let { status, data } = res;
self.listLoading = false;
if (status === 'ok') {
self.totalNum = data.totalNum;
data.rows.forEach(el => {
el.dataMap = el.dataMap ? JSON.stringify(el.dataMap) : '(空)'
})
self.project = data.rows
}
else {
self.$message.error({
message: data,
center: true,
})
}
}).catch((error) => {
self.$message.error({
message: '数据仓库列表获取失败,请稍后刷新重试哦~',
center: true,
});
self.listLoading = false;
});
},
//删除
handleDel: function (index, row) {
this.$confirm('确认删除该记录吗?', '提示', {
type: 'warning'
}).then(() => {
this.listLoading = true;
let self = this;
let params = {
'isDeleted': true
};
let headers = {
"Content-Type": "application/json",
};
updateTestDataStorage(this.$route.params.project_id, row._id, params, headers).then(res => {
let { status, data } = res;
if (status === 'ok') {
self.$message({
message: '删除成功',
center: true,
type: 'success'
})
} else {
self.$message.error({
message: data,
center: true,
})
}
self.getTestDataStorageList()
});
});
},
handleChangeStatus: function(index, row) {
let self = this;
self.statusChangeLoading = true;
let status = !row.status;
let params = {
'status': status
};
let headers = {
"Content-Type": "application/json",
};
updateTestDataStorage(this.$route.params.project_id, row._id, params, headers).then(res => {
let {status, data} = res;
self.statusChangeLoading = false;
if (status === 'ok') {
self.$message({
message: '状态变更成功',
center: true,
type: 'success'
});
row.status = !row.status;
}
else {
self.$message.error({
message: data,
center: true,
})
}
self.getTestDataStorageList()
}).catch(() => {
self.$message.error({
message: 'Host状态更新失败,请稍后重试哦',
center: true
})
self.statusChangeLoading = false;
self.getTestDataStorageList()
});
},
//显示编辑界面
handleEdit: function (index, row) {
this.editFormVisible = true;
this.editForm = Object.assign({}, this.editForm, row); // 新字段上线,需要使用this.editForm添加
},
//显示新增界面
handleAdd: function () {
this.addFormVisible = true;
},
//编辑
editSubmit: function () {
let self = this;
this.$refs.editForm.validate((valid) => {
if (valid) {
this.$confirm('确认提交吗?', '提示', {}).then(() => {
self.editLoading = true;
//NProgress.start();
let params = {
project_id: this.$route.params.project_id,
name: self.editForm.name,
dataMap: self.editForm.dataMap,
description: self.editForm.description,
lastUpdatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户'
};
let headers = {
"Content-Type": "application/json",
};
updateTestDataStorage(this.$route.params.project_id, self.editForm._id, params, headers).then(res => {
let {status, data} = res;
self.editLoading = false;
if (status === 'ok') {
self.$message({
message: '修改成功',
center: true,
type: 'success'
});
self.$refs['editForm'].resetFields();
self.editFormVisible = false;
self.getTestDataStorageList()
} else {
self.$message.error({
message: data,
center: true,
})
}
})
});
}
});
},
//新增
addSubmit: function () {
this.$refs.addForm.validate((valid) => {
if (valid) {
let self = this;
this.$confirm('确认提交吗?', '提示', {}).then(() => {
self.addLoading = true;
let params = {
name: self.addForm.name,
dataMap: self.addForm.dataMap,
description: self.addForm.description,
creatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户',
lastUpdatorNickName: unescape(getCookie('nickName').replace(/\\u/g, '%u')) || '未知用户'
};
let header = {
"Content-Type": "application/json",
};
addTestDataStorage(this.$route.params.project_id, params, header).then((res) => {
let {status, data} = res;
self.addLoading = false;
if (status === 'ok') {
self.$message({
message: '添加成功',
center: true,
type: 'success'
});
self.$refs['addForm'].resetFields();
self.addFormVisible = false;
self.getTestDataStorageList()
} else {
self.$message.error({
message: data,
center: true,
});
self.$refs['addForm'].resetFields();
self.addFormVisible = false;
self.getTestDataStorageList()
}
})
});
}
});
},
selsChange: function (sels) {
this.sels = sels;
},
//批量删除
batchRemove: function () {
let ids = this.sels.map(item => item.id);
let self = this;
this.$confirm('确认删除选中记录吗?', '提示', {
type: 'warning'
}).then(() => {
self.listLoading = true;
//NProgress.start();
let params = {
project_id: Number(this.$route.params.project_id),
ids: ids
};
let headers = {
"Content-Type": "application/json",
Authorization: 'Token ' + JSON.parse(sessionStorage.getItem('token'))
};
delHost(headers, params).then(res => {
let {msg, code, data} = res;
self.listLoading = false;
if (code === '999999') {
self.$message({
message: '删除成功',
center: true,
type: 'success'
})
}
else {
self.$message.error({
message: msg,
center: true,
})
}
self.getTestDataStorageList()
})
})
},
// 修改table tr行的背景色
reportRowStyle({ row, rowIndex }){
if (!(row.status === true))
return 'background-color: #DDDDDD'
else {
return ''
}
},
ReportTableRow({ row, rowIndex }){
return 'reportTableRow';
},
},
mounted() {
this.getTestDataStorageList();
}
}
</script>
<style lang="scss" scoped>
.return-list {
margin-top: 0px;
margin-bottom: 10px;
margin-left: 35px;
border-radius: 25px;
}
</style>