hyb
2025-05-14 87453ffd761425b9f363a09a0f8fe07d770cb325
初始化接口测试平台
900 files added
1 files modified
244544 ■■■■■ changed files
测试组/Test_platform/Interface_automation/.dockerignore 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.env.example 52 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.gitattributes 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.github/workflows/deploy.yml 97 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.gitignore 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/LICENSE 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/README.md 143 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/.env.example 57 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/.gitignore 26 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/exceptions/convert.py 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/exceptions/error.py 63 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/__init__.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/admin.py 80 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/apps.py 5 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/dto/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/dto/tree_dto.py 29 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0001_initial.py 256 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0002_project_groups.py 19 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0003_generalpurposemodel.py 23 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0004_alter_generalpurposemodel_options.py 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0005_delete_generalpurposemodel.py 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0006_auto_20240111_1219.py 61 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0007_auto_20240111_1228.py 49 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0008_auto_20240111_1228.py 58 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0009_auto_20240112_1349.py 58 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0010_auto_20240112_1412.py 49 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0011_auto_20240112_1412.py 58 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0012_loginlog.py 50 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0013_loginlog_name.py 18 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0014_delete_hostip.py 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/__init__.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/models.py 596 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/serializers.py 541 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/services/tree_service_impl.py 286 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tasks.py 414 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/templatetags/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/templatetags/custom_tags.py 50 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tests.py 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tests/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/urls.py 237 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/convert2hrp.py 144 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/day.py 148 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/decorator.py 42 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/dingtalk_helper.py 87 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/email_helper.py 67 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/enums/RequestBodyEnum.py 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/enums/TreeTypeEnum.py 15 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/loader.py 624 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py 364 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/parser.py 978 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/prepare.py 627 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/query_filters.py 81 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/qy_message.py 71 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/convertor.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/generator.py 917 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/har_convertor.py 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/response.py 208 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/runner.py 70 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/task.py 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/tree.py 152 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views.py 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/api.py 444 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/ci.py 315 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/config.py 275 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/debugtalk.py 85 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/project.py 343 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/report.py 238 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/run.py 471 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/schedule.py 224 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/suite.py 666 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/variables.py 163 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/yapi.py 40 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/测试发送.py 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/__init__.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/admin.py 73 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/apps.py 5 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/common/response.py 64 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0001_initial.py 46 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0002_myuser_name.py 18 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/__init__.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/models.py 32 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py 69 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/tests.py 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py 22 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py 146 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/schema/api_schema.py 33 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/schema/request.py 93 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/schema/testcase_schema.py 82 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/__init__.py 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/asgi.py 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/celery.py 74 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/settings.py 324 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/test.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/urls.py 62 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/__init__.py 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/auth.py 67 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/middleware.py 129 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/pagination.py 33 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/permissions.py 64 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/redis_manager.py 469 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/request_util.py 123 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/swagger.py 25 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/utils/ws_connection_manager.py 99 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/backend/wsgi.py 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/conf/docker.py 94 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/conf/env.py 95 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/crud/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/crud/base_crud.py 77 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/crud/crud_helper.py 72 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/__init__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/__about__.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/__init__.py 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/api.py 262 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/__init__.py 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/auxiliary_func.py 43 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/common_util.py 55 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/comparators.py 169 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/faker_helper.py 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/login_helper.py 46 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/rand_helper.py 35 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/request_helper.py 53 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/time_helper.py 163 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/cli.py 186 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/client.py 212 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/compat.py 60 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/context.py 265 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/exceptions.py 76 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/loader.py 1032 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/parser.py 838 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/report.py 282 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/response.py 327 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/runner.py 327 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/locustfile.py 41 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/locustfile_template 46 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/report_template.html 368 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/utils.py 521 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/validator.py 129 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/gunicorn_conf.py 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/log_message patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/manage.py 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/proxy.py 19 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/record_proxy/__init__.py 44 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/record_proxy/record.py 41 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/requirements.txt 148 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static/extent.js 505 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static/images/favicon.ico patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/autocomplete.css 260 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/base.css 981 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/changelists.css 346 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/dashboard.css 27 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/fonts.css 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/forms.css 515 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/login.css 78 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/nav_sidebar.css 120 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/responsive.css 964 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/responsive_rtl.css 80 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/rtl.css 264 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/ui.css 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/LICENSE-SELECT2.md 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/select2.css 634 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/select2.min.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/css/widgets.css 565 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/LICENSE.txt 202 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/README.txt 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Bold-webfont.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Light-webfont.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Regular-webfont.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/LICENSE 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/README.txt 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/calendar-icons.svg 14 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/gis/move_vertex_off.svg 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/gis/move_vertex_on.svg 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-addlink.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-alert.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-calendar.svg 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-changelink.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-clock.svg 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-deletelink.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-no.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-unknown-alt.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-unknown.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-viewlink.svg 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-yes.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/inline-delete.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/search.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/selector-icons.svg 36 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/sorting-icons.svg 19 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/tooltag-add.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/img/tooltag-arrowright.svg 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/SelectBox.js 143 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/SelectFilter2.js 255 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/actions.js 155 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/actions.min.js 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/admin/DateTimeShortcuts.js 431 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/admin/RelatedObjectLookups.js 181 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/autocomplete.js 37 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/calendar.js 205 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/cancel.js 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/change_form.js 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/collapse.js 26 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/collapse.min.js 5 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/core.js 209 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/inlines.js 295 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/inlines.min.js 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/jquery.init.js 8 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/nav_sidebar.js 39 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/popup_response.js 16 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate.js 42 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate.min.js 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate_init.js 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/timeparse.js 106 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/urlify.js 190 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/LICENSE-JQUERY.txt 26 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/LICENSE.txt 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/jquery.js 9822 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/jquery.min.js 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/LICENSE-SELECT2.md 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/LICENSE.md 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/af.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ar.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/az.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bg.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bn.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bs.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ca.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/cs.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/da.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/de.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/dsb.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/el.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/en.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/es.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/et.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/eu.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fa.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fi.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fr.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/gl.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/he.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hi.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hr.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hsb.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hu.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hy.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/id.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/is.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/it.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ja.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ka.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/km.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ko.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/lt.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/lv.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/mk.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ms.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/nb.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ne.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/nl.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pl.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ps.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pt-BR.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pt.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ro.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ru.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sk.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sl.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sq.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sr-Cyrl.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sr.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sv.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/th.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/tk.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/tr.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/uk.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/vi.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/zh-CN.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/zh-TW.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/select2.full.js 6438 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/select2.full.min.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/LICENSE-XREGEXP.txt 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/LICENSE.txt 21 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/xregexp.js 2307 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/xregexp.min.js 18 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/automatic/dicts.js 141 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/automatic/segment.js 65 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.css 212 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.css.map 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.less 212 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.css 442 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.css.map 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.less 440 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/login.css 38 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/alert.js 481 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/aside.js 338 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/autocomplete.js 1008 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/avatar.js 426 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/backtop.js 450 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/badge.js 395 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/breadcrumb-item.js 383 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/breadcrumb.js 354 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/button-group.js 323 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/button.js 418 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/calendar.js 967 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/card.js 358 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/carousel-item.js 495 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/carousel.js 757 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/cascader-panel.js 1651 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/cascader.js 1481 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox-button.js 644 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox-group.js 374 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox.js 690 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/col.js 236 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/collapse-item.js 558 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/collapse.js 390 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/color-picker.js 2025 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/container.js 349 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/date-picker.js 6300 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dialog.js 659 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/directives/mousewheel.js 28 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/directives/repeat-click.js 30 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/divider.js 205 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/drawer.js 618 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown-item.js 377 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown-menu.js 412 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown.js 688 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/element-ui.common.js 41278 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/footer.js 338 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/form-item.js 887 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/form.js 528 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/header.js 338 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/icon.js 326 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/image.js 1122 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/index.js 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/infinite-scroll.js 354 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/input-number.js 806 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/input.js 1024 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/link.js 391 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/loading.js 690 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/format.js 60 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/index.js 65 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/af-ZA.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ar.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/bg.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ca.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/cs-CZ.js 121 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/da.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/de.js 120 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ee.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/el.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/en.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/es.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/eu.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fa.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fi.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fr.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/he.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hr.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hu.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hy-AM.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/id.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/it.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ja.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/kg.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/km.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ko.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ku.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/kz.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/lt.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/lv.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/mn.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/nb-NO.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/nl.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pl.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pt-br.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pt.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ro.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ru-RU.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sk.js 121 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sl.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sr.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sv-SE.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ta.js 118 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/th.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/tk.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/tr-TR.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ua.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ug-CN.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/uz-UZ.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/vi.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/zh-CN.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/zh-TW.js 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/main.js 325 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu-item-group.js 369 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu-item.js 553 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu.js 926 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/message-box.js 1253 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/message.js 595 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/emitter.js 38 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/focus.js 13 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/locale.js 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/migrating.js 69 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/notification.js 666 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/option-group.js 405 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/option.js 534 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/page-header.js 381 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/pagination.js 1019 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/popover.js 662 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/progress.js 633 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio-button.js 522 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio-group.js 445 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio.js 556 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/rate.js 737 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/row.js 207 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/scrollbar.js 484 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/select.js 2244 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/slider.js 1263 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/spinner.js 362 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/step.js 583 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/steps.js 403 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/submenu.js 767 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/switch.js 613 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tab-pane.js 393 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/table-column.js 1048 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/table.js 5042 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tabs.js 990 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tag.js 367 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/alert.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/aside.css 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/autocomplete.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/avatar.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/backtop.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/badge.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/base.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/breadcrumb-item.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/breadcrumb.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/button-group.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/button.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/calendar.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/card.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/carousel-item.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/carousel.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/cascader-panel.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/cascader.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox-button.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox-group.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/col.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/collapse-item.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/collapse.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/color-picker.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/container.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/date-picker.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dialog.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/display.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/divider.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/drawer.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown-item.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown-menu.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/fonts/element-icons.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/fonts/element-icons.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/footer.css 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/form-item.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/form.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/header.css 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/icon.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/image.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/index.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/infinite-scroll.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/infiniteScroll.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/input-number.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/input.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/link.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/loading.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/main.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu-item-group.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu-item.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/message-box.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/message.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/notification.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/option-group.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/option.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/page-header.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/pagination.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/popover.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/popper.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/progress.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio-button.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio-group.css 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/rate.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/reset.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/row.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/scrollbar.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/select-dropdown.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/select.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/slider.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/spinner.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/step.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/steps.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/submenu.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/switch.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tab-pane.css patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/table-column.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/table.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tabs.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tag.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/time-picker.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/time-select.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/timeline-item.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/timeline.css 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tooltip.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/transfer.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tree.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/upload.css 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/time-picker.js 2994 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/time-select.js 1798 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/timeline-item.js 432 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/timeline.js 337 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tooltip.js 483 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/transfer.js 1165 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/transitions/collapse-transition.js 95 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tree.js 2357 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/af-ZA.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ar.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/bg.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ca.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/cs-CZ.js 137 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/da.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/de.js 136 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ee.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/el.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/en.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/es.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/eu.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fa.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fi.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fr.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/he.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hr.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hu.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hy-AM.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/id.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/it.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ja.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/kg.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/km.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ko.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ku.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/kz.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/lt.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/lv.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/mn.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/nb-NO.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/nl.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pl.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pt-br.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pt.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ro.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ru-RU.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sk.js 137 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sl.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sr.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sv-SE.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ta.js 134 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/th.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/tk.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/tr-TR.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ua.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ug-CN.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/uz-UZ.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/vi.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/zh-CN.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/zh-TW.js 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/upload.js 1466 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/after-leave.js 35 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/aria-dialog.js 110 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/aria-utils.js 127 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/clickoutside.js 81 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/date-util.js 322 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/date.js 370 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/dom.js 240 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-menubar.js 26 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-menuitem.js 64 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-submenu.js 71 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/merge.js 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popper.js 1268 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popup/index.js 235 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popup/popup-manager.js 207 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/resize-event.js 59 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/scroll-into-view.js 40 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/scrollbar-width.js 40 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/shared.js 14 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/types.js 31 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/util.js 273 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/vdom.js 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/vue-popper.js 205 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/css/all.min.css 5 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.svg 3460 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.woff2 patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.svg 804 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.woff2 patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.svg 4528 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.woff2 patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/bg.svg 93 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/favicon.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/logo.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/cookie.js 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/index.js 599 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/language.js 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/login.js 25 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/vue.min.js 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/en-us.js 42 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/pt-br.js 42 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/zh-hans.js 44 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/particles/app.js 132 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/particles/particles.js 1524 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.css 226 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.less 36 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.css 239 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.less 47 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.css 228 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.less 43 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/base.less 181 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.css 221 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.less 31 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.css 221 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.less 31 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.css 227 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.css 234 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.css 210 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.less 37 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.css 217 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.less 43 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.css 227 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.css 234 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.css 227 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.css 234 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.css 227 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.css 234 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.css 217 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.less 27 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.css 221 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.less 28 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.css 221 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.less 30 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.css 226 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.less 32 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.css 222 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.less 32 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.css 212 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.css.map 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.less 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.css 236 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.less 47 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.css 235 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.less 47 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.css 220 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.less 30 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/theme.js 178 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.css 232 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.css.map 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.less 8 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.css 227 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.css.map 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.less 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.css 232 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.css.map 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.less 8 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.css 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.js 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.js.map 168 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/README 18 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/immutable.js 4977 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/immutable.min.js 34 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/insQ.js 163 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/insQ.min.js 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-init.js 73 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/LICENSE 22 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/redoc.min.js 8 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/redoc.min.js.map 23 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/LICENSE 22 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc-logo.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc.min.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc.standalone.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/style.css 73 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/LICENSE 202 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/NOTICE 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/absolute-path.js 14 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/favicon-32x32.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/index.js 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/oauth2-redirect.html 75 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.css 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.css.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.js.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-init.js 391 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/extent.js 505 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/images/favicon.ico patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/import_export/action_formats.js patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-theme.min.css 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-theme.min.css.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-tweaks.css 233 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap.min.css 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap.min.css.map 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/default.css 82 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/font-awesome-4.0.3.css 1338 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/prettify.css 30 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/base.css 359 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/highlight.css 125 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/jquery.json-view.min.css 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/img/favicon.ico patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/img/grid.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/api.js 315 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/highlight.pack.js 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/jquery.json-view.min.js 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.svg 414 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.svg 288 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.woff2 patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/glyphicons-halflings-white.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/glyphicons-halflings.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/grid.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/ajax-form.js 127 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/bootstrap.min.js 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/coreapi-0.1.1.js 2043 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/csrf.js 52 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/default.js 47 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/jquery-3.5.1.min.js 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/prettify-min.js 28 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/tempWorkDir/.gitkeep patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/templates/report_template.html 8479 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/README.md 61 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/celery/Dockerfile 41 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/django/Dockerfile 41 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/mysql/conf.d/my.cnf 39 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/mysql/init/init.sql 7 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/proxy/Dockerfile 41 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/redis/redis.conf 24 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/web/Dockerfile 12 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/web/nginx.conf 52 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/docker-compose.yml 155 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/.babelrc 12 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/.editorconfig 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/.gitignore 23 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/.postcssrc.js 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/README.md 23 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/babel.config.js 15 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/build.js 45 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/check-versions.js 54 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/utils.js 100 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/vue-loader.conf.js 22 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/webpack.base.conf.js 86 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/webpack.dev.conf.js 102 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/build/webpack.prod.conf.js 144 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/config/dev.env.js 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/config/index.js 68 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/config/prod.env.js 11 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/index.html 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/jsconfig.json 19 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/package.json 82 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/App.vue 29 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/api/user.js 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/images/img.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/images/logo.svg 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/images/side-logo.svg 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/home.css 208 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.eot patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.svg 225 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.ttf patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.woff patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/iconfont.css 183 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/iconfont.js 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/reports.css 62 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/swagger.css 119 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/assets/styles/tree.css 83 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/main.js 133 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/home/Home.vue 35 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue 222 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Side.vue 172 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/DebugTalk.vue 131 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Extract.vue 188 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Headers.vue 361 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Hooks.vue 147 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Parameters.vue 143 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Request.vue 665 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/RunCodeResult.vue 43 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Validate.vue 433 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Variables.vue 286 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Logging.vue 12 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue 260 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/login/LoginLog.vue 478 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/RecordApi.vue 860 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/components/ApiBody.vue 728 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/components/ApiList.vue 1121 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/AutoTest.vue 588 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/RecordCase.vue 901 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/TestCase.vue 12 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/EditTest.vue 1188 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/TestBody.vue 458 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/TestList.vue 1243 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/RecordConfig.vue 176 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/components/ConfigBody.vue 223 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/components/ConfigList.vue 368 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/BaseMonacoEditor.vue 142 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/javascript-completion.js 38 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/log-language.js 58 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/python-completion.js 195 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/sql-completion.js 82 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDashBoard.vue 376 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDetail.vue 480 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue 970 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/reports/DebugReport.vue 483 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/reports/ReportList.vue 625 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/task/AddTasks.vue 709 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/task/Tasks.vue 579 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/variables/GlobalEnv.vue 736 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/Crontab.vue 400 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabDay.vue 146 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabHour.vue 135 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabMin.vue 136 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabMouth.vue 137 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabResult.vue 298 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabSecond.vue 153 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabWeek.vue 126 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabYear.vue 173 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js 523 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/router/index.js 169 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/actions.js 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/index.js 14 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/mutations.js 37 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/store/state.js 14 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/util/bus.js 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/util/format.js 57 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/validator.js 17 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/static/.gitkeep patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/static/favicon.ico patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/yarn.lock 7896 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/screenshots/1.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/screenshots/2.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/screenshots/3.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/screenshots/4.png patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/screenshots/5.gif patch | view | raw | blame | history
测试组/脚本/造数脚本 @ dc1bf9 2 ●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.dockerignore
New file
@@ -0,0 +1,10 @@
# backend
./backend/__pycache__/
./backend/.venv/
./backend/venv/
./backend/env/
./backend/.env
# frontend
./frontend/dist/
./frontend/node_modules/
测试组/Test_platform/Interface_automation/.env.example
New file
@@ -0,0 +1,52 @@
# docker部署时将.env.example文件重命名为.env
# 数据库地址
DATABASE_HOST=lunar-link-mysql
# 数据库端口
DATABASE_PORT=3306
# 数据库用户名
DATABASE_USER=root
# 数据库密码
DATABASE_PASSWORD="root"
# 数据库名
DATABASE_NAME=lunarlink
# RabbitMQ配置
MQ_USER=guest
MQ_PASSWORD=guest
MQ_HOST=lunar-link-rabbitmq
MQ_PORT=5672
# Redis配置
REDIS_ON=True
REDIS_HOST=lunar-link-redis
REDIS_PASSWORD="123456"
REDIS_PORT=6379
REDIS_DB=0
BASE_REPORT_URL=http://localhost:8081/api/lunarlink/reports
IM_REPORT_BASE_URL=http://localhost
IM_REPORT_PORT=8081
IM_REPORT_TITLE=自动化测试报告
# 企微机器人 Webhook 地址
QY_WEB_HOOK=""
# 使用 SMTP 服务器发送邮件
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# SMTP 服务器地址
EMAIL_HOST="smtp.qq.com"
# SMTP 服务器端口
EMAIL_PORT=465
# 发件人邮箱账号
EMAIL_HOST_USER="366237192@qq.com"
DEFAULT_FROM_EMAIL=""
# 发件人邮箱密码
EMAIL_HOST_PASSWORD="htyriwcjalctcaeb"
# 是否使用 TLS
EMAIL_USE_TLS=False
# 是否使用 SSL
EMAIL_USE_SSL=True
# 录制流量代理配置
PROXY_ON=True  # 是否开启代理
PROXY_PORT=7778
测试组/Test_platform/Interface_automation/.gitattributes
New file
@@ -0,0 +1,3 @@
*.html linguist-language=python
*.js linguist-language=python
*.css linguist-language=python
测试组/Test_platform/Interface_automation/.github/workflows/deploy.yml
New file
@@ -0,0 +1,97 @@
name: Build and Push Docker Images
#on:
#  push:
#    branches:
#      - main
on:
  workflow_dispatch: # 手动触发工作流
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
    - name: Check out code
      uses: actions/checkout@v4
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    - name: Log in to Docker registry
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        registry: ${{ secrets.DOCKER_HUB_ADDR }}
    - name: Cache Docker layers
      uses: actions/cache@v4
      with:
        path: /tmp/.buildx-cache
        key: ${{ runner.os }}-buildx-${{ github.sha }}
        restore-keys: |
          ${{ runner.os }}-buildx-
    - name: Build and push web Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./deployment/web/Dockerfile
        push: true
        tags: ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/web:${{ vars.VERSION }}
        platforms: linux/amd64
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    - name: Build and push django Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./deployment/django/Dockerfile
        push: true
        tags: ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/django:${{ vars.VERSION }}
        platforms: linux/amd64
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    - name: Build and push celery Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./deployment/celery/Dockerfile
        push: true
        tags: ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/celery:${{ vars.VERSION }}
        platforms: linux/amd64
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    - name: Build and push proxy Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./deployment/proxy/Dockerfile
        push: true
        tags: ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/proxy:${{ vars.VERSION }}
        platforms: linux/amd64
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    - name: Move cache
      run: |
        rm -rf /tmp/.buildx-cache
        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
    - name: SSH and Deploy to Server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd ${{ secrets.SSH_DIR }}
          docker stop lunar-link-web lunar-link-django lunar-link-celery
          docker rm lunar-link-web lunar-link-django lunar-link-celery
          docker rmi ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/web:${{ vars.VERSION }}
          docker rmi ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/django:${{ vars.VERSION }}
          docker rmi ${{ secrets.DOCKER_HUB_ADDR }}/${{ secrets.DOCKER_NAMESPACE }}/celery:${{ vars.VERSION }}
          sudo docker compose up -d lunar-link-web lunar-link-django lunar-link-celery
测试组/Test_platform/Interface_automation/.gitignore
New file
@@ -0,0 +1,11 @@
/backend/venv
/backend/.idea
/web/.idea
.idea
.env
.history/
.vscode/
*.sh
docker-compose-prod.yml
测试组/Test_platform/Interface_automation/LICENSE
New file
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 geekbing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
测试组/Test_platform/Interface_automation/README.md
New file
@@ -0,0 +1,143 @@
# LunarLink
[![GitHub License](https://img.shields.io/github/license/tahitimoon/LunarLink?color=yellow)](https://github.com/tahitimoon/LunarLink/blob/main/LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/tahitimoon/LunarLink)](https://github.com/tahitimoon/LunarLink/releases)
[![GitHub Repo stars](https://img.shields.io/github/stars/tahitimoon/LunarLink)](https://github.com/tahitimoon/LunarLink/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tahitimoon/LunarLink)](https://github.com/tahitimoon/LunarLink/fork)
[![Python Version](https://img.shields.io/badge/Python-%3E%3D3.9.5-green)](https://python.org/)
[![Django Version](https://img.shields.io/badge/Django-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/)
[![Static Badge](https://img.shields.io/badge/Node-%3E%3D16.0.0-brightgreen)](https://nodejs.org/en)
## 平台简介
基于HttpRunner + Django + Vue + Element UI 的接口自动化测试平台,生产可用。
此外,非常感谢 [花菜](https://github.com/lihuacai168)。没有 AnotherFasterRunner 就不会有 LunarLink :)
## 技术栈
- [x]  🎨 Django
- [x]  🎶 Django Rest framework
- [x]  🎉 Vue.js
- [x]  🎃 Element UI
- [x] 🏐 django-celery-beat(定时任务)
- [x]  🎲 Nginx(反向代理,https配置等)
- [x] 👟 HttpRunner(测试用例执行引擎)
- [x]  🔒 RabbitMQ
- [x]  🚚 Redis
- [x]  💎 MySQL
- [x] ⛏ Docker
## 功能模块
- 登录:账号认证、用户管理、权限管理
- 项目管理:新增项目、列表展示及相关操作,支持项目看板,显示项目每日、每周、每月不同维度数据
- 项目概览:项目基本信息,API、测试用例、任务执行总览,每日明细
- 接口管理:一个API对应后端一个HTTP接口,API可以单独运行,也可以作为一个用例步骤,支持API分组、参数提取、数据断言、变量引用、前后置钩子函数、接口调试、日志显示
- 接口导入:支持同步YApi (以YApi 做媒介间接支持Swagger、Postman、Har),无需手动录入接口
- 测试用例:支持分组管理,由多个API组成,支持关联接口的动态参数传递,不区分场景用例和单接口用例,支持同步、异步批量执行,生成测试报告
- 流量录制:为了适配复杂流程的接口测试场景,打开浏览器,进行一系列操作,就可以得到一个测试用例,不同接口之间的参数自动提取并关联
- 配置管理:自定义不同配置的请求base_url地址,公共请求头参数、局部变量、前后置钩子函数、参数化内容,API和用例可以选择不同配置执行
- 全局变量:定义变量供API使用,变量值也可以引用驱动代码中的函数,通用账号、秘钥可以定义在此处
- 驱动代码:支持Python脚本,定义前后置钩子函数、辅助函数可以轻松实现请求参数签名,加密和解密响应等功能
- 定时任务:可设置定时任务,遵循crontab表达式,可在线开启、关闭,用例执行支持串行、并行,完成后测试报告推送企业微信
- 历史报告:保存定时、调试、异步等不同类型的测试报告,可在线查看、筛选、删除,还可查看他人测试报告
##  准备工作
```
Python >= 3.9.5 (推荐3.9.x版本)
nodejs >= 16.0 (推荐最新)
Mysql >= 5.7.0 (推荐5.7.x版本)
RabbitMQ >= 3.x-management(默认需要,推荐最新版)
Redis >= 6.2.6
```
##  前端 ♝
建议使用yarn,项目提供了`yarn.lock`,使用其他包管理器,容易出现版本依赖问题。
```bash
# 克隆项目
git clone https://github.com/tahitimoon/LunarLink.git
# 进入项目目录
cd LunarLink/frontend
# 安装依赖
yarn install --registry=https://registry.npmmirror.com
# 启动服务
yarn start
# 浏览器访问 http://127.0.0.1:8888
# config/index.js 文件可配置启动端口等参数
# config/dev.env.js 文件可配置后端接口地址
# config/prod.env.js 文件保持不变
# 构建生产环境
# yarn build
```
##  后端 💈
```bash
# 克隆项目
git https://github.com/tahitimoon/LunarLink.git
# 进入项目目录
cd LunarLink/backend
# 将.env.example文件重命名为.env 并配置相关参数
mv .env.example .env
# 安装依赖环境
pip3 install -r requirements.txt
# 执行迁移命令:
python3 manage.py makemigrations
python3 manage.py migrate
# 创建管理员用户
python3 manage.py createsuperuser
# 启动项目
python3 manage.py runserver
# 开启流量录制代理
python3 proxy.py
```
##  访问项目
```bash
# 登录管理后台,设置账号姓名
http://127.0.0.1:8000/admin/
# 浏览器打开
http://127.0.0.1:8888
用户/密码:管理员用户/密码
# 接口文档地址
http://127.0.0.1:8000/swagger/
```
## 开启定时/异步任务
```bash
# 启动celery,在控制台执行
celery -A backend worker -B --loglevel=info
```
##  演示图 ✅
![](screenshots/1.png)
![](screenshots/2.png)
![](screenshots/3.png)
![](screenshots/4.png)
![](screenshots/5.gif)
##  Docker构建
请参考文档[Docker构建](deployment/README.md)
测试组/Test_platform/Interface_automation/backend/.env.example
New file
@@ -0,0 +1,57 @@
# 本地开发时将.env.example文件重命名为.env 并配置相关参数
# 数据库地址
DATABASE_HOST=127.0.0.1
# 数据库端口
DATABASE_PORT=3306
# 数据库用户名
DATABASE_USER=root
# 数据库密码
DATABASE_PASSWORD=""
# 数据库名
DATABASE_NAME=lunarlink
# RabbitMQ配置
MQ_USER=guest
MQ_PASSWORD=""
MQ_HOST=127.0.0.1
MQ_PORT=5672
# Redis配置
REDIS_ON=True
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=""
REDIS_PORT=6379
REDIS_DB=0
DEBUG=True  # 线上环境请设置为False
# 启动登录日志记录(通过调用api获取ip详细地址。如果是内网,关闭即可)
ENABLE_LOGIN_ANALYSIS_LOG=True
BASE_REPORT_URL=http://localhost:8000/api/lunarlink/reports
IM_REPORT_BASE_URL=http://localhost
IM_REPORT_PORT=8000
IM_REPORT_TITLE=自动化测试报告
# 企微机器人 Webhook 地址
QY_WEB_HOOK=""
# 使用 SMTP 服务器发送邮件
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# SMTP 服务器地址
EMAIL_HOST="smtphz.qiye.163.com"
# SMTP 服务器端口
EMAIL_PORT=465
# 发件人邮箱账号
EMAIL_HOST_USER=""
DEFAULT_FROM_EMAIL=""
# 发件人邮箱密码
EMAIL_HOST_PASSWORD=""
# 是否使用 TLS
EMAIL_USE_TLS=False
# 是否使用 SSL
EMAIL_USE_SSL=True
# 录制流量代理配置
PROXY_ON=True  # 是否开启代理
PROXY_PORT=7778
测试组/Test_platform/Interface_automation/backend/.gitignore
New file
@@ -0,0 +1,26 @@
*.log
*.pyc
*.iml
.idea
.env
celerybeat.pid
start.sh.bak
mysql
/venv/
/CACHE/
# web
.DS_Store
web/node_modules/
web/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
测试组/Test_platform/Interface_automation/backend/apps/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/6/7 16:45
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/exceptions/convert.py
New file
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
@File    : convert.py
@Time    : 2023/9/11 16:02
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
class HarConvertError(Exception):
    """convert har file error"""
class GenerateError(Exception):
    """generate case error"""
测试组/Test_platform/Interface_automation/backend/apps/exceptions/error.py
New file
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
@File    : error.py
@Time    : 2023/10/9 15:42
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
class BaseError(Exception):
    pass
class RedisError(BaseError):
    """redis error"""
    pass
class NotFoundError(BaseError):
    pass
class TaskTimeIllegal(BaseError):
    pass
class TaskNotFound(BaseError):
    pass
class RelationNotFound(BaseError):
    pass
class FunctionNotFound(NotFoundError):
    pass
class VariableNotFound(NotFoundError):
    pass
class ApiNotFound(NotFoundError):
    pass
class CaseStepNotFound(NotFoundError):
    pass
class ConfigNotFound(NotFoundError):
    pass
class TestCaseNotFound(NotFoundError):
    pass
class TaskNotFound(NotFoundError):
    pass
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/__init__.py
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/admin.py
New file
@@ -0,0 +1,80 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from lunarlink.models import Project
# Register your models here.
Users = get_user_model()
class ProjectAdmin(admin.ModelAdmin):
    # 列表要显示的字段
    list_display = (
        "name",
        "responsible",
        "get_groups",
        "get_creator_name",
        "get_updater_name",
        "create_time",
        "update_time",
    )
    def get_creator_name(self, obj):
        """返回项目创建者的名字"""
        return obj.creator.name if obj.creator else "-"
    def get_updater_name(self, obj):
        """返回项目创建者的名字"""
        user = Users.objects.filter(id=obj.updater).first()
        return user.name if user else "-"
    get_creator_name.short_description = "创建人"
    get_updater_name.short_description = "更新人"
    def get_groups(self, obj):
        """返回项目所属的所有分组"""
        return ", ".join([group.name for group in obj.groups.all()])
    get_groups.short_description = "所属分组"
    def has_add_permission(self, request):
        return False  # 移除增加按钮
    def has_delete_permission(self, request, obj=None):
        return False  # 移除删除按钮
    # 指定在编辑页面上要显示的字段
    fields = [
        "name",
        "desc",
        "responsible",
        "yapi_base_url",
        "yapi_openapi_token",
        "jira_project_key",
        "jira_bearer_token",
        "groups",
    ]
    # 指定不可编辑的字段
    readonly_fields = ("responsible",)
    # 为ManyToMany字段提供一个水平滚动选择器
    filter_horizontal = ("groups",)
    def get_queryset(self, request):
        # 获取原始的queryset
        qs = super().get_queryset(request)
        # 如果是超级管理员,返回所有项目
        if request.user.is_superuser:
            return qs
        # 获取当前用户所在的所有分组
        user_groups = request.user.groups.all()
        # 基于分组来过滤项目
        return qs.filter(groups__in=user_groups).distinct()
admin.site.register(Project, ProjectAdmin)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/apps.py
New file
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class lunarlinkConfig(AppConfig):
    name = 'lunarlink'
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/dto/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py
@Time    : 2023/1/14 16:00
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/dto/tree_dto.py
New file
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
@File    : tree_dto.py
@Time    : 2023/1/14 16:00
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 数据校验
"""
from typing import Dict, List
from pydantic import BaseModel, Field
class TreeUniqueIn(BaseModel):
    project_id: int
    type: int
class TreeUpdateIn(BaseModel):
    tree: List[Dict] = Field(alias="body")
    type: int = Field(alias="type")
class TreeOut(BaseModel):
    tree: List[Dict]
    id: int
    max: int
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0001_initial.py
New file
@@ -0,0 +1,256 @@
# Generated by Django 3.2.1 on 2023-08-28 14:58
from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields
class Migration(migrations.Migration):
    initial = True
    dependencies = [
    ]
    operations = [
        migrations.CreateModel(
            name='Case',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(max_length=100, verbose_name='用例名称')),
                ('relation', models.IntegerField(verbose_name='节点id')),
                ('length', models.IntegerField(verbose_name='API个数')),
                ('tag', models.IntegerField(choices=[(1, '冒烟用例'), (2, '集成用例'), (3, '监控用例'), (4, '核心用例')], default=2, verbose_name='用例标签')),
            ],
            options={
                'verbose_name': '用例信息',
                'db_table': 'case',
            },
        ),
        migrations.CreateModel(
            name='Project',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(help_text='项目名称', max_length=100, verbose_name='项目名称')),
                ('desc', models.CharField(help_text='简要介绍', max_length=100, verbose_name='简要介绍')),
                ('responsible', models.CharField(help_text='负责人', max_length=20, verbose_name='负责人')),
                ('yapi_base_url', models.CharField(blank=True, default='', help_text='yapi的openapi url', max_length=100, verbose_name='yapi的openapi url')),
                ('yapi_openapi_token', models.CharField(blank=True, default='', help_text='yapi openapi的token', max_length=128, verbose_name='yapi openapi的token')),
                ('jira_project_key', models.CharField(blank=True, default='', help_text='jira项目key', max_length=30, verbose_name='jira项目key')),
                ('jira_bearer_token', models.CharField(blank=True, default='', help_text='jira bearer_token', max_length=45, verbose_name='jira bearer_token')),
            ],
            options={
                'verbose_name': '项目信息',
                'verbose_name_plural': '项目信息',
                'db_table': 'project',
            },
        ),
        migrations.CreateModel(
            name='Report',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(max_length=100, verbose_name='报告名称')),
                ('type', models.IntegerField(choices=[(1, '调试'), (2, '异步'), (3, '定时'), (4, '部署')], verbose_name='报告类型')),
                ('status', models.BooleanField(blank=True, choices=[(0, '失败'), (1, '成功')], verbose_name='报告状态')),
                ('summary', models.TextField(verbose_name='报告基础信息')),
                ('ci_metadata', jsonfield.fields.JSONField(default=dict)),
                ('ci_project_id', models.IntegerField(db_index=True, default=0, null=True, verbose_name='gitlab的项目id')),
                ('ci_job_id', models.CharField(db_index=True, default=None, max_length=15, null=True, unique=True, verbose_name='gitlab的项目id')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '测试报告',
                'db_table': 'report',
            },
        ),
        migrations.CreateModel(
            name='Visit',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('user', models.CharField(db_index=True, max_length=100, verbose_name='访问url的用户名')),
                ('ip', models.CharField(db_index=True, max_length=20, verbose_name='用户的ip')),
                ('project', models.CharField(db_index=True, default=0, max_length=4, verbose_name='项目id')),
                ('url', models.CharField(db_index=True, max_length=255, verbose_name='被访问的url')),
                ('path', models.CharField(db_index=True, default='', max_length=100, verbose_name='被访问的接口路径')),
                ('request_params', models.CharField(db_index=True, default='', max_length=255, verbose_name='请求参数')),
                ('request_method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PUT', 'PUT'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('OPTION', 'OPTION')], db_index=True, max_length=7, verbose_name='请求方法')),
                ('request_body', models.TextField(verbose_name='请求体')),
                ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
            ],
            options={
                'db_table': 'visit',
            },
        ),
        migrations.CreateModel(
            name='Variables',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('key', models.CharField(max_length=100, verbose_name='变量名')),
                ('value', models.CharField(max_length=1024, verbose_name='变量值')),
                ('description', models.CharField(max_length=100, null=True, verbose_name='全局变量描述')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '全局变量',
                'db_table': 'variables',
            },
        ),
        migrations.CreateModel(
            name='ReportDetail',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('summary_detail', models.TextField(verbose_name='报告详细信息')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('report', models.OneToOneField(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.report')),
            ],
            options={
                'verbose_name': '测试报告详情',
                'db_table': 'report_detail',
            },
        ),
        migrations.CreateModel(
            name='Relation',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('tree', models.TextField(default=[], verbose_name='结构主体')),
                ('type', models.IntegerField(default=1, verbose_name='树类型')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '树形结构关系',
                'db_table': 'relation',
            },
        ),
        migrations.CreateModel(
            name='HostIP',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(max_length=100, verbose_name='host名称')),
                ('value', models.TextField(verbose_name='值')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': 'HOST配置',
                'db_table': 'host_ip',
            },
        ),
        migrations.CreateModel(
            name='Debugtalk',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('code', models.TextField(default='# write you code', help_text='python代码', verbose_name='python代码')),
                ('project', models.OneToOneField(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '驱动库',
                'db_table': 'debugtalk',
            },
        ),
        migrations.CreateModel(
            name='Config',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(max_length=100, verbose_name='环境名称')),
                ('body', models.TextField(verbose_name='主体信息')),
                ('base_url', models.CharField(max_length=100, verbose_name='请求地址')),
                ('is_default', models.BooleanField(default=False, verbose_name='默认配置')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '环境信息',
                'db_table': 'config',
            },
        ),
        migrations.CreateModel(
            name='CaseStep',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(max_length=100, verbose_name='用例名称')),
                ('body', models.TextField(verbose_name='主体信息')),
                ('url', models.CharField(max_length=255, verbose_name='请求地址')),
                ('method', models.CharField(max_length=10, verbose_name='请求方式')),
                ('step', models.IntegerField(verbose_name='顺序')),
                ('source_api_id', models.IntegerField(verbose_name='api来源')),
                ('case', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.case')),
            ],
            options={
                'verbose_name': '用例信息 Step',
                'db_table': 'case_step',
            },
        ),
        migrations.AddField(
            model_name='case',
            name='project',
            field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project'),
        ),
        migrations.CreateModel(
            name='API',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('creator', models.CharField(help_text='创建人', max_length=20, null=True, verbose_name='创建人')),
                ('updater', models.CharField(help_text='更新人', max_length=20, null=True, verbose_name='更新人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('name', models.CharField(db_index=True, max_length=100, verbose_name='接口名称')),
                ('body', models.TextField(verbose_name='主体信息')),
                ('url', models.CharField(db_index=True, max_length=255, verbose_name='请求地址')),
                ('method', models.CharField(max_length=10, verbose_name='请求方式')),
                ('relation', models.IntegerField(verbose_name='节点id')),
                ('rig_id', models.IntegerField(db_index=True, null=True, verbose_name='网关API_id')),
                ('rig_env', models.IntegerField(choices=[(0, '测试环境'), (1, '生产环境'), (2, '预发布环境')], default=0, verbose_name='网关环境')),
                ('tag', models.IntegerField(choices=[(0, '未知'), (1, '成功'), (2, '失败'), (3, '自动成功'), (4, '废弃')], default=0, verbose_name='API标签')),
                ('yapi_catid', models.IntegerField(default=0, null=True, verbose_name='yapi的分组id')),
                ('yapi_id', models.IntegerField(default=0, null=True, verbose_name='yapi的id')),
                ('yapi_add_time', models.CharField(default='', max_length=10, null=True, verbose_name='yapi创建时间')),
                ('yapi_up_time', models.CharField(default='', max_length=10, null=True, verbose_name='yapi更新时间')),
                ('yapi_username', models.CharField(default='', max_length=30, null=True, verbose_name='yapi的原作者')),
                ('project', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='lunarlink.project')),
            ],
            options={
                'verbose_name': '接口信息',
                'db_table': 'api',
            },
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0002_project_groups.py
New file
@@ -0,0 +1,19 @@
# Generated by Django 3.2.1 on 2023-08-28 14:59
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
        ('lunarlink', '0001_initial'),
    ]
    operations = [
        migrations.AddField(
            model_name='project',
            name='groups',
            field=models.ManyToManyField(blank=True, help_text='这个项目所属的分组', to='auth.Group', verbose_name='分组'),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0003_generalpurposemodel.py
New file
@@ -0,0 +1,23 @@
# Generated by Django 3.2.1 on 2023-11-21 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0002_project_groups'),
    ]
    operations = [
        migrations.CreateModel(
            name='GeneralPurposeModel',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
            options={
                'db_table': 'general_purpose',
                'permissions': [('custom_permission', 'Can perform a custom action')],
            },
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0004_alter_generalpurposemodel_options.py
New file
@@ -0,0 +1,17 @@
# Generated by Django 3.2.1 on 2023-11-21 14:39
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0003_generalpurposemodel'),
    ]
    operations = [
        migrations.AlterModelOptions(
            name='generalpurposemodel',
            options={'permissions': [('custom_permission', 'Can perform a custom action')], 'verbose_name': '自定义权限'},
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0005_delete_generalpurposemodel.py
New file
@@ -0,0 +1,16 @@
# Generated by Django 3.2.1 on 2023-11-21 16:49
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0004_alter_generalpurposemodel_options'),
    ]
    operations = [
        migrations.DeleteModel(
            name='GeneralPurposeModel',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0006_auto_20240111_1219.py
New file
@@ -0,0 +1,61 @@
# Generated by Django 3.2.1 on 2024-01-11 12:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('lunarlink', '0005_delete_generalpurposemodel'),
    ]
    operations = [
        migrations.AddField(
            model_name='api',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='case',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='casestep',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='config',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='debugtalk',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='hostip',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='project',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='report',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
        migrations.AddField(
            model_name='variables',
            name='create_by',
            field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0007_auto_20240111_1228.py
New file
@@ -0,0 +1,49 @@
# Generated by Django 3.2.1 on 2024-01-11 12:28
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0006_auto_20240111_1219'),
    ]
    operations = [
        migrations.RemoveField(
            model_name='api',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='case',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='casestep',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='config',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='debugtalk',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='hostip',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='project',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='report',
            name='creator',
        ),
        migrations.RemoveField(
            model_name='variables',
            name='creator',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0008_auto_20240111_1228.py
New file
@@ -0,0 +1,58 @@
# Generated by Django 3.2.1 on 2024-01-11 12:28
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0007_auto_20240111_1228'),
    ]
    operations = [
        migrations.RenameField(
            model_name='api',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='case',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='casestep',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='config',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='debugtalk',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='hostip',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='project',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='report',
            old_name='create_by',
            new_name='creator',
        ),
        migrations.RenameField(
            model_name='variables',
            old_name='create_by',
            new_name='creator',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0009_auto_20240112_1349.py
New file
@@ -0,0 +1,58 @@
# Generated by Django 3.2.1 on 2024-01-12 13:49
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0008_auto_20240111_1228'),
    ]
    operations = [
        migrations.AddField(
            model_name='api',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='case',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='casestep',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='config',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='debugtalk',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='hostip',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='project',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='report',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
        migrations.AddField(
            model_name='variables',
            name='modifier',
            field=models.IntegerField(help_text='修改人', null=True, verbose_name='修改人'),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0010_auto_20240112_1412.py
New file
@@ -0,0 +1,49 @@
# Generated by Django 3.2.1 on 2024-01-12 14:12
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0009_auto_20240112_1349'),
    ]
    operations = [
        migrations.RemoveField(
            model_name='api',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='case',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='casestep',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='config',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='debugtalk',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='hostip',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='project',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='report',
            name='updater',
        ),
        migrations.RemoveField(
            model_name='variables',
            name='updater',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0011_auto_20240112_1412.py
New file
@@ -0,0 +1,58 @@
# Generated by Django 3.2.1 on 2024-01-12 14:12
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0010_auto_20240112_1412'),
    ]
    operations = [
        migrations.RenameField(
            model_name='api',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='case',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='casestep',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='config',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='debugtalk',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='hostip',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='project',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='report',
            old_name='modifier',
            new_name='updater',
        ),
        migrations.RenameField(
            model_name='variables',
            old_name='modifier',
            new_name='updater',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0012_loginlog.py
New file
@@ -0,0 +1,50 @@
# Generated by Django 3.2.1 on 2024-01-15 10:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('lunarlink', '0011_auto_20240112_1412'),
    ]
    operations = [
        migrations.CreateModel(
            name='LoginLog',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')),
                ('update_time', models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')),
                ('updater', models.IntegerField(help_text='修改人', null=True, verbose_name='修改人')),
                ('is_deleted', models.BooleanField(default=False, verbose_name='是否删除')),
                ('username', models.CharField(blank=True, help_text='登录用户名', max_length=32, null=True, verbose_name='登录用户名')),
                ('ip', models.CharField(blank=True, help_text='登录ip', max_length=32, null=True, verbose_name='登录ip')),
                ('agent', models.TextField(blank=True, help_text='agent信息', null=True, verbose_name='agent信息')),
                ('browser', models.CharField(blank=True, help_text='浏览器名', max_length=200, null=True, verbose_name='浏览器名')),
                ('os', models.CharField(blank=True, help_text='操作系统', max_length=200, null=True, verbose_name='操作系统')),
                ('continent', models.CharField(blank=True, help_text='州', max_length=50, null=True, verbose_name='州')),
                ('country', models.CharField(blank=True, help_text='国家', max_length=50, null=True, verbose_name='国家')),
                ('province', models.CharField(blank=True, help_text='省份', max_length=50, null=True, verbose_name='省份')),
                ('city', models.CharField(blank=True, help_text='城市', max_length=50, null=True, verbose_name='城市')),
                ('district', models.CharField(blank=True, help_text='县区', max_length=50, null=True, verbose_name='县区')),
                ('isp', models.CharField(blank=True, help_text='运营商', max_length=50, null=True, verbose_name='运营商')),
                ('area_code', models.CharField(blank=True, help_text='区域代码', max_length=50, null=True, verbose_name='区域代码')),
                ('country_english', models.CharField(blank=True, help_text='英文全称', max_length=50, null=True, verbose_name='英文全称')),
                ('country_code', models.CharField(blank=True, help_text='简称', max_length=50, null=True, verbose_name='简称')),
                ('longitude', models.CharField(blank=True, help_text='经度', max_length=50, null=True, verbose_name='经度')),
                ('latitude', models.CharField(blank=True, help_text='纬度', max_length=50, null=True, verbose_name='纬度')),
                ('login_type', models.IntegerField(choices=[(1, '普通登录')], default=1, help_text='登录类型', verbose_name='登录类型')),
                ('creator', models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
            ],
            options={
                'verbose_name': '登录日志',
                'verbose_name_plural': '登录日志',
                'db_table': 'login_log',
                'ordering': ('-create_time',),
            },
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0013_loginlog_name.py
New file
@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2024-01-15 14:08
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0012_loginlog'),
    ]
    operations = [
        migrations.AddField(
            model_name='loginlog',
            name='name',
            field=models.CharField(blank=True, help_text='登录用户姓名', max_length=20, null=True, verbose_name='登录用户姓名'),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/0014_delete_hostip.py
New file
@@ -0,0 +1,16 @@
# Generated by Django 3.2.1 on 2024-04-10 14:53
from django.db import migrations
class Migration(migrations.Migration):
    dependencies = [
        ('lunarlink', '0013_loginlog_name'),
    ]
    operations = [
        migrations.DeleteModel(
            name='HostIP',
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/migrations/__init__.py
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/models.py
New file
@@ -0,0 +1,596 @@
from ast import literal_eval
import jsonfield
from django.contrib.auth.models import Group
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils import timezone
from django_celery_beat.models import PeriodicTask
from model_utils import Choices
from backend import settings
# Create your models here.
class SoftDeleteQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_deleted=False)
class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return SoftDeleteQuerySet(self.model, using=self._db).active()
    def with_deleted(self):
        return SoftDeleteQuerySet(self.model, using=self._db)
class BaseTable(models.Model):
    """
    公共字段
    """
    class Meta:
        abstract = True
        verbose_name = "公共字段表"
    create_time = models.DateTimeField(
        verbose_name="创建时间", auto_now_add=True, help_text="创建时间"
    )
    update_time = models.DateTimeField(
        verbose_name="更新时间", auto_now=True, help_text="更新时间"
    )
    creator = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        related_query_name="creator_query",
        null=True,
        verbose_name="创建人",
        help_text="创建人",
        on_delete=models.SET_NULL,
        db_constraint=False,
    )
    updater = models.IntegerField(
        verbose_name="修改人",
        null=True,
        help_text="修改人",
    )
    is_deleted = models.BooleanField(verbose_name="是否删除", default=False)
    objects = SoftDeleteManager()
class Project(BaseTable):
    """项目信息表"""
    class Meta:
        verbose_name = "项目信息"
        verbose_name_plural = "项目信息"
        db_table = "project"
    name = models.CharField(
        verbose_name="项目名称", null=False, max_length=100, help_text="项目名称"
    )
    desc = models.CharField(
        verbose_name="简要介绍", max_length=100, null=False, help_text="简要介绍"
    )
    responsible = models.CharField(
        verbose_name="负责人", max_length=20, null=False, help_text="负责人"
    )
    yapi_base_url = models.CharField(
        verbose_name="yapi的openapi url",
        max_length=100,
        null=False,
        default="",
        blank=True,
        help_text="yapi的openapi url",
    )
    yapi_openapi_token = models.CharField(
        verbose_name="yapi openapi的token",
        max_length=128,
        null=False,
        default="",
        blank=True,
        help_text="yapi openapi的token",
    )
    jira_project_key = models.CharField(
        verbose_name="jira项目key",
        null=False,
        default="",
        max_length=30,
        blank=True,
        help_text="jira项目key",
    )
    jira_bearer_token = models.CharField(
        verbose_name="jira bearer_token",
        null=False,
        default="",
        max_length=45,
        blank=True,
        help_text="jira bearer_token",
    )
    groups = models.ManyToManyField(
        to=Group, verbose_name="分组", help_text="这个项目所属的分组", blank=True
    )
@receiver(pre_save, sender=Project)
def delete_related_objects(sender, instance, **kwargs):
    if instance.is_deleted:
        related_models = [
            Config,
            API,
            Case,
            Variables,
            Report,
            Debugtalk,
            Relation,
        ]  # 所有与 Project 直接关联的模型
        # 先筛选出要删除的对象
        case_objects = Case.objects.filter(project=instance)
        report_objects = Report.objects.filter(project=instance)
        for related_model in related_models:
            related_model.objects.filter(project=instance).update(is_deleted=True)
        PeriodicTask.objects.filter(description=instance.id).delete()
        # 注意:这部分操作可能会很慢,如果有大量的数据,考虑性能问题
        ReportDetail.objects.filter(report__in=report_objects).update(is_deleted=True)
        CaseStep.objects.filter(case__in=case_objects).update(
            is_deleted=True,
            updater=instance.updater,
            update_time=timezone.now(),
        )
class Config(BaseTable):
    """环境信息表"""
    class Meta:
        verbose_name = "环境信息"
        db_table = "config"
    name = models.CharField(verbose_name="环境名称", null=False, max_length=100)
    body = models.TextField(verbose_name="主体信息", null=False)
    base_url = models.CharField(verbose_name="请求地址", null=False, max_length=100)
    project = models.ForeignKey(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
    is_default = models.BooleanField("默认配置", default=False)
class API(BaseTable):
    """
    API信息表
    """
    class Meta:
        verbose_name = "接口信息"
        db_table = "api"
    ENV_TYPE = ((0, "测试环境"), (1, "生产环境"), (2, "预发布环境"))
    TAG = Choices(
        (0, "未知"),
        (1, "成功"),
        (2, "失败"),
        (3, "自动成功"),
        (4, "废弃"),
    )
    name = models.CharField(
        verbose_name="接口名称", null=False, max_length=100, db_index=True
    )
    body = models.TextField(verbose_name="主体信息", null=False)
    url = models.CharField(
        verbose_name="请求地址", null=False, max_length=255, db_index=True
    )
    method = models.CharField(verbose_name="请求方式", null=False, max_length=10)
    project = models.ForeignKey(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
    relation = models.IntegerField(verbose_name="节点id", null=False)
    rig_id = models.IntegerField(verbose_name="网关API_id", null=True, db_index=True)
    rig_env = models.IntegerField(verbose_name="网关环境", choices=ENV_TYPE, default=0)
    tag = models.IntegerField(verbose_name="API标签", choices=TAG, default=0)
    yapi_catid = models.IntegerField(verbose_name="yapi的分组id", null=True, default=0)
    yapi_id = models.IntegerField(verbose_name="yapi的id", null=True, default=0)
    yapi_add_time = models.CharField(
        verbose_name="yapi创建时间", null=True, default="", max_length=10
    )
    yapi_up_time = models.CharField(
        verbose_name="yapi更新时间", null=True, default="", max_length=10
    )
    yapi_username = models.CharField(
        verbose_name="yapi的原作者", null=True, default="", max_length=30
    )
class Case(BaseTable):
    """
    用例信息表
    """
    class Meta:
        verbose_name = "用例信息"
        db_table = "case"
    tag = (
        (1, "冒烟用例"),
        (2, "集成用例"),
        (3, "监控用例"),
        (4, "核心用例"),
    )
    name = models.CharField(verbose_name="用例名称", null=False, max_length=100)
    project = models.ForeignKey(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
    relation = models.IntegerField(verbose_name="节点id", null=False)
    length = models.IntegerField(verbose_name="API个数", null=False)
    tag = models.IntegerField(verbose_name="用例标签", choices=tag, default=2)
    @property
    def tasks(self):
        task_objs = PeriodicTask.objects.filter(description=self.project.id).values(
            "id",
            "name",
            "args",
        )
        def process_task(task):
            # 处理每个任务的name字段
            name_parts = task["name"].split("_")
            if len(name_parts) > 1:
                task["name"] = name_parts[1]
            return task
        # 过滤并处理任务
        processed_tasks = [
            process_task(task)
            for task in task_objs
            if self.id in literal_eval(task.pop("args"))
        ]
        return processed_tasks
@receiver(pre_save, sender=Case)
def delete_related_steps(sender, instance, **kwargs):
    """
    当一个 Case 对象被删除时,删除所有与其相关的 CaseStep 对象。
    """
    CaseStep.objects.filter(case=instance).update(
        is_deleted=True, update_time=timezone.now()
    )
class CaseStep(BaseTable):
    """
    测试用例 Step.
    """
    class Meta:
        verbose_name = "用例信息 Step"
        db_table = "case_step"
    name = models.CharField(verbose_name="用例名称", null=False, max_length=100)
    body = models.TextField(verbose_name="主体信息", null=False)
    url = models.CharField(verbose_name="请求地址", null=False, max_length=255)
    method = models.CharField(verbose_name="请求方式", null=False, max_length=10)
    case = models.ForeignKey(to=Case, on_delete=models.CASCADE, db_constraint=False)
    step = models.IntegerField(verbose_name="顺序", null=False)
    source_api_id = models.IntegerField(verbose_name="api来源", null=False)
class Variables(BaseTable):
    """
    全局变量
    """
    class Meta:
        verbose_name = "全局变量"
        db_table = "variables"
    key = models.CharField(verbose_name="变量名", null=False, max_length=100)
    value = models.CharField(verbose_name="变量值", null=False, max_length=1024)
    project = models.ForeignKey(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
    description = models.CharField(
        verbose_name="全局变量描述", null=True, max_length=100
    )
class Debugtalk(BaseTable):
    """
    驱动代码表
    """
    class Meta:
        verbose_name = "驱动库"
        db_table = "debugtalk"
    code = models.TextField(
        verbose_name="python代码",
        default="# write you code",
        null=False,
        help_text="python代码",
    )
    project = models.OneToOneField(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
class Report(BaseTable):
    """报告存储"""
    report_type = (
        (1, "调试"),
        (2, "异步"),
        (3, "定时"),
        (4, "部署"),
    )
    report_status = (
        (0, "失败"),
        (1, "成功"),
    )
    class Meta:
        verbose_name = "测试报告"
        db_table = "report"
    name = models.CharField(
        verbose_name="报告名称",
        null=False,
        max_length=100,
    )
    type = models.IntegerField(
        verbose_name="报告类型",
        choices=report_type,
    )
    status = models.BooleanField(
        verbose_name="报告状态",
        choices=report_status,
        blank=True,
    )
    summary = models.TextField(verbose_name="报告基础信息", null=False)
    project = models.ForeignKey(
        to=Project,
        on_delete=models.CASCADE,
        db_constraint=False,
    )
    ci_metadata = jsonfield.JSONField()
    ci_project_id = models.IntegerField(
        verbose_name="gitlab的项目id",
        default=0,
        null=True,
        db_index=True,
    )
    ci_job_id = models.CharField(
        verbose_name="gitlab的项目id",
        unique=True,
        null=True,
        default=None,
        db_index=True,
        max_length=15,
    )
    @property
    def ci_job_url(self):
        if self.ci_metadata:
            return self.ci_metadata.get("ci_job_url")
@receiver(pre_save, sender=Report)
def delete_related_report_detail(sender, instance, **kwargs):
    """
    当一个 Report 对象被删除时,删除所有与其相关的 ReportDetail 对象。
    """
    ReportDetail.objects.filter(report=instance).update(is_deleted=True)
class ReportDetail(models.Model):
    """报告详情"""
    class Meta:
        verbose_name = "测试报告详情"
        db_table = "report_detail"
    report = models.OneToOneField(
        to=Report,
        on_delete=models.CASCADE,
        null=True,
        db_constraint=False,
    )
    summary_detail = models.TextField(verbose_name="报告详细信息")
    is_deleted = models.BooleanField(verbose_name="是否删除", default=False)
    objects = SoftDeleteManager()
class Relation(models.Model):
    """树形结构关系"""
    class Meta:
        verbose_name = "树形结构关系"
        db_table = "relation"
    tree = models.TextField(verbose_name="结构主体", null=False, default=[])
    type = models.IntegerField(verbose_name="树类型", default=1)
    project = models.ForeignKey(
        to=Project, on_delete=models.CASCADE, db_constraint=False
    )
    is_deleted = models.BooleanField(verbose_name="是否删除", default=False)
    objects = SoftDeleteManager()
class Visit(models.Model):
    METHODS = Choices(
        ("GET", "GET"),
        ("POST", "POST"),
        ("PUT", "PUT"),
        ("PATCH", "PATCH"),
        ("DELETE", "DELETE"),
        ("OPTION", "OPTION"),
    )
    class Meta:
        db_table = "visit"
    user = models.CharField(
        verbose_name="访问url的用户名", max_length=100, db_index=True
    )
    ip = models.CharField(verbose_name="用户的ip", max_length=20, db_index=True)
    project = models.CharField(
        verbose_name="项目id", max_length=4, db_index=True, default=0
    )
    url = models.CharField(verbose_name="被访问的url", max_length=255, db_index=True)
    path = models.CharField(
        verbose_name="被访问的接口路径", max_length=100, default="", db_index=True
    )
    request_params = models.CharField(
        verbose_name="请求参数", max_length=255, default="", db_index=True
    )
    request_method = models.CharField(
        verbose_name="请求方法", max_length=7, choices=METHODS, db_index=True
    )
    request_body = models.TextField(verbose_name="请求体")
    create_time = models.DateTimeField(
        verbose_name="创建时间", auto_now_add=True, db_index=True
    )
class LoginLog(BaseTable):
    LOGIN_TYPE_CHOICES = ((1, "普通登录"),)
    name = models.CharField(
        max_length=20,
        verbose_name="登录用户姓名",
        null=True,
        blank=True,
        help_text="登录用户姓名",
    )
    username = models.CharField(
        max_length=32,
        verbose_name="登录用户名",
        null=True,
        blank=True,
        help_text="登录用户名",
    )
    ip = models.CharField(
        max_length=32,
        verbose_name="登录ip",
        null=True,
        blank=True,
        help_text="登录ip",
    )
    agent = models.TextField(
        verbose_name="agent信息",
        null=True,
        blank=True,
        help_text="agent信息",
    )
    browser = models.CharField(
        max_length=200,
        verbose_name="浏览器名",
        null=True,
        blank=True,
        help_text="浏览器名",
    )
    os = models.CharField(
        max_length=200,
        verbose_name="操作系统",
        null=True,
        blank=True,
        help_text="操作系统",
    )
    continent = models.CharField(
        max_length=50,
        verbose_name="州",
        null=True,
        blank=True,
        help_text="州",
    )
    country = models.CharField(
        max_length=50,
        verbose_name="国家",
        null=True,
        blank=True,
        help_text="国家",
    )
    province = models.CharField(
        max_length=50,
        verbose_name="省份",
        null=True,
        blank=True,
        help_text="省份",
    )
    city = models.CharField(
        max_length=50,
        verbose_name="城市",
        null=True,
        blank=True,
        help_text="城市",
    )
    district = models.CharField(
        max_length=50,
        verbose_name="县区",
        null=True,
        blank=True,
        help_text="县区",
    )
    isp = models.CharField(
        max_length=50,
        verbose_name="运营商",
        null=True,
        blank=True,
        help_text="运营商",
    )
    area_code = models.CharField(
        max_length=50,
        verbose_name="区域代码",
        null=True,
        blank=True,
        help_text="区域代码",
    )
    country_english = models.CharField(
        max_length=50,
        verbose_name="英文全称",
        null=True,
        blank=True,
        help_text="英文全称",
    )
    country_code = models.CharField(
        max_length=50,
        verbose_name="简称",
        null=True,
        blank=True,
        help_text="简称",
    )
    longitude = models.CharField(
        max_length=50,
        verbose_name="经度",
        null=True,
        blank=True,
        help_text="经度",
    )
    latitude = models.CharField(
        max_length=50,
        verbose_name="纬度",
        null=True,
        blank=True,
        help_text="纬度",
    )
    login_type = models.IntegerField(
        default=1,
        choices=LOGIN_TYPE_CHOICES,
        verbose_name="登录类型",
        help_text="登录类型",
    )
    class Meta:
        db_table = "login_log"
        verbose_name = "登录日志"
        verbose_name_plural = verbose_name
        ordering = ("-create_time",)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/serializers.py
New file
@@ -0,0 +1,541 @@
# -*- coding: utf-8 -*-
"""
@File    : serializers.py
@Time    : 2023/1/14 14:25
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 序列化&反序列化
"""
import datetime
import json
from ast import literal_eval
from typing import Union
from croniter import croniter
from django.contrib.auth import get_user_model
from django.db.models import Q
from django_celery_beat.models import PeriodicTask
from rest_framework import serializers
from lunarlink import models
from lunarlink.utils.parser import Parse
from lunarlink.utils.tree import get_tree_relation_name
Users = get_user_model()
class ProjectSerializer(serializers.ModelSerializer):
    """项目信息序列化"""
    api_cover_rate = serializers.SerializerMethodField()
    creator_name = serializers.SlugRelatedField(
        slug_field="name",
        source="creator",
        read_only=True,
    )
    updater_name = serializers.SerializerMethodField(read_only=True)
    class Meta:
        model = models.Project
        fields = [
            "id",
            "name",
            "desc",
            "responsible",
            "creator",
            "creator_name",
            "create_time",
            "updater",
            "updater_name",
            "update_time",
            "yapi_openapi_token",
            "yapi_base_url",
            "api_cover_rate",
            "jira_project_key",
            "jira_bearer_token",
        ]
    def get_updater_name(self, obj):
        if not hasattr(obj, "updater") or obj.updater is None:
            return None
        user = Users.objects.filter(id=obj.updater).first()
        return user.name if user else None
    def get_api_cover_rate(self, obj) -> str:
        """
        接口覆盖率,百分比保留两位小数
        :param obj: Project实例对象
        :return:
        """
        apis = (
            models.API.objects.filter(project_id=obj.id, is_deleted=False)
            .filter(~Q(tag=4))
            .values("url", "method")
        )
        api_unique = {f'{api["url"]}_{api["method"]}' for api in apis}
        case_steps = (
            models.CaseStep.objects.filter(case__project_id=obj.id)
            .filter(~Q(method="config"))
            .values("url", "method")
        )
        case_steps_unique = {
            f'{case_step["url"]}_{case_step["method"]}' for case_step in case_steps
        }
        if len(api_unique) == 0:
            return "0.00"
        if len(case_steps_unique) > len(api_unique):
            return "100.00"
        return "%.2f" % (len(case_steps_unique & api_unique) / len(api_unique) * 100)
class VisitSerializer(serializers.ModelSerializer):
    """
    访问统计序列化
    """
    class Meta:
        model = models.Visit
        fields = "__all__"
class DebugTalkSerializer(serializers.ModelSerializer):
    """
    驱动代码序列化
    """
    class Meta:
        model = models.Debugtalk
        fields = "__all__"
class RelationSerializer(serializers.ModelSerializer):
    """树形结构序列化"""
    class Meta:
        model = models.Relation
        fields = "__all__"
class AssertSerializer(serializers.Serializer):
    """API搜索序列化"""
    class Meta:
        model = models.API
    node = serializers.IntegerField(min_value=0, default=None)
    project = serializers.IntegerField(required=True, min_value=1)
    search = serializers.CharField(default="")
    creator = serializers.CharField(required=False, default="")
    tag = serializers.ChoiceField(choices=models.API.TAG, default="")
    rigEnv = serializers.ChoiceField(choices=models.API.ENV_TYPE, default="")
    is_deleted = serializers.BooleanField(default=False)
    onlyMe = serializers.BooleanField(default=False)
    showYAPI = serializers.BooleanField(default=True)
    start_time = serializers.CharField(required=False, default=None)
    end_time = serializers.CharField(required=False, default=None)
class CaseSearchSerializer(serializers.Serializer):
    """
    用例反序列化验证器
    """
    creator = serializers.CharField(required=False, default="")
    node = serializers.IntegerField(min_value=0, default=None)
    project = serializers.IntegerField(required=True, min_value=1)
    search = serializers.CharField(default="")
    searchType = serializers.CharField(default="")
    caseType = serializers.CharField(default="")
    onlyMe = serializers.BooleanField(default=False)
    start_time = serializers.CharField(required=False, default=None)
    end_time = serializers.CharField(required=False, default=None)
class CaseSerializer(serializers.ModelSerializer):
    """用例信息序列化"""
    creator_name = serializers.SlugRelatedField(
        slug_field="name",
        source="creator",
        read_only=True,
    )
    tag = serializers.CharField(source="get_tag_display")
    tasks = serializers.ListField(read_only=True)  # 包含用例的定时任务
    updater_name = serializers.SerializerMethodField(read_only=True)
    class Meta:
        model = models.Case
        fields = "__all__"
    def get_updater_name(self, obj):
        if not hasattr(obj, "updater") or obj.updater is None:
            return None
        user = Users.objects.filter(id=obj.updater).first()
        return user.name if user else None
class CaseStepSerializer(serializers.ModelSerializer):
    """用例步骤序列化"""
    body = serializers.SerializerMethodField()
    class Meta:
        model = models.CaseStep
        fields = [
            "id",
            "name",
            "url",
            "method",
            "body",
            "case",
            "source_api_id",
            "creator",
            "updater",
        ]
        depth = 1
    def get_body(self, obj):
        body = literal_eval(obj.body)
        if "base_url" in body["request"].keys():
            return {"name": body["name"], "method": "config"}
        else:
            parse = Parse(literal_eval(obj.body))
            parse.parse_http()
            return parse.testcase
class APIRelatedCaseSerializer(serializers.Serializer):
    case_name = serializers.CharField(source="case.name")
    case_id = serializers.CharField(source="case.id")
    class Meta:
        fields = ["case_id", "case_name"]
class APISerializer(serializers.ModelSerializer):
    """
    接口信息序列化
    """
    creator_name = serializers.SlugRelatedField(
        slug_field="name",
        source="creator",
        read_only=True,
    )
    body = serializers.SerializerMethodField()
    tag_name = serializers.CharField(source="get_tag_display")
    cases = serializers.SerializerMethodField()
    relation_name = serializers.SerializerMethodField()
    updater_name = serializers.SerializerMethodField(read_only=True)
    class Meta:
        model = models.API
        fields = [
            "id",
            "name",
            "url",
            "method",
            "project",
            "relation",
            "body",
            "rig_env",
            "tag",
            "tag_name",
            "update_time",
            "is_deleted",
            "creator",
            "creator_name",
            "updater",
            "updater_name",
            "cases",
            "relation_name",
        ]
    def get_body(self, obj):
        parse = Parse(literal_eval(obj.body))
        parse.parse_http()
        return parse.testcase
    def get_cases(self, obj):
        case_steps = models.CaseStep.objects.filter(source_api_id=obj.id)
        cases = APIRelatedCaseSerializer(many=True, instance=case_steps)
        return cases.data
    def get_relation_name(self, obj):
        relation_obj = models.Relation.objects.get(project_id=obj.project_id, type=1)
        label = get_tree_relation_name(literal_eval(relation_obj.tree), obj.relation)
        return label
    def get_updater_name(self, obj):
        if not hasattr(obj, "updater") or obj.updater is None:
            return None
        user = Users.objects.filter(id=obj.updater).first()
        return user.name if user else None
class ConfigSerializer(serializers.ModelSerializer):
    """配置信息序列化"""
    body = serializers.SerializerMethodField()
    class Meta:
        model = models.Config
        fields = [
            "id",
            "base_url",
            "body",
            "name",
            "update_time",
            "is_default",
            "creator",
            "updater",
        ]
    def get_body(self, obj):
        parse = Parse(literal_eval(obj.body), level="config")
        parse.parse_http()
        return parse.testcase
class VariablesSerializer(serializers.ModelSerializer):
    """
    变量信息序列化
    """
    key = serializers.CharField(allow_null=False, max_length=100, required=True)
    value = serializers.CharField(allow_null=False, max_length=1024)
    description = serializers.CharField(required=False, allow_blank=True)
    class Meta:
        model = models.Variables
        fields = "__all__"
class ReportSerializer(serializers.ModelSerializer):
    """报告信息序列化"""
    creator = serializers.SlugRelatedField(
        slug_field="name",
        read_only=True,
    )
    type = serializers.CharField(source="get_type_display")
    time = serializers.SerializerMethodField()
    stat = serializers.SerializerMethodField()
    platform = serializers.SerializerMethodField()
    success = serializers.SerializerMethodField()
    ci_job_url = serializers.CharField()
    class Meta:
        model = models.Report
        fields = [
            "id",
            "name",
            "type",
            "time",
            "stat",
            "platform",
            "success",
            "creator",
            "updater",
            "ci_job_url",
        ]
    def get_time(self, obj):
        return json.loads(obj.summary)["time"]
    def get_stat(self, obj):
        return json.loads(obj.summary)["stat"]
    def get_platform(self, obj):
        return json.loads(obj.summary)["platform"]
    def get_success(self, obj):
        return json.loads(obj.summary)["success"]
def get_cron_next_execute_time(crontab_expr: str) -> int:
    """
    获取下一个符合给定cron表达式的时间,并将其转换为时间戳返回
    :param crontab_expr: cron表达式时间
    :return:
    """
    now = datetime.datetime.now()
    cron = croniter(crontab_expr, now)
    next_time: datetime.datetime = cron.get_next(datetime.datetime)
    return int(next_time.timestamp())
class PeriodicTaskSerializer(serializers.ModelSerializer):
    """
    定时任务信息序列化
    """
    kwargs = serializers.SerializerMethodField()
    args = serializers.SerializerMethodField()
    name = serializers.SerializerMethodField()
    last_run_at = serializers.SerializerMethodField()
    class Meta:
        model = PeriodicTask
        fields = [
            "id",
            "name",
            "args",
            "kwargs",
            "enabled",
            "date_changed",
            "enabled",
            "description",
            "total_run_count",
            "last_run_at",
        ]
    def get_name(self, obj):
        """
        兼容定时任务名称必须唯一
        :param obj:
        :return:
        """
        name: str = obj.name
        return name.split("_")[1]
    def get_kwargs(self, obj):
        kwargs = json.loads(obj.kwargs)
        if obj.enabled:
            kwargs["next_execute_time"] = get_cron_next_execute_time(kwargs["crontab"])
        kwargs["ci_project_ids"] = kwargs.get("ci_project_ids", "")
        kwargs["ci_env"] = kwargs.get("ci_env", "请选择")
        kwargs["config"] = kwargs.get("config", "请选择")
        # False:串行,True:并行
        kwargs["is_parallel"] = kwargs.get("is_parallel", False)
        return kwargs
    def get_args(self, obj):
        case_id_list = json.loads(obj.args)
        # 数据格式, list of dict: [{"id":case_id, "name"}]
        return list(
            models.Case.objects.filter(pk__in=case_id_list).values("id", "name")
        )
    def get_last_run_at(self, obj) -> Union[str, int]:
        if obj.last_run_at:
            return int(obj.last_run_at.timestamp())
        return ""
class ScheduleDeSerializer(serializers.Serializer):
    """
    定时任务反序列化
    """
    switch = serializers.BooleanField(required=True, help_text="定时任务开关")
    crontab = serializers.CharField(
        required=True, help_text="定时任务表达式", max_length=100, allow_blank=True
    )
    ci_project_ids = serializers.CharField(
        required=True,
        allow_blank=True,
        help_text="Gitlab的项目id,多个用逗号分开,一个项目id对应多个task,但只能在同一个项目中",
    )
    strategy = serializers.CharField(required=True, help_text="发送通知策略", max_length=20)
    receiver = serializers.CharField(
        required=True, help_text="邮件接收者列表", allow_blank=True, max_length=100
    )
    mail_cc = serializers.CharField(
        required=True, help_text="邮件抄送列表", allow_blank=True, max_length=100
    )
    name = serializers.CharField(required=True, help_text="定时任务名称", max_length=100)
    webhook = serializers.CharField(
        required=True,
        help_text="飞书/企微/钉钉webhook url",
        trim_whitespace=True,
        allow_blank=True,
        max_length=500,
    )
    updater = serializers.CharField(
        required=False,
        help_text="更新人",
        max_length=20,
        allow_null=True,
        allow_blank=True,
    )
    creator = serializers.CharField(
        required=False,
        help_text="创建人",
        max_length=20,
        allow_null=True,
        allow_blank=True,
    )
    data = serializers.ListField(required=True, help_text="用例id")
    project = serializers.IntegerField(
        required=True, help_text="测试平台的项目id", min_value=1
    )
    def validate_crontab(self, value):
        """
        检查cron表达式是否有效
        """
        if not croniter.is_valid(value):
            raise serializers.ValidationError("无效的 cron 表达式")
        return value
    def validate_ci_project_ids(self, ci_project_ids):
        if ci_project_ids:
            not_allowed_project_ids = set()
            kwargs_list = PeriodicTask.objects.filter(
                ~Q(description=self.initial_data["project"])
            ).values("kwargs")
            for kwargs in kwargs_list:
                not_allowed_project_id: str = json.loads(kwargs["kwargs"]).get(
                    "ci_project_ids", ""
                )
                if not_allowed_project_id:
                    not_allowed_project_ids.update(not_allowed_project_id.split(","))
            validation_errors = set()
            for ci_project_id in ci_project_ids.split(","):
                if ci_project_id in not_allowed_project_ids:
                    validation_errors.add(ci_project_id)
            if validation_errors:
                raise serializers.ValidationError(
                    f"{','.join(validation_errors)} 已经在其他项目存在"
                )
class CISerializer(serializers.Serializer):
    """持续集成序列化"""
    ci_job_id = serializers.IntegerField(
        required=True,
        min_value=1,
        help_text="gitlab-ci job id",
    )
    ci_job_url = serializers.CharField(required=True, max_length=500)
    ci_pipeline_id = serializers.IntegerField(required=True)
    ci_pipeline_url = serializers.CharField(required=True, max_length=500)
    ci_project_id = serializers.IntegerField(required=True, min_value=1)
    ci_project_name = serializers.CharField(required=True, max_length=100)
    ci_project_namespace = serializers.CharField(required=False, max_length=100)
    start_job_user = serializers.CharField(
        required=True,
        max_length=100,
        help_text="GITLAB_USER_NAME",
    )
class CIReportSerializer(serializers.Serializer):
    """持续集成报告序列化"""
    ci_job_id = serializers.IntegerField(
        required=True,
        min_value=1,
        help_text="gitlab-ci job id",
    )
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/services/tree_service_impl.py
New file
@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
"""
@File    : tree_service_impl.py
@Time    : 2023/1/14 16:10
@Author  : geekbing
@LastEditTime : 2023/7/27 21:10
@LastEditors : geekbing
@Description : 树形结构操作
"""
from ast import literal_eval
import traceback
from typing import Dict, List, Optional
from loguru import logger
from crud.base_crud import GenericCURD
from lunarlink.dto.tree_dto import TreeOut, TreeUniqueIn, TreeUpdateIn
from lunarlink.models import API, Case, Relation
from lunarlink.utils.response import (
    TREE_GET_SUCCESS,
    TREE_UPDATE_SUCCESS,
    StandResponse,
)
from lunarlink.utils.tree import get_tree_max_id
from lunarlink.utils.enums.TreeTypeEnum import TreeType
class TreeService:
    def __init__(self):
        self.model = Relation
        self.curd = GenericCURD(self.model)
    def get_or_create(self, query: TreeUniqueIn) -> StandResponse[TreeOut]:
        default_tree = [
            {
                "id": 1,
                "label": "默认目录",
                "children": [],
            }
        ]
        tree_obj, is_created = self.curd.get_or_create(
            filter_kwargs=query.dict(),
            defaults={"tree": default_tree, "project_id": query.project_id},
        )
        if is_created:
            logger.info(f"tree created {query=}")
            body: List[Dict] = tree_obj.tree
        else:
            logger.info(f"tree exist {query}")
            body: List[Dict] = literal_eval(tree_obj.tree)
        node_api_case_counts = {}
        for node in body:
            if query.type == TreeType.API:
                TreeService.add_api_count_to_node(
                    tree_obj.project_id,
                    node,
                    node_api_case_counts,
                )
            else:
                TreeService.add_case_count_to_node(
                    tree_obj.project_id,
                    node,
                    node_api_case_counts,
                )
        for root_node in body:
            TreeService.add_api_case_count_to_tree(root_node, node_api_case_counts)
        tree = {
            "tree": body,
            "id": tree_obj.id,
            "success": True,
            "max": get_tree_max_id(body),
        }
        return StandResponse[TreeOut](**TREE_GET_SUCCESS, data=TreeOut(**tree))
    @staticmethod
    def add_case_count_to_node(project_id: int, node: Dict, node_case_count: Dict):
        """
        递归获取节点的用例数量
        :param project_id:
        :param node:
        :param node_case_count:
        :return:
        """
        node_id = node["id"]
        case_count = Case.objects.filter(
            project_id=project_id, relation=node_id
        ).count()
        node_case_count[node_id] = case_count
        for child in node.get("children", []):
            TreeService.add_case_count_to_node(project_id, child, node_case_count)
    @staticmethod
    def add_api_count_to_node(project_id: int, node: Dict, node_api_count: Dict):
        """
        递归获取节点的接口数量
        :param project_id:
        :param node:
        :param node_api_count:
        :return:
        """
        node_id = node["id"]
        api_count = API.objects.filter(project_id=project_id, relation=node_id).count()
        node_api_count[node_id] = api_count
        for child in node.get("children", []):
            TreeService.add_api_count_to_node(project_id, child, node_api_count)
    @staticmethod
    def add_api_case_count_to_tree(node: Dict, node_api_case_counts: Dict):
        """
        将节点的接口或用例数量嵌入原树形结构中
        :param node:
        :param node_api_case_counts:
        :return:
        """
        data_count = node_api_case_counts.get(node["id"], 0)
        if "children" in node:
            for child in node["children"]:
                data_count += TreeService.add_api_case_count_to_tree(
                    child, node_api_case_counts
                )
        node["data_count"] = data_count
        return data_count
    @staticmethod
    def check_related_api(node_id: int, project_id: int) -> bool:
        # 检查是否存在关联的接口
        return API.objects.filter(relation=node_id, project_id=project_id).exists()
    @staticmethod
    def check_related_case(node_id: int, project_id: int) -> bool:
        # 检查是否存在关联的用例
        return Case.objects.filter(relation=node_id, project_id=project_id).exists()
    def get_all_ids_from_tree(self, tree):
        """
        获取树的所有节点id
        :param tree:
        :return:
        """
        ids = [node["id"] for node in tree]
        for node in tree:
            if "children" in node:
                ids.extend(self.get_all_ids_from_tree(node["children"]))
        return ids
    def _check_related_api_recursive(self, current_tree, payload_tree, project_id):
        """
        提取payload.tree所有节点id,递归检查current_tree中是否存在这些id,如果不存在,检查是否有关联的API
        """
        payload_tree_id_list = self.get_all_ids_from_tree(payload_tree)
        for node in current_tree:
            if node["id"] not in payload_tree_id_list:
                # 这个节点正在被删除,检查是否有关联的API
                if self.check_related_api(node["id"], project_id):
                    return True
            # 如果该节点有子节点,继续递归检查
            if "children" in node and node["children"]:
                if self._check_related_api_recursive(
                    node["children"], payload_tree, project_id
                ):
                    return True
        return False
    def _check_related_case_recursive(self, current_tree, payload_tree, project_id):
        """
        提取payload.tree所有节点id,递归检查current_tree中是否存在这些id,如果不存在,检查是否有关联的用例
        """
        payload_tree_id_list = self.get_all_ids_from_tree(payload_tree)
        for node in current_tree:
            if node["id"] not in payload_tree_id_list:
                # 这个节点正在被删除,检查是否有关联的API
                if self.check_related_case(node["id"], project_id):
                    return True
            # 如果该节点有子节点,继续递归检查
            if "children" in node and node["children"]:
                if self._check_related_case_recursive(
                    node["children"], payload_tree, project_id
                ):
                    return True
        return False
    @staticmethod
    def get_current_tree(relation_obj) -> List:
        """
        获取当前的树形结构, 转成python对象
        :param relation_obj:
        :return:
        """
        if not relation_obj:
            return []
        return literal_eval(relation_obj.tree)
    def patch(self, tree_id: int, payload: TreeUpdateIn):
        """
        更新树形结构
        :param tree_id:
        :param payload:
        :return:
        """
        # 获取当前的树结构
        if not payload.tree:
            return StandResponse[Optional[TreeOut]](
                code="9999",
                success=False,
                msg="删除失败, 至少保留一个根目录",
                data=None,
            )
        relation_obj = self.curd.get_obj_by_pk(pk=tree_id)
        current_tree = self.get_current_tree(relation_obj)
        # 检查每个节点,如果在当前树中找到了这个节点,但在新树中找不到,就意味着这个节点被删除了
        if payload.type == TreeType.API:
            if self._check_related_api_recursive(
                current_tree, payload.tree, relation_obj.project_id
            ):
                # 如果存在关联的接口,返回提示信息
                return StandResponse[Optional[TreeOut]](
                    code="9999",
                    success=False,
                    msg="目录有关联接口,不能删除",
                    data=None,
                )
        elif payload.type == TreeType.CASE:
            # 如果存在关联的用例,返回提示信息
            if self._check_related_case_recursive(
                current_tree, payload.tree, relation_obj.project_id
            ):
                # 如果存在关联的接口,返回提示信息
                return StandResponse[Optional[TreeOut]](
                    code="9999",
                    success=False,
                    msg="目录有关联用例,不能删除",
                    data=None,
                )
        # 如果没有问题,尝试更新
        try:
            tree_obj = self.curd.update_obj_by_pk(
                pk=tree_id, updater="", payload=payload.dict()
            )
        except Exception as e:
            return self._handle_exception(e)
        tree: List[Dict] = tree_obj.tree
        node_api_case_counts = {}
        for node in tree:
            if payload.type == TreeType.API:
                TreeService.add_api_count_to_node(
                    tree_obj.project_id,
                    node,
                    node_api_case_counts,
                )
            else:
                TreeService.add_case_count_to_node(
                    tree_obj.project_id,
                    node,
                    node_api_case_counts,
                )
        for root_node in tree:
            TreeService.add_api_case_count_to_tree(root_node, node_api_case_counts)
        return StandResponse[TreeOut](
            **TREE_UPDATE_SUCCESS,
            data=TreeOut(tree=tree, id=tree_obj.id, max=get_tree_max_id(tree)),
        )
    @staticmethod
    def _handle_exception(e: Exception) -> StandResponse[Optional[TreeOut]]:
        err: str = traceback.format_exc()
        logger.warning(f"Exception {e} occurred with traceback: {err}")
        return StandResponse[Optional[TreeOut]](
            code="9999", success=False, msg=err, data=None
        )
tree_service = TreeService()
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tasks.py
New file
@@ -0,0 +1,414 @@
# -*- coding: utf-8 -*-
"""
@File    : tasks.py
@Time    : 2023/2/6 11:07
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 定时任务、异步任务
"""
import logging
from ast import literal_eval
from enum import IntEnum
from celery import shared_task, Task
from django_bulk_update.helper import bulk_update
from django_celery_beat.models import PeriodicTask
from django.db.models import F
from django.core.exceptions import ObjectDoesNotExist
from lunarlink import models
from lunarlink.utils.loader import save_summary, debug_api, debug_suite
from lunarlink.utils.parser import Yapi
from lunarlink.utils import qy_message, email_helper
from lunarlink.utils.message_template import dd_msg_template
from lunarlink.utils import response
from lunarlink.utils.dingtalk_helper import DingTalkHelper
from lunarlink.utils.message_template import parse_message
import time
# 填写你的钉钉机器人 access_token 和 secret
ACCESS_TOKEN = 'd42ec7c82717e8cad1c06c2caef4484cac590eb779aecec8e541750c29588d16'
SECRET = 'SEC96c0ef4507f86bdcb5c0c1237a32260130127825ec9439f9c54f69e9fd1a7056'
# 创建 DingTalkHelper 实例
dingtalk_helper = DingTalkHelper(ACCESS_TOKEN, SECRET)
logger = logging.getLogger(__name__)
class ReportType(IntEnum):
    DEPLOY = 4
    TIMING = 3
def update_task_total_run_count(task_id):
    """增加任务的总运行次数
    :param task_id:
    :return:
    """
    if task_id:
        PeriodicTask.objects.filter(id=task_id).update(
            date_changed=F("date_changed"),
            total_run_count=F("total_run_count") + 1,
        )
class MyBaseTask(Task):
    def run(self, *args, **kwargs):
        pass
    def on_failure(self, exc, task_id, args, kwargs, einfo):
        update_task_total_run_count(kwargs.get("task_id"))
    def on_success(self, retval, task_id, args, kwargs):
        update_task_total_run_count(kwargs.get("task_id"))
@shared_task
def async_import_yapi_api(yapi_base_url, yapi_token, project_id):
    """异步导入yapi接口"""
    yapi = Yapi(
        yapi_base_url=yapi_base_url,
        token=yapi_token,
        faster_project_id=project_id,
    )
    apis_imported_from_yapi = models.API.objects.filter(
        project_id=project_id,
        creator__name="yapi",
    )
    imported_apis_mapping = {
        api.yapi_id: api.yapi_up_time for api in apis_imported_from_yapi
    }
    create_ids, update_ids = yapi.get_create_or_update_apis(
        imported_apis_mapping=imported_apis_mapping
    )
    try:
        # 获取yapi的分组, 然后更新api tree
        yapi.create_relation_id(project_id=yapi.faster_project_id)
    except Exception as e:
        logger.error(f"导入yapi失败:{e}")
        return {"status": "error", "message": response.YAPI_ADD_FAILED}
    # 通过id获取所有api的详情
    create_ids.extend(update_ids)
    if not create_ids:
        return {"status": "error", "message": response.YAPI_NOT_NEED_CREATE_OR_UPDATE}
    new_api_instances = []
    update_api_instances = []
    for api_detail in yapi.get_batch_api_detail(api_ids=create_ids):
        # 把yapi解析成符合faster的api格式
        api_instances = yapi.get_parsed_apis(api_info=[api_detail])
        updated_api, new_api = yapi.merge_api(
            api_instances=api_instances,
            apis_imported_from_yapi=apis_imported_from_yapi,
        )
        new_api_instances.extend(new_api)
        update_api_instances.extend(updated_api)
    created_objs = models.API.objects.bulk_create(objs=new_api_instances)
    bulk_update(update_api_instances)
    created_apis_count = len(created_objs)
    updated_apis_count = len(update_api_instances)
    return {
        "status": "success",
        "created_apis_count": created_apis_count,
        "updated_apis_count": updated_apis_count,
    }
@shared_task
def async_debug_api(api, project, name, config=None, user=None):
    """
    异步执行api
    :param api:
    :param project:
    :param name:
    :param config:
    :param user:
    :return:
    """
    summary = debug_api(
        api=api,
        project=project,
        config=config,
        save=False,
        user=user,
    )
    report_id = save_summary(
        name=name,
        summary=summary,
        project=project,
        user=user,
    )
    return {
        "status": "success",
        "report_id": report_id,
    }
@shared_task
def async_debug_suite(suite, project, obj, report, config, user=None):
    """异步执行suite"""
    summary, _ = debug_suite(
        suite=suite,
        project=project,
        obj=obj,
        config=config,
        save=False,
        user=user,
    )
    report_id = save_summary(
        name=report,
        summary=summary,
        project=project,
        user=user,
    )
    return {
        "status": "success",
        "report_id": report_id,
    }
def get_test_suite(args):
    """
    获取测试用例集
    :param args:
    :return:
    """
    case_ids = [pk for pk in args if models.Case.objects.filter(id=pk).exists()]
    cases = models.Case.objects.in_bulk(case_ids)
    return [{"name": case.name, "id": case.id} for case in cases.values()]
def process_override_config(kwargs, project):
    """
    处理覆盖用例原有配置
    :param kwargs:
    :param project:
    :return:
    """
    override_config = kwargs.get("config", "")
    override_config_body = None
    if override_config and override_config != "请选择":
        try:
            override_config_body = literal_eval(
                models.Config.objects.get(
                    name=override_config, project__id=project
                ).body
            )
        except ObjectDoesNotExist:
            logger.error(response.CONFIG_NOT_EXISTS["msg"])
    return override_config_body
def build_test_sets(suite, project, override_config_body):
    """
    构建测试集
    :param suite:
    :param project:
    :param override_config_body:
    :return:
    """
    test_sets = []
    config_list = []
    for content in suite:
        test_list = (
            models.CaseStep.objects.filter(case__id=content["id"])
            .order_by("step")
            .values("body")
        )
        testcase_list = []
        config = None
        for test in test_list:
            body = literal_eval(test["body"])
            if "base_url" in body["request"].keys():
                if override_config_body:
                    config = override_config_body
                    continue
                try:
                    config = literal_eval(
                        models.Config.objects.get(
                            name=body["name"], project__id=project
                        ).body
                    )
                except ObjectDoesNotExist:
                    logger.error(response.CONFIG_NOT_EXISTS["msg"])
                continue
            testcase_list.append(body)
        config_list.append(config)
        test_sets.append(testcase_list)
    return test_sets, config_list
def execute_test_suite(test_sets, project, suite, config_list, is_parallel):
    """
    执行测试套件
    :param test_sets:
    :param project:
    :param suite:
    :param config_list:
    :param is_parallel:
    :return:
    """
    return debug_suite(
        suite=test_sets,
        project=project,
        obj=suite,
        config=config_list,
        allow_parallel=is_parallel,
        save=False,
    )
def prepare_report_details(kwargs):
    """
    准备报告详情
    :param kwargs:
    :return:
    """
    task_name = kwargs["task_name"]
    if kwargs.get("run_type") == "deploy":
        task_name = "部署_" + task_name
        report_type = ReportType.DEPLOY.value
    else:
        report_type = ReportType.TIMING.value
    return task_name, report_type
def save_schedule_summary(task_name, summary, project, report_type):
    """
    保存测试报告
    :param task_name:
    :param summary:
    :param project:
    :param report_type:
    :return:
    """
    return save_summary(
        name=task_name,
        summary=summary,
        project=project,
        report_type=report_type,
    )
def send_notifications(args, kwargs, summary, task_name, report_id):
    """
    发送通知
    :param args:
    :param kwargs:
    :param summary:
    :param task_name:
    :param report_id:
    :return:
    """
    strategy = kwargs.get("strategy")
    if strategy == "始终发送" or (
        strategy == "仅失败发送" and summary["stat"]["failures"] > 0
    ):
        summary.update({"task_name": task_name, "report_id": report_id})
        webhook = kwargs.get("webhook", "")
        email_recipient = kwargs.get("receiver", "")
        email_cc = kwargs.get("mail_cc", "")
        # 解析 summary 获取各项数据
        parsed_data = parse_message(summary, case_count=len(args))
        t_name = parsed_data["task_name"]
        duration = parsed_data["duration"]
        case_count = parsed_data["case_count"]
        pass_count = parsed_data["pass_count"]
        error_count = parsed_data["error_count"]
        fail_count = parsed_data["fail_count"]
        fail_rate = parsed_data["fail_rate"]
        report_url = parsed_data["report_url"]
        creator = kwargs.get("creator", "")
        updater = kwargs.get("updater", "")
        # 生成钉钉消息模板(返回字典,包含 msgtype 和 markdown)
        dd_message = dd_msg_template(
            task_name=t_name,
            duration=duration,
            case_count=case_count,
            pass_count=pass_count,
            error_count=error_count,
            fail_count=fail_count,
            fail_rate=fail_rate,
            report_url=report_url,
            creator=creator,
            updater=updater,
        )
        # 将 dd_message 中的 markdown 内容传递给钉钉发送方法
        message_text = dd_message["markdown"]["content"]
        if webhook:
            qy_message.send_message(
                summary=summary,
                webhook=webhook,
                case_count=len(args),
            )
            dingtalk_helper.send_markdown(title=t_name, text=message_text)
        if email_recipient:
            email_helper.send(
                summary=summary,
                email_recipient=email_recipient,
                email_cc=email_cc,
                case_count=len(args),
            )
            dingtalk_helper.send_markdown(title=t_name, text=message_text)
@shared_task(base=MyBaseTask, queue="beat_tasks")
def schedule_debug_suite(*args, **kwargs):
    """定时任务"""
    project = kwargs.get("project")
    suite = get_test_suite(args)
    override_config_body = process_override_config(kwargs, project)
    test_sets, config_list = build_test_sets(
        suite=suite,
        project=project,
        override_config_body=override_config_body,
    )
    is_parallel = kwargs.get("is_parallel", False)
    summary, _ = execute_test_suite(
        test_sets=test_sets,
        project=project,
        suite=suite,
        config_list=config_list,
        is_parallel=is_parallel,
    )
    task_name, report_type = prepare_report_details(kwargs)
    report_id = save_schedule_summary(
        task_name=task_name,
        summary=summary,
        project=project,
        report_type=report_type,
    )
    send_notifications(
        args,
        kwargs,
        summary,
        task_name,
        report_id,
    )
    return {
        "status": "success",
        "report_id": report_id,
    }
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/templatetags/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/3/8 11:27
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/templatetags/custom_tags.py
New file
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
@File    : custom_tags.py
@Time    : 2023/3/8 11:29
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 格式化测试报告中的JSON
"""
import json
import time
from django import template
from loguru import logger
register = template.Library()
@register.filter(name="json_dumps")
def json_dumps(value):
    """将Python对象序列化为JSON格式的字符串
    :param value: 要过滤的变量
    :return:
    """
    try:
        if isinstance(value, dict):
            return json.dumps(
                value, indent=4, separators=(",", ": "), ensure_ascii=False
            )
        else:
            return json.dumps(
                json.loads(value), indent=4, separators=(",", ": "), ensure_ascii=False
            )
    except (TypeError, json.JSONDecodeError) as e:
        logger.error(f"An error occurred while processing JSON: {e}")
        return value
@register.filter(name="convert_timestamp")
def convert_timestamp(value):
    """将时间戳转换为指定格式的本地时间,并返回格式化后的字符串
    :param value: 时间戳
    :return:
    """
    try:
        return time.strftime("%Y--%m--%d %H:%M:%S", time.localtime(int(float(value))))
    except (ValueError, TypeError):
        pass
    return value
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tests.py
New file
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tests/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/2/28 17:24
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 测试用例
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/urls.py
New file
@@ -0,0 +1,237 @@
# -*- coding: utf-8 -*-
"""
@File    : urls.py
@Time    : 2023/1/14 11:23
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from django.urls import path
from lunarlink.views import (
    api,
    ci,
    config,
    debugtalk,
    project,
    report,
    run,
    schedule,
    suite,
    yapi,
    variables,  # 确保variables已经导入
)
urlpatterns = [
    # 访问统计相关接口
    path(
        "visit",
        project.VisitView.as_view({"get": "list"}),
    ),
    # 项目相关接口
    path(
        "project",
        project.ProjectView.as_view(
            {
                "get": "list",
                "post": "add",
                "patch": "update",
                "delete": "delete",
            }
        ),
    ),
    path("project/<int:pk>", project.ProjectView.as_view({"get": "single"})),
    path("project/yapi/<int:pk>", project.ProjectView.as_view({"get": "yapi_info"})),
    path("dashboard", project.DashBoardView.as_view()),
    # 二叉树接口
    path("tree/<int:pk>", project.TreeView.as_view()),
    # 导入yapi
    path("yapi/<int:pk>", yapi.YAPIView.as_view()),
    # api模板
    path(
        "api",
        api.APITemplateView.as_view(
            {
                "post": "add",
                "get": "list",
                "delete": "bulk_destroy",
            }
        ),
    ),
    path(
        "api/<int:pk>",
        api.APITemplateView.as_view(
            {
                "delete": "destroy",
                "patch": "update",
                "post": "copy",
                "get": "single",
            }
        ),
    ),
    path(
        "api/move_api",
        api.APITemplateView.as_view({"patch": "move"}),  # api修改relation所属目录
    ),
    path(
        "api/tag",
        api.APITemplateView.as_view({"patch": "add_tag"}),  # api修改状态
    ),
    path(
        "api/sync/<int:pk>",
        api.APITemplateView.as_view({"patch": "sync_case"}),  # api同步测试用例
    ),
    # 测试用例
    path(
        "test",
        suite.TestCaseView.as_view(
            {
                "get": "get",
                "post": "post",
                "delete": "bulk_destroy",
            }
        ),
    ),
    path(
        "test/<int:pk>",
        suite.TestCaseView.as_view(
            {
                # 如果请求方法和处理方法同名时可以省略
                "post": "copy",
                "delete": "destroy",
                "put": "put",
                "patch": "patch",
            }
        ),
    ),
    path(
        "test/move_case",
        suite.TestCaseView.as_view({"patch": "move"}),  # case修改relation
    ),
    path(
        "test/tag",
        suite.TestCaseView.as_view({"patch": "update_tag"}),
    ),
    path("teststep/<int:pk>", suite.CaseStepView.as_view()),
    # 用例录制
    path("record/start", suite.RecordStartView.as_view(), name="record_start"),
    path("record/stop", suite.RecordStopView.as_view(), name="record_stop"),
    path("record/status", suite.RecordStatusView.as_view(), name="record_status"),
    path("record/remove", suite.RecordRemoveView.as_view(), name="record_remove"),
    path("record_case", suite.GenerateCaseView.as_view(), name="generate_case"),
    # run api 运行API
    path("run_api_pk/<int:pk>", run.run_api_pk),
    path("run_api", run.run_api),
    # run testsuite 运行测试用例集
    path("run_testsuite", run.run_testsuite),
    path("run_testsuite_pk/<int:pk>", run.run_testsuite_pk),
    path("run_test", run.run_test),
    path("run_suite_tree", run.run_suite_tree),
    path("run_multi_tests", run.run_multi_tests),
    # config-配置管理
    path(
        "config",
        config.ConfigView.as_view(
            {
                "post": "add",
                "get": "list",
                "delete": "bulk_destroy",
            }
        ),
    ),
    path(
        "config/<int:pk>",
        config.ConfigView.as_view(
            {
                "patch": "update",
                "post": "copy",
                "delete": "destroy",
                "get": "all",
            }
        ),
    ),
    # 全局变量
    path(
        "variables",
        variables.VariablesView.as_view(
            {
                "get": "list",
                "post": "add",
                "delete": "bulk_destroy",
            }
        ),
    ),
    path(
        "variables/<int:pk>",
        variables.VariablesView.as_view(
            {
                "patch": "update",
                "delete": "destroy",
            }
        ),
    ),
    # debugtalk.py相关接口
    path("debugtalk/<int:pk>", debugtalk.DebugTalkView.as_view({"get": "debugtalk"})),
    path(
        "debugtalk",
        debugtalk.DebugTalkView.as_view(
            {
                "patch": "update",
                "post": "run",
            }
        ),
    ),
    # 历史报告
    path(
        "reports",
        report.ReportView.as_view(
            {
                "get": "list",
                "delete": "bulk_destroy",
            }
        ),
    ),
    path(
        "reports/<int:pk>",
        report.ReportView.as_view(
            {
                "delete": "destroy",
                "get": "look",
            }
        ),
    ),
    # 定时任务相关接口
    path(
        "schedule",
        schedule.ScheduleView.as_view(
            {
                "get": "list",
                "post": "add",
            }
        ),
    ),
    path(
        "schedule/<int:pk>",
        schedule.ScheduleView.as_view(
            {
                "get": "run",
                "put": "update",
                "patch": "patch",
                "delete": "delete",
                "post": "copy",
            }
        ),
    ),
    # gitlab-ci, 当前暂未用到
    path(
        "gitlab-ci/",
        ci.CIView.as_view(
            {
                "post": "run_ci_tests",
                "get": "get_ci_report_url",
            }
        ),
    ),
]
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py
@Time    : 2023/1/14 15:10
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/convert2hrp.py
New file
@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
"""
@File    : convert2hrp.py
@Time    : 2023/2/13 10:29
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import json
import os
from enum import Enum
from typing import Any, Dict, List, Text, Union, Callable
from urllib.parse import urlparse
from pydantic import BaseModel, Field
from pydantic import HttpUrl
Name = Text
Url = Text
BaseUrl = Union[HttpUrl, Text]
VariablesMapping = Dict[Text, Any]
FunctionsMapping = Dict[Text, Callable]
Headers = Dict[Text, Text]
Cookies = Dict[Text, Text]
Verify = bool
Hooks = List[Union[Text, Dict[Text, Text]]]
Export = List[Text]
Validators = List[Dict]
Env = Dict[Text, Any]
class MethodEnum(Text, Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    DELETE = "DELETE"
    HEAD = "HEAD"
    OPTIONS = "OPTIONS"
    PATCH = "PATCH"
class TConfig(BaseModel):
    name: Name
    verify: Verify = False
    base_url: BaseUrl = ""
    # Text: prepare variables in debugtalk.py, ${gen_variables(}
    variables: Union[VariablesMapping, Text] = {}
    parameters: Union[VariablesMapping, Text] = {}
    # setup_hooks: Hooks = []
    # teardown_hooks: Hooks = []
    export: Export = []
    path: Text = None
    weight: int = 1
class TRequest(BaseModel):
    """requests.Request model"""
    # TODO: 先注释TRequest-method类型,后期再优化
    method: str
    url: Url
    params: Dict[Text, Text] = {}
    headers: Headers = {}
    req_json: Union[Dict, List, Text] = Field(None)
    body: Union[Text, Dict[Text, Any]] = None
    cookies: Cookies = {}
    timeout: float = 120
    allow_redirects: bool = True
    verify: Verify = False
    upload: Dict = {}  # used for upload files
class TStep(BaseModel):
    name: Name
    request: Union[TRequest, None] = None
    testcase: Union[Text, Callable, None] = None
    variables: VariablesMapping = {}
    setup_hooks: Hooks = []
    # used to extract request's response field
    extract: VariablesMapping = {}
    # used to export session variable from referenced testcase
    export: Export = []
    validators: Validators = Field([], alias="validate")
    validate_script: List[Text] = []
class TestCase(BaseModel):
    config: TConfig
    teststeps: List[TStep]
class Hrp:
    def __init__(self, faster_req_json: Dict):
        self.faster_req_json = faster_req_json
    def parse_url(self):
        url = self.faster_req_json["url"]
        o = urlparse(url=url)
        baseurl = o.scheme + "://" + o.netloc
        return baseurl, o.path
    def get_headers(self):
        headers: Dict = self.faster_req_json.get("headers", {})
        # Content-Length may be error
        headers.pop("Content-Length", None)
        return headers
    def get_request(self) -> TRequest:
        base_url, path = self.parse_url()
        req = TRequest(
            method=self.faster_req_json["method"],
            url=base_url + path,
            params=self.faster_req_json.get("params", {}),
            headers=self.get_headers(),
            body=self.faster_req_json.get("body", {}),
            req_json=self.faster_req_json.get("json", {}),
            verify=self.faster_req_json.get("verify", False),
        )
        return req
    def get_step(self) -> TStep:
        _, path = self.parse_url()
        return TStep(
            name=path,
            request=self.get_request(),
        )
    def get_config(self) -> TConfig:
        base_url, _ = self.parse_url()
        return TConfig(
            name=base_url,
            base_url=base_url,
        )
    def get_testcase(self) -> TestCase:
        config = self.get_config()
        teststeps: List = [self.get_step()]
        return TestCase(
            config=config,
            teststeps=teststeps,
        )
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/day.py
New file
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
"""
@File    : day.py
@Time    : 2023/1/14 17:06
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import datetime
from httprunner.builtin import get_ts_int
def get_day(days: int = 0, **kwargs):
    """
    获取日期,默认有连接符
    :param days: 间隔时间
    :param kwargs:
    :return:
    >>> get_day()
    # 今天的日期 2023-02-01
    >>> get_day(1)
    # 明天的日期
    >>> get_day(-1)
    # 昨天的日期
    >>> get_day(h=9,m=15,s=30)
    # 今天的9时15分30秒 20230116 9:15:30
    """
    d = datetime.timedelta(days)
    n = datetime.datetime.now()
    time_str = f"%Y-%m-%d"
    if kwargs:
        h = kwargs.get("h", "00")
        m = kwargs.get("m", "00")
        s = kwargs.get("s", "00")
        time_str = f"{time_str} {h}:{m}:{s}"
    return (n + d).strftime(time_str)
def get_month(month_delta: int = 0, base_ts=get_ts_int) -> str:
    """生成年月
    :param month_delta: 间隔月份
    :param base_ts: 基准时间
    :return:
    >>> base_ts = 1629439316  # 2021-08-20 14:01:56
    >>> get_month(5, base_ts)
    '202201'
    >>> get_month(17, base_ts)
    '202301'
    >>> get_month(-20, base_ts)
    '201912'
    >>> get_month(-8, base_ts)
    '202012'
    >>> get_month(base_ts=base_ts)
    '202108'
    >>> get_month(1, base_ts)
    '202109'
    >>> get_month(-1, base_ts)
    '202107'
    """
    if callable(base_ts):
        base_ts = base_ts()
    dt = datetime.datetime.fromtimestamp(base_ts)
    year, month = dt.year, dt.month
    # 超过12个月,先计算年份,然后再对月份差取余
    if abs(month_delta) > 12:
        year_delta = abs(month_delta) // 12
        if month_delta > 0:
            year += year_delta
            month_delta = month_delta % 12
        else:
            year -= year_delta
            month_delta = -(abs(month_delta) % 12)
    # 月份差是正数,有两种情况
    # 1.月份差和当前的月份相加 > 12, 年份+1, 月份等于超过的减去12
    # 2.月份差和当前月份相加 <= 12, 直接相加
    if month_delta >= 0:
        if month + month_delta > 12:
            year += 1
            month = month + month_delta - 12
        else:
            month += month_delta
    else:
        # 月份差是负数, 有两种情况
        # 1.月份差 <= 0, 年份-1, 月份从12开始倒退
        # 2.直接倒退月份
        if month - abs(month_delta) <= 0:
            year -= 1
            # month + month_delta 是负数
            month = 12 + (month + month_delta)
        else:
            month += month_delta
    month = str(month).rjust(2, "0")
    return f"{year}{month}"
def get_week(weeks: int = 0) -> str:
    """
    生成一年中的第n周
    :param weeks:
    :return:
    """
    week_delta = 7 * 24 * 60 * 60 * weeks
    ts = get_ts_int(week_delta)
    dt = datetime.datetime.fromtimestamp(ts)
    year, week_number, _ = dt.isocalendar()
    # 确保周数是两位数,例如 '01', '02', ..., '11', '12', ...
    week_str = str(week_number).zfill(2)
    # '202333' 2023年的第33周
    return f"{year}{week_str}"
def get_month_format(month_delta: int = 0, base_ts=get_ts_int):
    """
    :param month_delta:
    :param base_ts:
    :return:
    """
    month = get_month(month_delta, base_ts)
    return f"{month[:4]}年{month[4:]}月"
def get_week_format(weeks: int) -> str:
    """
    :param weeks:
    :return:
    """
    week = get_week(weeks)
    return f"{week[:4]}年{week[4:]}周"
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/decorator.py
New file
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
@File    : decorator.py
@Time    : 2023/1/16 14:43
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 日志
"""
import functools
import logging
from lunarlink.utils import parser
logger = logging.getLogger(__name__)
def request_log(level):
    def wrapper(func):
        @functools.wraps(func)
        def inner_wrapper(request, *args, **kwargs):
            msg_data = (
                f"before process request data:\n{parser.format_json(request.data)}"
            )
            msg_params = f"before process request params:\n{parser.format_json(request.query_params)}"
            if level == "INFO":
                if request.data:
                    logger.info(msg_data)
                if request.query_params:
                    logger.info(msg_params)
            elif level == "DEBUG":
                if request.data:
                    logger.debug(msg_data)
                if request.query_params:
                    logger.debug(msg_params)
            return func(request, *args, **kwargs)
        return inner_wrapper
    return wrapper
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/dingtalk_helper.py
New file
@@ -0,0 +1,87 @@
import time
import hmac
import hashlib
import base64
import urllib.parse
import requests
import json
class DingTalkHelper:
    def __init__(self, access_token, secret):
        self.access_token = access_token
        self.secret = secret
    def calculate_sign(self):
        # 获取当前时间戳,单位为毫秒
        timestamp = str(round(time.time() * 1000))
        # 使用 HMAC SHA256 加密算法生成签名
        string_to_sign = '{}\n{}'.format(timestamp, self.secret)
        string_to_sign_enc = string_to_sign.encode('utf-8')
        secret_enc = self.secret.encode('utf-8')
        # 计算 HMAC 值
        hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
        # 使用 base64 编码并对 URL 进行编码
        sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
        return timestamp, sign
    def send_markdown(self, title, text, is_at_all=False):
        timestamp, sign = self.calculate_sign()
        dingtalk_webhook = f'https://oapi.dingtalk.com/robot/send?access_token={self.access_token}&timestamp={timestamp}&sign={sign}'
        payload = {
            "msgtype": "markdown",
            "markdown": {
                "title": title,
                "text": text
            },
            "at": {
                "isAtAll": is_at_all
            }
        }
        try:
            response = requests.post(dingtalk_webhook, json=payload)
            if response.status_code == 200:
                print("Markdown消息发送成功!")
            else:
                print(f"发送失败,状态码:{response.status_code}")
        except Exception as e:
            print(f"发送异常:{e}")
    def send_message(self, message):
        # 计算签名和时间戳
        timestamp, sign = self.calculate_sign()
        # 构建钉钉 Webhook URL,拼接上 timestamp 和 sign
        dingtalk_webhook = f'https://oapi.dingtalk.com/robot/send?access_token={self.access_token}&timestamp={timestamp}&sign={sign}'
        headers = {
            'Content-Type': 'application/json',
        }
        # 构建发送的消息内容
        payload = {
            "msgtype": "text",
            "text": {
                "content": message
            }
        }
        try:
            # 发送请求
            response = requests.post(dingtalk_webhook, json=payload, headers=headers)
            # 输出返回的状态码和响应内容
            print(f"Response status: {response.status_code}")
            print(f"Response text: {response.text}")
            if response.status_code == 200:
                print("Message sent successfully!")
            else:
                print("Failed to send message.")
        except Exception as e:
            print(f"Error occurred: {e}")
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/email_helper.py
New file
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""
@File    : email_helper.py
@Time    : 2023/9/8 11:05
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 邮件发送
"""
import logging
from typing import Dict, Union
from django.core.mail import EmailMessage
from lunarlink.utils.message_template import parse_message, email_msg_template
logger = logging.getLogger(__name__)
def send_mail_with_cc(
    subject: str,
    html_message: str,
    recipient_list: list[str],
    cc_list: list[str] = None,
) -> bool:
    """
    发送带有抄送人的HTML邮件
    :param subject: 邮件主题
    :param html_message: HTML格式的邮件内容
    :param recipient_list: 收件人邮箱列表
    :param cc_list: 抄送人邮箱列表
    :return: 发送邮件成功返回True,失败返回False
    """
    email = EmailMessage(
        subject=subject,
        body=html_message,
        to=recipient_list,
        cc=cc_list,
    )
    email.content_subtype = "html"
    return email.send()
def send(summary: Union[Dict, str], email_recipient: str, email_cc: str, **kwargs):
    """
    发送邮件
    :param summary: 报告摘要
    :param email_recipient: 邮件接收人
    :param email_cc: 邮件抄送人
    :param kwargs:
    :return:
    """
    recipient_list = email_recipient.split(";")
    ccr_list = email_cc.split(";")
    parsed_data = parse_message(summary=summary, **kwargs)
    message = email_msg_template(**parsed_data)
    is_send = send_mail_with_cc(
        recipient_list=recipient_list,
        cc_list=ccr_list,
        **message,
    )
    if is_send:
        logger.info(f"邮件发送成功, 收件人:{email_recipient},抄送人:{email_cc}")
    else:
        logger.error(f"邮件发送失败, 收件人:{email_recipient},抄送人:{email_cc}")
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/enums/RequestBodyEnum.py
New file
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
@File    : RequestBodyEnum.py
@Time    : 2023/9/11 16:01
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 请求数据类型
"""
# 请求类型
from enum import IntEnum
class BodyType(IntEnum):
    none = 0
    json = 1
    form = 2
    x_form = 3
    binary = 4
    graphQL = 5
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/enums/TreeTypeEnum.py
New file
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
@File    : TreeTypeEnum.py.py
@Time    : 2023/12/6 11:01
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 分组目录类型枚举
"""
from enum import IntEnum
class TreeType(IntEnum):
    API = 1
    CASE = 2
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/loader.py
New file
@@ -0,0 +1,624 @@
# -*- coding: utf-8 -*-
"""
@File    : loader.py
@Time    : 2023/1/16 15:27
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import concurrent.futures
import copy
import datetime
import importlib
import json
import logging
import os
import shutil
import sys
import time
import types
import tempfile
from ast import literal_eval
from typing import Dict, List, Tuple, Union
from concurrent.futures import ThreadPoolExecutor
from bs4 import BeautifulSoup
from django.core.exceptions import ObjectDoesNotExist
from requests.utils import dict_from_cookiejar
from requests.cookies import RequestsCookieJar
from backend.settings import BASE_DIR
from lunarlink import models
from lunarlink.utils.parser import Format
from lunarlink.views.report import ConvertRequest
from httprunner import HttpRunner
from apps.exceptions.error import (
    ApiNotFound,
    ConfigNotFound,
    CaseStepNotFound,
)
logger = logging.getLogger(__name__)
TEST_NOTE_EXISTS = {
    "code": "0102",
    "status": False,
    "msg": "节点下没有接口或用例集",
}
def is_function(tup: Tuple) -> bool:
    """
    判断元组内的对象是否为function, 真返回True, 否返回False
    :param tup:
    :return:
    """
    name, item = tup
    return isinstance(item, types.FunctionType)
def is_variable(tup: Tuple):
    """
    Takes (name, object) tuple, returns True if it is a variable.
    :param tup:
    :return:
    """
    name, item = tup
    if callable(item):
        # function or class
        return False
    if isinstance(item, types.ModuleType):
        # imported module
        return False
    if name.startswith("_"):
        # private property
        return False
    return True
class FileLoader:
    @staticmethod
    def dump_python_file(python_file, data):
        """dump python file"""
        with open(python_file, "w", encoding="utf-8") as stream:
            stream.write(data)
    @staticmethod
    def load_python_module(file_path):
        """load python module.
        :param file_path: python path
        :return:
            dict: variables and functions mapping for specified python module
                {
                    "variables": {},
                    "functions": {}
                }
        """
        debugtalk_module = {"variables": {}, "functions": {}}
        debugtalk_module_name = "debugtalk"
        # 修复切换项目后,debugtalk 有缓存
        if sys.modules.get(debugtalk_module_name):
            del sys.modules[debugtalk_module_name]
        sys.path.insert(0, file_path)
        module = importlib.import_module(debugtalk_module_name)
        # 修复重载bug
        importlib.reload(module)
        sys.path.pop(0)
        for name, item in vars(module).items():
            if is_function((name, item)):
                debugtalk_module["functions"][name] = item
            elif is_variable((name, item)):
                if isinstance(item, tuple):
                    continue
                debugtalk_module["variables"][name] = item
            else:
                pass
        return debugtalk_module
def load_debugtalk(project: int):
    """
    import debugtalk.py in sys.path and reload
    :param project:
    :return:
    """
    # debugtalk.py
    code = models.Debugtalk.objects.get(project__id=project).code
    tempfile_path = tempfile.mkdtemp(
        prefix="LunarLink", dir=os.path.join(BASE_DIR, "tempWorkDir")
    )
    file_path = os.path.join(tempfile_path, "debugtalk.py")
    os.chdir(tempfile_path)
    try:
        FileLoader.dump_python_file(file_path, code)
        debugtalk = FileLoader.load_python_module(os.path.dirname(file_path))
        return debugtalk, file_path
    except Exception as e:
        logger.error(e)
        os.chdir(BASE_DIR)  # 递归删除文件夹下的所有子文件夹和子文件
        shutil.rmtree(os.path.dirname(file_path))
def parse_tests(
    testcases: List,
    debugtalk: Dict,
    name=None,
    config=None,
    project=None,
):
    """
    get test case structure
    :param testcases:
    :param debugtalk: 驱动代码
    :param name:
    :param config: 配置文件
    :param project:
    :return:
    """
    refs = {
        "env": {},
        "def-api": {},
        "def-testcase": {},
        "debugtalk": debugtalk,
    }
    test_set = {
        "config": {"name": testcases[-1]["name"], "variables": []},
        "teststeps": testcases,
    }
    if config:
        test_set["config"] = config
    if name:
        test_set["config"]["name"] = name
    # 获取当前项目的全局变量
    global_variables = (
        models.Variables.objects.filter(project=project).all().values("key", "value")
    )
    # 并集,重复内容只保留一个
    all_config_variables_keys = set().union(
        *(d.keys() for d in test_set["config"].setdefault("variables", []))
    )
    global_variables_list_of_dict = []
    for item in global_variables:
        if item["key"] not in all_config_variables_keys:
            global_variables_list_of_dict.append({item["key"]: item["value"]})
    # 有 variables 就直接 extend,没有就加一个[], 再 extend
    # 配置的 variables 和全局变量重叠,优先使用配置中的 variables
    test_set["config"].setdefault("variables", []).extend(global_variables_list_of_dict)
    test_set["config"]["refs"] = refs
    # 配置中的变量和全局变量合并
    variables_mapping = {}
    if config:
        for variables in config["variables"]:
            variables_mapping.update(variables)
    return test_set
def debug_api(
    api: Union[Dict, List],
    project: int,
    name=None,
    config=None,
    save=True,
    user=None,
):
    """
    调式接口
    :param api:
    :param project:
    :param name:
    :param config:
    :param save:
    :param user:
    :return:
    """
    if len(api) == 0:
        return TEST_NOTE_EXISTS
    # testcase
    if isinstance(api, dict):
        """
        httprunner scripts or teststeps
        """
        api = [api]
    # 参数化参数过滤,只加载api中调用到的参数
    if config and config.get("parameters"):
        api_params = []
        for item in api:
            params = (
                item["request"].get("params", {})
                or item["request"].get("json", {})
                or item["request"].get("data", {})
            )
            for v in params.values():
                if isinstance(v, list):
                    api_params.extend(v)
                else:
                    api_params.append(v)
        parameters = []
        for value in config["parameters"]:
            for key in value.keys():
                # key可能是key-key1这种模式, 所以需要分割
                for i in key.split("-"):
                    if "$" + i in api_params:
                        parameters.append(value)
                        break
        config["parameters"] = parameters
    debugtalk = load_debugtalk(project=project)
    debugtalk_content = debugtalk[0]
    debugtalk_path = debugtalk[1]
    os.chdir(os.path.dirname(debugtalk_path))
    try:
        testcase_list = [
            parse_tests(
                testcases=api,
                debugtalk=debugtalk_content,
                name=name,
                config=config,
                project=project,
            )
        ]
        kwargs = {"failfast": False}
        runner = HttpRunner(**kwargs)
        runner.run(path_or_testcases=testcase_list)
        summary = parse_summary(summary=runner.summary)
        if save:
            # 保存报告信息
            save_summary(
                name=name,
                summary=summary,
                project=project,
                report_type=1,
                user=user,
            )
        # 复制一份 response 的 json
        for details in summary.get("details", []):
            for record in details.get("records", []):
                json_data = record["meta_data"]["response"].pop("json", {})
                if json_data:
                    record["meta_data"]["response"]["jsonCopy"] = json_data
        ConvertRequest.generate_curl(report_details=summary["details"])
        return summary
    except Exception as e:
        logger.error(f"debug_api error")
        raise SyntaxError(str(e))
    finally:
        os.chdir(BASE_DIR)
        shutil.rmtree(os.path.dirname(debugtalk_path))
def debug_suite(
    suite,
    project,
    obj,
    config=None,
    save=True,
    user=None,
    report_type=1,
    report_name="",
    allow_parallel=False,
):
    """debug suite
    :param suite: list[list[dict]], 用例列表
    :param project: int, 项目id
    :param obj: list[dict] [{"id": int "name": str}], 用例的名称和id
    :param config: list[dict], 每个用例运行的配置
    :param save:
    :param user:
    :param report_type: int, 默认类型是调试
    :param report_name:
    :param allow_parallel: bool, 是否允许并行
    :return:
    """
    if len(suite) == 0:
        return TEST_NOTE_EXISTS, 0
    debugtalk = load_debugtalk(project=project)
    debugtalk_content = debugtalk[0]
    debugtalk_path = debugtalk[1]
    os.chdir(os.path.dirname(debugtalk_path))
    # 先记录配置的名称,parse_tests会改变config
    config_name_list = [d["name"] for d in config]
    try:
        test_sets = create_test_sets(
            suite=suite,
            obj=obj,
            debugtalk_content=debugtalk_content,
            config=config,
            project=project,
        )
        if allow_parallel:
            summary = debug_suite_parallel(test_sets)
        else:
            kwargs = {"failfast": False}
            runner = HttpRunner(**kwargs)
            runner.run(test_sets)
            summary = parse_summary(runner.summary)
        # 统计用例级别的数据
        summary = update_summary(
            obj=obj,
            test_sets=test_sets,
            project=project,
            summary=summary,
            config_name_list=config_name_list,
        )
        report_id = 0
        if save:
            report_id = save_summary(
                name=report_name or f"批量运行{len(test_sets)}条用例",
                summary=summary,
                project=project,
                report_type=report_type,
                user=user,
            )
        # 复制一份response的json
        summary = process_response_json(summary)
        return summary, report_id
    except Exception as e:
        raise SyntaxError(str(e))
    finally:
        os.chdir(BASE_DIR)
        shutil.rmtree(os.path.dirname(debugtalk_path))
def create_test_sets(suite, obj, debugtalk_content, config, project):
    """根据给定的 suite, debugtalk_content, config 和 project,创建 test_sets
    :param obj:
    :param suite:
    :param debugtalk_content:
    :param config:
    :param project:
    :return:
    """
    test_sets = []
    for index in range(len(suite)):
        # copy.deepcopy 修复引用bug
        testcases = copy.deepcopy(
            parse_tests(
                testcases=suite[index],
                debugtalk=debugtalk_content,
                name=obj[index]["name"],
                config=config[index],
                project=project,
            )
        )
        test_sets.append(testcases)
    return test_sets
def update_summary(obj, test_sets, project, summary, config_name_list):
    """
    根据给定的 summary, config_name_list 和 details,更新 summary。
    """
    details: List = summary["details"]
    failure_case_config_mapping_list = []
    for index, detail in enumerate(details):
        if detail["success"] is False:
            # 用例失败时, 记录用例执行的配置
            failure_case_config = {"config_name": config_name_list[index]}
            failure_case_config.update(obj[index])
            failure_case_config_mapping_list.append(failure_case_config)
    case_count = len(test_sets)
    case_fail_rate = f"{len(failure_case_config_mapping_list) / case_count:.2%}"
    summary["stat"].update(
        {
            "failure_case_config_mapping_list": failure_case_config_mapping_list,
            "case_count": case_count,
            "case_fail_rate": case_fail_rate,
            "project": project,
        }
    )
    return summary
def process_response_json(summary):
    """
    处理 summary 中的 response json,以便在 summary 中添加 jsonCopy。
    """
    for _details in summary.get("details", []):
        for record in _details.get("records", []):
            json_data = record["meta_data"]["response"].pop("json", {})
            if json_data:
                record["meta_data"]["response"]["jsonCopy"] = json_data
    return summary
def debug_suite_parallel(test_sets: List):
    """
    并行运行用例
    :param test_sets:
    :return:
    """
    def run_test(test_set: Dict):
        kwargs = {"failfast": False}
        runner = HttpRunner(**kwargs)
        runner.run([test_set])
        return parse_summary(runner.summary)
    start = time.time()
    # 限制最多10个线程
    workers = min(len(test_sets), 10)
    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {executor.submit(run_test, t): t for t in test_sets}
        results = [
            future.result() for future in concurrent.futures.as_completed(futures)
        ]
    duration = time.time() - start
    return merge_parallel_result(results, duration)
def merge_parallel_result(results: List, duration: float):
    """
    合并并行的结果,保持和串行的运行结果一致
    :param results: 用例执行结果
    :param duration: 用例执行时间
    :return:
    """
    base_result: Dict = results.pop()
    for result in results:
        base_result["success"] = result["success"] and base_result["success"]
        for k, v in base_result["stat"].items():
            base_result["stat"][k] = v + result["stat"][k]
        for k, v in base_result["time"].items():
            if k == "start_at":
                base_result["time"][k] = min(v, result["time"][k])
            else:
                base_result["time"][k] = v + result["time"][k]
        base_result["details"].extend(result["details"])
    base_result["time"]["duration"] = duration
    # 删除多余的key
    keys = list(base_result.keys())
    for k in keys:
        if k not in ("success", "stat", "time", "platform", "details"):
            base_result.pop(k)
    return base_result
def parse_summary(summary):
    """序列化summary
    :param summary:
    :return:
    """
    for detail in summary["details"]:
        for record in detail["records"]:
            for key, value in record["meta_data"]["request"].items():
                if isinstance(value, bytes):
                    record["meta_data"]["request"][key] = value.decode("utf-8")
                if isinstance(value, RequestsCookieJar):
                    record["meta_data"]["request"][key] = dict_from_cookiejar(value)
            for key, value in record["meta_data"]["response"].items():
                if isinstance(value, bytes):
                    record["meta_data"]["response"][key] = value.decode("utf-8")
                if isinstance(value, RequestsCookieJar):
                    record["meta_data"]["response"][key] = dict_from_cookiejar(value)
            if "text/html" in record["meta_data"]["response"]["content_type"]:
                record["meta_data"]["response"]["content"] = BeautifulSoup(
                    record["meta_data"]["response"]["content"], features="html.parser"
                ).prettify()
            if record["status"] == "failure":
                record["meta_data"].update({"validators": []})
    return summary
def save_summary(name, summary, project, report_type=2, user=None, ci_metadata=None):
    """保存报告信息"""
    if ci_metadata is None:
        ci_metadata = {}
    if "status" in summary.keys():
        return
    if name == "" or name is None:
        name = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    # 需要先复制一份,不然会影响debug_api返回给前端的报告
    summary = copy.deepcopy(summary)
    summary_detail = summary.pop("details")
    report = models.Report.objects.create(
        **{
            "project": models.Project.objects.get(id=project),
            "name": name,
            "type": report_type,
            "status": summary["success"],
            "summary": json.dumps(summary, ensure_ascii=False),
            "creator_id": user,
            "ci_metadata": ci_metadata,
            "ci_project_id": ci_metadata.get("ci_project_id"),
            "ci_job_id": ci_metadata.get("ci_job_id", None),
        }
    )
    models.ReportDetail.objects.create(
        summary_detail=summary_detail,
        report=report,
    )
    return report.id
def load_test(test, project=None):
    """
    格式化测试用例
    :param test:
    :param project:
    :return:
    """
    try:
        format_http = Format(test["newBody"])
        format_http.parse()
        testcase = format_http.testcase
    except KeyError:
        if "case" in test.keys():
            if test["body"]["method"] == "config":
                try:
                    case_step = models.Config.objects.get(
                        name=test["body"]["name"], project=project
                    )
                except ObjectDoesNotExist:
                    raise ConfigNotFound("指定的配置不存在")
            else:
                try:
                    case_step = models.CaseStep.objects.get(id=test["id"])
                except ObjectDoesNotExist:
                    raise CaseStepNotFound("指定的用例步骤不存在")
        else:
            if test["body"]["method"] == "config":
                try:
                    case_step = models.Config.objects.get(
                        name=test["body"]["name"], project=project
                    )
                except ObjectDoesNotExist:
                    raise ConfigNotFound("指定的配置不存在")
            else:
                try:
                    case_step = models.API.objects.get(id=test["id"])
                except ObjectDoesNotExist:
                    raise ApiNotFound("指定的接口不存在")
        testcase = literal_eval(case_step.body)
        name = test["body"]["name"]
        if case_step.name != name:
            testcase["name"] = name
    return testcase
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py
New file
@@ -0,0 +1,364 @@
# -*- coding: utf-8 -*-
"""
@File    : message_template.py
@Time    : 2023/11/21 17:29
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 消息模板
"""
from typing import Dict
from django.conf import settings
import time
def parse_message(summary: Dict, **kwargs):
    """
    解析消息模板
    :param summary: 测试报告摘要
    :param kwargs: 其他参数
    :return:
    """
    task_name = summary["task_name"]
    rows_count = summary["stat"]["testsRun"]
    pass_count = summary["stat"]["successes"]
    fail_count = summary["stat"]["failures"]
    error_count = summary["stat"]["errors"]
    duration = "%.2fs" % summary["time"]["duration"]
    report_id = summary["report_id"]
    base_url = settings.IM_REPORT_SETTING.get("base_url")
    port = settings.IM_REPORT_SETTING.get("port")
    report_url = f"{base_url}:{port}/api/lunarlink/reports/{report_id}"
    executed = rows_count
    fail_rate = "{:.2%}".format(fail_count / executed)
    case_count = kwargs.get("case_count")
    return {
        "task_name": task_name,
        "duration": duration,
        "case_count": case_count,
        "pass_count": pass_count,
        "error_count": error_count,
        "fail_count": fail_count,
        "fail_rate": fail_rate,
        "report_url": report_url,
    }
def email_msg_template(
    task_name,
    duration,
    case_count,
    pass_count,
    error_count,
    fail_count,
    fail_rate,
    report_url,
):
    """
    定制邮件报告消息模板(高级优化版)
    :param task_name: 任务名称
    :param duration: 总耗时
    :param case_count: 用例个数
    :param pass_count: 成功接口个数
    :param error_count: 异常接口个数
    :param fail_count: 失败接口个数
    :param fail_rate: 失败比例
    :param report_url: 报告链接
    :return: 邮件主题及 HTML 内容
    """
    email_subject = f"【自动化测试报告】{task_name}"
    email_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自动化测试报告</title>
  <!-- 引入 Google 字体 -->
  <link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
  <style>
    body {{
      margin: 0;
      padding: 0;
      background-color: #f5f7fa;
      font-family: 'Roboto', sans-serif;
      color: #333;
    }}
    .container {{
      max-width: 700px;
      margin: 40px auto;
      background: #fff;
      border-radius: 12px;
      overflow: hidden;
      box-shadow: 0 8px 24px rgba(0,0,0,0.1);
    }}
    .header {{
      background: linear-gradient(135deg, #4facfe, #00f2fe);
      padding: 30px;
      text-align: center;
      color: #fff;
    }}
    .header h1 {{
      margin: 0;
      font-size: 28px;
      font-weight: 700;
    }}
    .header p {{
      margin: 8px 0 0;
      font-size: 16px;
    }}
    .content {{
      padding: 30px 40px;
    }}
    .summary-grid {{
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 20px;
      margin-top: 20px;
    }}
    .card {{
      background: #f9fafc;
      border-radius: 8px;
      padding: 20px;
      text-align: center;
      box-shadow: 0 2px 6px rgba(0,0,0,0.05);
    }}
    .card .label {{
      font-size: 14px;
      color: #777;
      margin-bottom: 8px;
      display: block;
    }}
    .card .value {{
      font-size: 24px;
      font-weight: 700;
    }}
    .value.success {{
      color: #2ecc71;
    }}
    .value.error {{
      color: #e74c3c;
    }}
    .value.warning {{
      color: #f39c12;
    }}
    .report-btn {{
      display: block;
      width: 240px;
      margin: 30px auto 0;
      text-align: center;
      padding: 15px;
      background-color: #4a90e2;
      color: #fff;
      text-decoration: none;
      border-radius: 6px;
      font-weight: 500;
      transition: background 0.3s ease;
    }}
    .report-btn:hover {{
      background-color: #3b7dd8;
    }}
    .footer {{
      text-align: center;
      font-size: 12px;
      color: #aaa;
      padding: 20px;
      background: #f0f2f5;
    }}
    @media (max-width: 768px) {{
      .container {{
        margin: 20px;
      }}
      .summary-grid {{
        grid-template-columns: 1fr 1fr;
      }}
    }}
    @media (max-width: 480px) {{
      .summary-grid {{
        grid-template-columns: 1fr;
      }}
    }}
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>自动化测试报告</h1>
      <p>{task_name}</p>
    </div>
    <div class="content">
      <div class="summary-grid">
        <div class="card">
          <span class="label">总耗时</span>
          <span class="value">{duration}</span>
        </div>
        <div class="card">
          <span class="label">用例个数</span>
          <span class="value">{case_count}</span>
        </div>
        <div class="card">
          <span class="label">失败比例</span>
          <span class="value warning">{fail_rate}</span>
        </div>
        <div class="card">
          <span class="label">成功接口</span>
          <span class="value success">{pass_count}</span>
        </div>
        <div class="card">
          <span class="label">异常接口</span>
          <span class="value error">{error_count}</span>
        </div>
        <div class="card">
          <span class="label">失败接口</span>
          <span class="value error">{fail_count}</span>
        </div>
      </div>
      <a href="{report_url}" class="report-btn">点击查看详细报告</a>
    </div>
    <div class="footer">
      本邮件由系统自动发出,请勿回复
    </div>
  </div>
</body>
</html>
"""
    return {
        "subject": email_subject,
        "html_message": email_content,
    }
def qy_msg_template(
    task_name,
    duration,
    case_count,
    pass_count,
    error_count,
    fail_count,
    fail_rate,
    report_url,
    msg_type: str = "markdown",
):
    """
    定制企业微信消息模板
    :param task_name:
    :param duration:
    :param case_count:
    :param pass_count:
    :param error_count:
    :param fail_count:
    :param fail_rate:
    :param report_url:
    :param msg_type:
    :return:
    """
    if msg_type == "markdown":
        msg_template = {"msgtype": "markdown", "markdown": {"content": ""}}
        content = f"""<font color=\'warning\'>**接口自动化测试报告**</font> \n
        >任务名称: <font color=\'comment\'>{task_name}</font>
        >总共耗时: <font color=\'comment\'>{duration}</font>
        >用例个数: <font color=\'comment\'>{case_count}</font>
        >成功接口: <font color=\'info\'>**{pass_count}**</font>
        >异常接口: <font color=\'comment\'>**{error_count}**</font>
        >失败接口: <font color=\'comment\'>**{fail_count}**</font>
        >失败比例: <font color=\'comment\'>**{fail_rate}**</font>
        >测试报告: <font color=\'comment\'>[点击查看]({report_url})</font>"""
        msg_template["markdown"]["content"] = content
        return msg_template
    text = f" 任务名称: {task_name}\n 总共耗时: {duration}\n 成功接口: {pass_count}个\n 异常接口: {error_count}个\n 失败接口: {fail_count}个\n 失败比例: {fail_rate}\n 查看详情: {report_url}"
    return text
def dd_msg_template(
    task_name,
    duration,
    case_count,
    pass_count,
    error_count,
    fail_count,
    fail_rate,
    report_url,
    msg_type: str = "markdown",
    creator=None,
    updater=None,
):
    """
    定制钉钉消息模板:包含自动评价、图标提示、markdown美观布局
    :param task_name: 任务名称
    :param duration: 总耗时
    :param case_count: 用例个数
    :param pass_count: 成功接口个数
    :param error_count: 异常接口个数
    :param fail_count: 失败接口个数
    :param fail_rate: 失败比例(字符串或百分比)
    :param report_url: 报告链接
    :param msg_type: 消息类型,默认 markdown
    :return: 钉钉消息内容(字典格式)
    """
    # 安全转换失败比例
    try:
        fr = float(str(fail_rate).strip().replace("%", "").replace("%", ""))
    except Exception:
        fr = 0.0
    # 状态图标
    status_icon = "✅" if fail_count == 0 and error_count == 0 else ("⚠️" if fail_count == 0 else "❌")
    # 自动评价等级
    if error_count == 0 and fail_count == 0:
        evaluation = "测试结果优秀"
        evaluation_icon = "🟢"
    elif fr <= 5:
        evaluation = "测试结果良好"
        evaluation_icon = "🟡"
    elif fr <= 10:
        evaluation = "测试结果一般"
        evaluation_icon = "🟠"
    else:
        evaluation = "测试结果严重异常"
        evaluation_icon = "🔴"
    if msg_type == "markdown":
        content = (
            f"## {status_icon} **接口自动化测试完成通知**\n\n"
            f"### 📌 任务名称:**{task_name}**\n"
            f"### 📈 自动评价:{evaluation_icon} `{evaluation}`\n\n"
            f"> ⏱️ **总耗时:** {duration}  \n"
            f"> 📊 **执行用例数:** {case_count}  \n"
            f"> ✅ **成功接口数:** {pass_count}  \n"
            f"> ⚠️ **异常接口数:** {error_count}  \n"
            f"> ❌ **失败接口数:** {fail_count}  \n"
            f"> 📉 **失败比例:** {fail_rate}  \n\n"
            f"> 👤 **创建人:** {creator if creator else '未记录'}  \n"  # 新增创建人显示
            f"> ✏️ **更新人:** {updater if updater else '未记录'}  \n\n"  # 新增更新人显示
            f"🔗 [👉 点击查看详细报告]({report_url})\n\n"
            f"📬 详细内容也可通过邮件查看。\n\n"
            "---\n"
            "如有异常,请及时处理!🎉"
        )
        return {"msgtype": "markdown", "markdown": {"content": content}}
    else:
        # 普通 text 消息 fallback
        content = (
            f"{status_icon} 接口自动化测试完成通知\n"
            f"任务名称:{task_name}\n"
            f"自动评价:{evaluation_icon} {evaluation}\n"
            f"总耗时:{duration}\n"
            f"执行用例数:{case_count}\n"
            f"成功接口数:{pass_count}\n"
            f"异常接口数:{error_count}\n"
            f"失败接口数:{fail_count}\n"
            f"失败比例:{fail_rate}\n"
            f"创建人:{creator}\n"
            f"更新人:{updater}\n"
            f"报告地址:{report_url}\n"
            f"详细内容请查看邮件,如有异常,请及时处理!。"
        )
        return {"msgtype": "text", "text": {"content": content}}
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/parser.py
New file
@@ -0,0 +1,978 @@
# -*- coding: utf-8 -*-
"""
@File    : parser.py
@Time    : 2023/1/14 15:10
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : API用例解析
"""
import datetime
import json
import logging
from ast import literal_eval
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Dict, Generator, List, Tuple, Union
import json5
import requests
from lunarlink import models
from lunaruser import models as user_models
from lunarlink.utils.tree import (
    get_all_ycatid,
    get_tree_max_id,
    get_tree_ycatid_mapping,
)
logger = logging.getLogger(__name__)
class Format:
    """
    解析标准HttpRunner脚本 前端->后端
    """
    def __init__(self, body: Dict, level: str = "test"):
        """初始化要解析的参数
        body => {
                    header: header -> [{key:'', value:'', desc:''},],
                    request: request -> {
                        form: formData - > [{key: '', value: '', type: 1, desc: ''},],
                        json: jsonData -> {},-
                        params: paramsData -> [{key: '', value: '', type: 1, desc: ''},]
                        files: files -> {"fields","binary"}
                    },
                    extract: extract -> [{key:'', value:'', desc:''}],
                    validate: validate -> [{expect: '', actual: '', comparator: 'equals', type: 1},],
                    variables: variables -> [{key: '', value: '', type: 1, desc: ''},],
                    hooks: hooks -> [{setup: '', teardown: ''},],
                    url: url -> string
                    method: method -> string
                    name: name -> string
                }
        """
        try:
            self.name = body.pop("name", None)
            self.__headers = body.get("header", {}).get("header", {})
            self.__variables = body.get("variables", {}).get("variables", {})
            self.__setup_hooks = body.get("hooks", {}).get("setup_hooks", {})
            self.__teardown_hooks = body.get("hooks", {}).get("teardown_hooks", {})
            if level == "test":
                # 配置移除request参数
                self.__params = (
                    body.get("request", {}).get("params", {}).get("params", {})
                )
                self.__data = body.get("request", {}).get("form", {}).get("data", {})
                self.__json = body.get("request", {}).get("json", {})
                self.__files = body.get("request", {}).get("files", {}).get("files", {})
                self.__desc = {
                    "header": body.get("header", {}).get("desc", {}),
                    "data": body.get("request", {}).get("form", {}).get("desc", {}),
                    "files": body.get("request", {}).get("files", {}).get("desc", {}),
                    "params": body.get("request", {}).get("params", {}).get("desc", {}),
                    "variables": body.get("variables", {}).get("desc", {}),
                }
                self.url = body.pop("url", None)
                self.method = body.pop("method", None)
                self.__times = body.pop("times", None)
                self.__extract = body.get("extract", {}).get("extract", {})
                self.__validate = body.pop("validate", {}).get("validate", {})
                self.__desc["extract"] = body.get("extract", {}).get("desc", {})
            else:
                self.__params = {}
                self.__data = {}
                self.__json = {}
                self.__files = {}
            if level == "config":
                self.__desc = {
                    "header": body.get("header", {}).get("desc", {}),
                    "variables": body.get("variables", {}).get("desc", {}),
                }
                self.base_url = body.pop("base_url")
                self.is_default = body.pop("is_default")
                self.__parameters = body["parameters"].pop("parameters")
                self.__desc["parameters"] = body["parameters"].pop("desc")
            self.__level = level
            self.testcase = None
            self.project = body.pop("project", None)
            self.relation = body.pop("nodeId", None)
            # lunarlink的API没有rig_id字段,需要兼容
            self.rig_id = body.get("rig_id", 0)
            self.rig_env = body.get("rig_env", 0)
        except KeyError:
            pass
    def parse(self):
        """
        返回标准化HttpRunner "desc" 字段, 执行时需去除
        :return:
        """
        if not hasattr(self, "rig_id"):
            self.rig_id = None
        if not hasattr(self, "rig_env"):
            self.rig_env = 0
        test = {}
        if self.__level == "test":
            test = {
                "name": self.name,
                "rig_id": self.rig_id,
                "times": self.__times,
                "request": {"url": self.url, "method": self.method, "verify": False},
                "desc": self.__desc,
            }
            if self.__extract:
                test["extract"] = self.__extract
            if self.__validate:
                test["validate"] = self.__validate
        elif self.__level == "config":
            test = {
                "name": self.name,
                "request": {
                    "base_url": self.base_url,
                },
                "desc": self.__desc,
            }
            if self.__parameters:
                test["parameters"] = self.__parameters
        if self.__headers:
            test["request"]["headers"] = self.__headers
        if self.__params:
            test["request"]["params"] = self.__params
        if self.__data:
            test["request"]["data"] = self.__data
        if self.__json:
            test["request"]["json"] = self.__json
        # 兼容一些接口需要传空json
        if self.__json == {}:
            test["request"]["json"] = {}
        if self.__files:
            test["request"]["files"] = self.__files
        if self.__variables:
            test["variables"] = self.__variables
        if self.__setup_hooks:
            test["setup_hooks"] = self.__setup_hooks
        if self.__teardown_hooks:
            test["teardown_hooks"] = self.__teardown_hooks
        self.testcase = test
class Parse:
    """
    标准HttpRunner脚本解析至前端 后端->前端
    """
    def __init__(self, body: Dict, level: str = "test"):
        """
        body: => {
                "name": "get token with $user_agent, $os_platform, $app_version",
                "request": {
                    "url": "/api/get-token",
                    "method": "POST",
                    "headers": {
                        "app_version": "$app_version",
                        "os_platform": "$os_platform",
                        "user_agent": "$user_agent"
                    },
                    "json": {
                        "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}"
                    },
                    "extract": [
                        {"token": "content.token"}
                    ],
                    "validate": [
                        {"eq": ["status_code", 200]},
                        {"eq": ["headers.Content-Type", "application/json"]},
                        {"eq": ["content.success", true]}
                    ],
                    "setup_hooks": [],
                    "teardown_hooks": []
                }
        """
        self.name = body.get("name")
        self.__request = body.get("request")  #
        self.__variables = body.get("variables")
        self.__setup_hooks = body.get("setup_hooks", [])
        self.__teardown_hooks = body.get("teardown_hooks", [])
        self.__desc = body.get("desc")
        if level == "test":
            self.__times = body.get("times", 1)  # 如果导入没有times 默认为1
            self.__extract = body.get("extract")
            self.__validate = body.get("validate")
        if level == "config":
            self.__parameters = body.get("parameters")
        self.__level = level
        self.testcase = None
    @staticmethod
    def __get_type(content: Any) -> Tuple:
        """返回data_type 默认string"""
        var_type = {
            "str": 1,
            "int": 2,
            "float": 3,
            "bool": 4,
            "list": 5,
            "dict": 6,
            "NoneType": 7,
        }
        key = str(type(content).__name__)
        # 黑魔法,为了兼容值是int,但又是$引用变量的情况
        if key == "str" and "$int" in content:
            return var_type["int"], content
        if key == "NoneType":
            return var_type["NoneType"], content
        if key in ["list", "dict"]:
            content = json.dumps(content, ensure_ascii=False)
        else:
            content = str(content)
        return var_type[key], content
    def parse_http(self):
        """解析成标准前端脚本格式"""
        init = [
            {
                "key": "",
                "value": "",
                "desc": "",
            }
        ]
        init_p = [
            {
                "key": "",
                "value": "",
                "desc": "",
                "type": 1,
            }
        ]
        # 初始化test结构
        test = {
            "name": self.name,
            "header": init,
            "request": {
                "data": init_p,
                "params": init_p,
                "json_data": "",
            },
            "variables": init_p,
            "hooks": [
                {
                    "setup": "",
                    "teardown": "",
                }
            ],
        }
        if self.__level == "test":
            test["times"] = self.__times
            test["method"] = self.__request["method"]
            test["url"] = self.__request["url"]
            test["validate"] = [
                {
                    "expect": "",
                    "actual": "",
                    "comparator": "equals",
                    "type": 1,
                }
            ]
            test["extract"] = init
            if self.__extract:
                test["extract"] = []
                for content in self.__extract:
                    for key, value in content.items():
                        test["extract"].append(
                            {
                                "key": key,
                                "value": value,
                                "desc": self.__desc["extract"][key],
                            }
                        )
            if self.__validate:
                test["validate"] = []
                for content in self.__validate:
                    for key, value in content.items():
                        obj = Parse.__get_type(value[1])
                        # 兼容旧的断言
                        desc = ""
                        if len(value) >= 3:
                            # value[2]为None时,设置为''
                            desc = value[2] or ""
                        test["validate"].append(
                            {
                                "expect": obj[1],
                                "actual": value[0],
                                "comparator": key,
                                "type": obj[0],
                                "desc": desc,
                            }
                        )
        elif self.__level == "config":
            test["base_url"] = self.__request["base_url"]
            test["parameters"] = init
            if self.__parameters:
                test["parameters"] = []
                for content in self.__parameters:
                    for key, value in content.items():
                        test["parameters"].append(
                            {
                                "key": key,
                                "value": Parse.__get_type(value)[1],
                                "desc": self.__desc["parameters"][key],
                            }
                        )
        if self.__request.get("headers"):
            test["header"] = []
            for key, value in self.__request.pop("headers").items():
                test["header"].append(
                    {"key": key, "value": value, "desc": self.__desc["header"][key]}
                )
        if self.__request.get("data"):
            test["request"]["data"] = []
            for key, value in self.__request.pop("data").items():
                obj = Parse.__get_type(value)
                test["request"]["data"].append(
                    {
                        "key": key,
                        "value": obj[1],
                        "type": obj[0],
                        "desc": self.__desc["data"][key],
                    }
                )
        if self.__request.get("params"):
            test["request"]["params"] = []
            for key, value in self.__request.pop("params").items():
                test["request"]["params"].append(
                    {
                        "key": key,
                        "value": value,
                        "type": 1,
                        "desc": self.__desc["params"][key],
                    }
                )
        if self.__request.get("json"):
            test["request"]["json_data"] = json.dumps(
                self.__request.pop("json"),
                indent=4,
                separators=(",", ": "),
                ensure_ascii=False,
            )
        if self.__variables:
            test["variables"] = []
            for content in self.__variables:
                for key, value in content.items():
                    obj = Parse.__get_type(value)
                    test["variables"].append(
                        {
                            "key": key,
                            "value": obj[1],
                            "desc": self.__desc["variables"][key],
                            "type": obj[0],
                        }
                    )
        if self.__setup_hooks or self.__teardown_hooks:
            test["hooks"] = []
            if len(self.__setup_hooks) > len(self.__teardown_hooks):
                for index in range(0, len(self.__setup_hooks)):
                    teardown = ""
                    if index < len(self.__teardown_hooks):
                        teardown = self.__teardown_hooks[index]
                    test["hooks"].append(
                        {
                            "setup": self.__setup_hooks[index],
                            "teardown": teardown,
                        }
                    )
            else:
                for index in range(0, len(self.__teardown_hooks)):
                    setup = ""
                    if index < len(self.__setup_hooks):
                        setup = self.__setup_hooks[index]
                    test["hooks"].append(
                        {
                            "setup": setup,
                            "teardown": self.__teardown_hooks[index],
                        }
                    )
        self.testcase = test
class Yapi:
    def __init__(
        self,
        yapi_base_url: str,
        token: str,
        faster_project_id: int,
    ):
        self.__yapi_base_url = yapi_base_url
        self.__token = token
        self.faster_project_id = faster_project_id
        self.api_info: List = []
        self.api_ids: List = []
        # self.category_info: List = []
        # api基础信息,不包含请求报文
        self.api_list_url = self.__yapi_base_url + "/api/interface/list"
        # api详情,包含详细的请求报文
        self.api_details_url = self.__yapi_base_url + "/api/interface/get"
        # api所有分组目录,也包含了api的基础信息
        self.category_info_url = self.__yapi_base_url + "/api/interface/list_menu"
    def get_category_info(self) -> Dict:
        """获取接口菜单列表
        :return:
        """
        try:
            res = requests.get(
                self.category_info_url, params={"token": self.__token}
            ).json()
        except Exception as e:
            logger.error(f"获取yapi的目录失败:{e}")
        else:
            if res["errcode"] == 0:
                return res
            else:
                return {"errcode": 1, "errmsg": "获取yapi的目录失败!", "data": []}
    def get_api_uptime_mapping(self):
        """yapi所有api的更新时间映射关系,{api_id: api_up_time}
        :return:
        """
        ""
        category_info_list = self.get_category_info()
        mapping = {}
        for category_info in category_info_list["data"]:
            category_detail = category_info.get("list", [])
            for category in category_detail:
                api_id = category["_id"]
                up_time = category["up_time"]
                mapping[api_id] = up_time
        return mapping
    def get_category_id_name_mapping(self):
        """获取yapi的分组信息映射关系,{category_id: category_name}
        :return:
        """
        try:
            res = self.get_category_info()
        except Exception as e:
            logger.error(f"获取yapi的目录失败:{e}")
        else:
            if res["errcode"] == 0:
                # {'category_id': 'category_name'}
                category_id_name_mapping = {}
                for category_info in res["data"]:
                    # 排除为空的分组
                    if category_info.get("list"):
                        category_name = category_info.get("name")
                        category_id = category_info.get("_id")
                        category_id_name_mapping[category_id] = category_name
                return category_id_name_mapping
    def get_api_info_list(self):
        """获取接口列表数据
        :return:
        """
        try:
            res = requests.get(
                self.api_list_url,
                params={
                    "token": self.__token,
                    "page": 1,
                    "limit": 100000,
                },
            ).json()
        except Exception as e:
            logger.error(f"获取api list失败: {e}")
        else:
            if res["errcode"] == 0:
                return res
    def get_api_ids(self) -> List:
        """
        获取yapi的api_ids
        :return:
        """
        api_list = self.get_api_info_list()
        return [api["id"] for api in api_list["data"]["list"]]
    def get_batch_api_detail(self, api_ids: List[int]) -> Generator[dict, None, None]:
        """
        获取yapi的所有api的详细信息
        :param api_ids:
        :return:
        """
        token = self.__token
        session = requests.Session()  # 创建一个 Session 对象
        def fetch_api_detail(api_id):
            try:
                response = session.get(
                    f"{self.api_details_url}?token={token}&id={api_id}"
                )
                response.raise_for_status()  # 如果状态码不是200,会引发HTTPError异常
                res = response.json()
                return res["data"]
            except requests.HTTPError as http_err:
                logger.error(f"HTTP error occurred: {http_err}")
            except Exception as e:
                logger.error(f"Error occurred: {e}")
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = {executor.submit(fetch_api_detail, api_id) for api_id in api_ids}
            for future in as_completed(futures):
                api_detail = future.result()
                if api_detail is not None:
                    yield api_detail  # 使用 yield 关键字,返回一个生成器
    @staticmethod
    def get_variable_default_value(
        variable_type: str, variable_value: Union[Dict, Any]
    ):
        """
        获取变量默认值
        :param variable_type:
        :param variable_value:
        :return:
        """
        if isinstance(variable_value, dict) is False:
            return ""
        variable_type = variable_type.lower()
        if variable_type in ("integer", "number", "bigdecimal"):
            return variable_value.get("default", 0)
        elif variable_type == "date":
            return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        elif variable_type == "string":
            return ""
        return ""
    def create_relation_id(self, project_id):
        """创建yapi所属目录
        :param project_id:
        :return:
        """
        category_id_name_mapping: Dict = self.get_category_id_name_mapping()
        obj = models.Relation.objects.get(project_id=project_id, type=1)
        eval_tree: List = literal_eval(obj.tree)
        yapi_catids: List = [yapi_catid for yapi_catid in get_all_ycatid(eval_tree, [])]
        if category_id_name_mapping is None:
            return
        for cat_id, cat_name in category_id_name_mapping.items():
            if cat_id not in yapi_catids:
                tree_id = get_tree_max_id(tree=eval_tree)
                base_tree_node = {
                    "id": tree_id + 1,
                    "yapi_catid": cat_id,
                    "label": cat_name,
                    "children": [],
                }
                eval_tree.append(base_tree_node)
        obj.tree = json.dumps(eval_tree, ensure_ascii=False)
        obj.save()
    def yapi2faster(self, source_api_info):
        """yapi单个api转成faster格式
        :param source_api_info:
        :return:
        """
        logger.info(f"正在处理yapi的接口id是{source_api_info.get('_id')}")
        api_info_template = {
            "header": {
                "header": {},
                "desc": {},
            },
            "request": {
                "form": {
                    "data": {},
                    "desc": {},
                },
                "json": {},
                "params": {
                    "params": {},
                    "desc": {},
                },
                "files": {
                    "files": {},
                    "desc": {},
                },
            },
            "extract": {
                "extract": [],
                "desc": {},
            },
            "validate": {
                "validate": [],
            },
            "variables": {
                "variables": [],
                "desc": {},
            },
            "hooks": {
                "setup_hooks": [],
                "teardown_hooks": [],
            },
            "url": "",
            "method": "",
            "name": "",
            "times": 1,
            "nodeId": 0,
            "project": self.faster_project_id,
        }
        default_validator = {"equals": ["status_code", 200]}
        api_info_template["validate"]["validate"].append(default_validator)
        # 限制api的名称最大长度,避免溢出
        api_info_template["name"] = source_api_info.get("title", "默认api名称")[:100]
        # path中{var}替换成$var格式
        api_info_template["url"] = (
            source_api_info.get("path", "").replace("{", "$").replace("}", "")
        )
        api_info_template["method"] = source_api_info.get("method", "GET")
        # yapi的分组id
        api_info_template["yapi_catid"] = source_api_info["catid"]
        api_info_template["yapi_id"] = source_api_info["_id"]
        # 十位时间戳
        api_info_template["yapi_add_time"] = source_api_info.get("add_time", "")
        api_info_template["yapi_up_time"] = source_api_info.get("up_time", "")
        # yapi原作者名
        api_info_template["yapi_username"] = source_api_info.get("username", "")
        req_body_type = source_api_info.get("req_body_type")
        req_body_other = source_api_info.get("req_body_other", "")
        if req_body_type == "json" and req_body_other != "":
            try:
                req_body = json.loads(req_body_other)
            except json.decoder.JSONDecodeError:
                # TODO: 解析带注释的json req_body没起作用
                req_body = json5.loads(req_body_other, encoding="utf8")
            except Exception as e:
                logger.error(
                    f"yapi: {source_api_info['_id']}, req_body json loads failed: {source_api_info.get('req_body_other', e)}"
                )
            else:
                # TODO: 递归遍历properties所有节点
                if isinstance(req_body, dict):
                    req_body_properties = req_body.get("properties")
                    if isinstance(req_body_properties, dict):
                        for field_name, field_value in req_body_properties.items():
                            if isinstance(field_value, dict) is False:
                                continue
                            any_of = field_value.get("anyOf")
                            if isinstance(any_of, list):
                                if len(any_of) > 0:
                                    field_value: dict = any_of[0]
                            field_type = field_value.get("type", "unKnow")
                            if field_type == "unKnow":
                                logger.error(
                                    f'yapi: {source_api_info["_id"]}, req_body json type is unKnow'
                                )
                            if not (field_type == "array" or field_type == "object"):
                                self.set_ordinary_variable(
                                    api_info_template=api_info_template,
                                    field_name=field_name,
                                    field_type=field_type,
                                    field_value=field_value,
                                )
                            if field_type == "array":
                                items: dict = field_value["items"]
                                # 特殊字段处理,通用的查询条件
                                if field_name == "conditions":
                                    set_customized_variable(api_info_template, items)
                                else:
                                    items_type: str = items.get("type")
                                    if items_type != "array" and items_type != "object":
                                        self.set_ordinary_variable(
                                            api_info_template=api_info_template,
                                            field_name=field_name,
                                            field_type=field_type,
                                            field_value=field_value,
                                        )
                            if field_type == "object":
                                properties: dict = field_value.get("properties")
                                if properties and isinstance(properties, dict):
                                    for (
                                        property_name,
                                        property_value,
                                    ) in properties.items():
                                        field_type = property_value["type"]
                                        if not (
                                            field_type == "array"
                                            or field_type == "object"
                                        ):
                                            self.set_ordinary_variable(
                                                api_info_template=api_info_template,
                                                field_name=property_name,
                                                field_type=field_type,
                                                field_value=property_value,
                                            )
        req_query: List = source_api_info.get("req_query", [])
        if req_query:
            for param in req_query:
                param_name = param["name"]
                param_desc = param.get("desc", "")
                param_example = param.get("example", "")
                api_info_template["request"]["params"]["params"][
                    param_name
                ] = f"${param_name}"
                api_info_template["request"]["params"]["desc"][param_name] = param_desc
                api_info_template["variables"]["variables"].append(
                    {param_name: param_example}
                )
                api_info_template["variables"]["desc"][param_name] = param_desc
        req_body_form: List = source_api_info.get("req_body_form", [])
        if req_body_form:
            for data in req_body_form:
                form_name = data.get("name")
                form_desc = data.get("desc", "")
                form_example = data.get("example", "")
                api_info_template["request"]["form"]["data"][
                    form_name
                ] = f"${form_name}"
                api_info_template["request"]["form"]["desc"][form_name] = form_desc
                api_info_template["variables"]["variables"].append(
                    {form_name: form_example}
                )
                api_info_template["variables"]["desc"][form_name] = form_desc
        req_params: List = source_api_info.get("req_params", [])
        if req_params:
            for param in req_params:
                param_name = param.get("name")
                param_desc = param.get("desc", "")
                param_example = param.get("example", "")
                api_info_template["variables"]["variables"].append(
                    {param_name: param_example}
                )
                api_info_template["variables"]["desc"][param_name] = param_desc
        return api_info_template
    def set_ordinary_variable(
        self, api_info_template, field_name, field_type, field_value
    ):
        api_info_template["request"]["json"][field_name] = f"${field_name}"
        api_info_template["variables"]["variables"].append(
            {field_name: self.get_variable_default_value(field_type, field_value)}
        )
        api_info_template["variables"]["desc"][field_name] = field_value.get(
            "description", ""
        )
    def get_parsed_apis(self, api_info) -> List:
        """
        批量创建fastapi格式的api
        :param api_info:
        :return: 返回多个 API 类的实例
        """
        apis = [
            self.yapi2faster(api) for api in api_info if isinstance(api, dict) is True
        ]
        proj = models.Project.objects.get(id=self.faster_project_id)
        obj = models.Relation.objects.get(project_id=self.faster_project_id, type=1)
        yapi_user = user_models.MyUser.objects.filter(name="yapi").first()
        yapi_user_id = yapi_user.id if yapi_user else None
        eval_tree: List = literal_eval(obj.tree)
        tree_ycatid_mapping = get_tree_ycatid_mapping(value=eval_tree)
        api_instances = []
        for api in apis:
            format_api = Format(api)
            format_api.parse()
            yapi_catid: int = api["yapi_catid"]
            api_body = {
                "name": format_api.name,
                "body": format_api.testcase,
                "url": format_api.url,
                "method": format_api.method,
                "project": proj,
                "relation": tree_ycatid_mapping.get(yapi_catid, 0),
                # 直接从yapi原来的api中获取
                "yapi_catid": yapi_catid,
                "yapi_id": api["yapi_id"],
                "yapi_add_time": api["yapi_add_time"],
                "yapi_up_time": api["yapi_up_time"],
                "yapi_username": api["yapi_username"],
                # 默认为yapi用户
                "creator_id": yapi_user_id,
            }
            api_instances.append(models.API(**api_body))
        return api_instances
    @staticmethod
    def merge_api(
        api_instances: List, apis_imported_from_yapi: List
    ) -> Tuple[List, List]:
        """
        将 yapi 获取的 api 和已导入测试平台的 api 进行合并
        两种情况:
        1. parsed_api.yapi_id不存在测试平台
        2. yapi的id已经存在测试平台,新获取的 parsed_api.yapi_up_time > imported_api.yapi_up_time
        :param api_instances: 解析后的 API 实例
        :param apis_imported_from_yapi: 原 api 信息
        :return: 返回要更新的 API 实例和要新增的 API 实例
        """
        imported_apis_mapping = {
            api.yapi_id: api.yapi_up_time for api in apis_imported_from_yapi
        }
        imported_apis_index = {
            api.yapi_id: index for index, api in enumerate(apis_imported_from_yapi)
        }
        new_api_instances = []
        update_api_instances = []
        imported_apis_ids = set(imported_apis_mapping.keys())
        for api in api_instances:
            yapi_id = api.yapi_id
            # parsed_api.yapi_id不存在测试平台
            if yapi_id not in imported_apis_ids:
                new_api_instances.append(api)
            else:
                # yapi的id已经存在测试平台
                imported_yapi_up_time = imported_apis_mapping[yapi_id]
                if api.yapi_up_time > int(imported_yapi_up_time):
                    index = imported_apis_index[yapi_id]
                    imported_api = apis_imported_from_yapi[index]
                    imported_api.method = api.method
                    imported_api.name = api.name
                    imported_api.url = api.url
                    imported_api.body = api.body
                    imported_api.yapi_up_time = api.yapi_up_time
                    update_api_instances.append(imported_api)
        return update_api_instances, new_api_instances
    def get_create_or_update_apis(self, imported_apis_mapping):
        """
        返回需要新增和更新的api_id
        imported_apis_mapping: {yapi_id: yapi_up_time}
        新增:
            yapi_id不存在测试平台imported_apis_mapping中
        更新:
            yapi_id存在测试平台imported_apis_mapping, 且up_time大于测试平台的
        :param imported_apis_mapping:
        :return:
        """
        api_uptime_mapping: Dict = self.get_api_uptime_mapping()
        create_ids = []
        update_ids = []
        for yapi_id, yapi_up_time in api_uptime_mapping.items():
            imported_yapi_up_time = imported_apis_mapping.get(yapi_id)
            if not imported_yapi_up_time:
                # 新增
                create_ids.append(yapi_id)
            elif yapi_up_time > int(imported_yapi_up_time):
                # 更新
                update_ids.append(yapi_id)
        return create_ids, update_ids
# 特殊字段conditions
def set_customized_variable(api_info_template, items):
    if items["type"] == "object":
        properties: dict = items["properties"]
        attr_name: dict = properties.get("attributeName", {})
        attribute_name_enum: list = attr_name.get("enum", [""])
        if len(attribute_name_enum) == 0:
            attribute_name_enum = [""]
        target_value: list = [f"${value}" for value in attribute_name_enum]
        # 查询条件字段默认模板
        api_info_template["request"]["json"]["conditions"] = {
            "attributeName": f"${attribute_name_enum[0]}",
            "rangeType": "$rangeType",
            "targetValue": target_value,
        }
        for attr in attribute_name_enum:
            api_info_template["variables"]["variables"].append({attr: ""})
            api_info_template["variables"]["desc"][attr] = attr_name.get(
                "description", ""
            )
        # 查询条件比较类型
        range_type: dict = properties.get("rangeType", {})
        range_type_enum: list = range_type.get("enum", [""])
        api_info_template["variables"]["variables"].append(
            {"rangeType": range_type_enum[0]}
        )
        api_info_template["variables"]["desc"][
            "rangeType"
        ] = f"条件匹配方式:{','.join(range_type_enum)}"
        # 默认排序
        api_info_template["request"]["json"]["orderBy"] = [
            {
                "attributeName": f"${attribute_name_enum[0]}",
                "rankType": "DESC",
            }
        ]
def format_json(value):
    """
    将一个JSON对象格式化为易读的字符串。
    如果输入值因为任何原因无法被序列化为JSON,就返回原始输入值。
    :param value: 需要格式化的JSON对象
    :return: 如果成功,返回格式化后的字符串;否则,返回原始输入值。
    """
    try:
        return json.dumps(value, indent=4, separators=(",", ": "), ensure_ascii=False)
    except (TypeError, OverflowError):
        return value
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/prepare.py
New file
@@ -0,0 +1,627 @@
# -*- coding: utf-8 -*-
"""
@File    : prepare.py
@Time    : 2023/1/16 17:02
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description :
"""
import logging
from ast import literal_eval
from typing import Type
import pydash
import requests
from collections import defaultdict
from typing import Dict, List, Tuple
from django.db.models import Count, Model, F, Q
from django.db.models.query import QuerySet
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from apps.exceptions.error import ApiNotFound, ConfigNotFound, CaseStepNotFound
from lunarlink import models
from lunarlink.utils.day import get_day, get_week, get_month
from lunarlink.utils.parser import Format
logger = logging.getLogger(__name__)
def project_init(project, creator):
    """新建项目初始化"""
    # 自动生成默认debugtalk.py
    models.Debugtalk.objects.create(project=project, creator=creator)
def get_sql_dateformat(date_type: str) -> str:
    create_time = ""
    if date_type == "week":
        create_time = "CAST(YEARWEEK(create_time, 1) AS CHAR)"
    elif date_type == "month":
        create_time = "DATE_FORMAT(create_time, '%%Y%%m')"
    elif date_type == "day":
        create_time = "DATE_FORMAT(create_time, '%%Y-%%m-%%d')"
    return create_time
def get_recent_date(date_type) -> List:
    """
    获取最近6天,6周,6月的日期
    :param date_type: day | week | month
    :return:
    """
    if date_type == "day":
        return [get_day(n) for n in range(-5, 1)]
    elif date_type == "week":
        return [get_week(n) for n in range(-5, 1)]
    elif date_type == "month":
        return [get_month(n) for n in range(-5, 1)]
def list2dict(arr: List) -> Dict:
    """
    将一个包含字典的列表转换为一个字典
    :param arr: [{create_time: xxx, counts: xxx}, ...]
    :return: {create_time: counts, ...}
    """
    keys = []
    values = []
    for value in arr:
        keys.append(str(value.get("create_time")))
        values.append(value.get("counts"))
    return dict(zip(keys, values))
def complete_list(arr: List[Dict], date_type: str) -> List:
    """获取最近6天,6周,6月的数量, 并返回一个包含6个元素的列表
    :param arr: [{create_time: xxx, counts: xxx}, ...]
    :param date_type: day | week | month
    :return: [1, 2, 3, 4, 5, 6]
    """
    mapping = list2dict(arr)  # {create_time: counts, ...}
    recent_six_date_list = get_recent_date(date_type)  # ['08-13', '08-14', ...]
    # [1, 2, 3, 4, 5, 6]
    count = [mapping.get(date, 0) for date in recent_six_date_list]
    return count
def get_project_apis(project_id) -> Tuple:
    """统计项目中手动创建和yapi导入的接口数量"""
    query = models.API.objects.filter(is_deleted=False).filter(~Q(tag=4))
    if project_id:
        query = query.filter(project_id=project_id)
    project_api_map: Dict = query.aggregate(
        用户创建=Count("pk", filter=~Q(creator__name="yapi")),
        yapi导入=Count("pk", filter=Q(creator__name="yapi")),
    )
    return list(project_api_map.keys()), list(project_api_map.values())
def aggregate_case_by_tag(project_id) -> Tuple:
    """按照分类统计项目中的用例"""
    query = models.Case.objects
    if project_id:
        query = query.filter(project_id=project_id)
    case_count: Dict = query.aggregate(
        冒烟用例=Count("pk", filter=Q(tag=1)),
        集成用例=Count("pk", filter=Q(tag=2)),
        监控用例=Count("pk", filter=Q(tag=3)),
        核心用例=Count("pk", filter=Q(tag=4)),
    )
    return list(case_count.keys()), list(case_count.values())
def aggregate_reports_by_type(project_id) -> Tuple:
    """按照类型统计项目中的报告"""
    query = models.Report.objects
    if project_id:
        query = query.filter(project_id=project_id)
    report_count: Dict = query.aggregate(
        测试=Count("pk", filter=Q(type=1)),
        异步=Count("pk", filter=Q(type=2)),
        定时=Count("pk", filter=Q(type=3)),
    )
    return list(report_count.keys()), list(report_count.values())
def aggregate_reports_by_status(project_id) -> Tuple[List, List]:
    """按照状态统计项目中的报告"""
    query = models.Report.objects
    if project_id:
        query = query.filter(project_id=project_id)
    report_count: Dict = query.aggregate(
        失败=Count("pk", filter=Q(status=0)),
        成功=Count("pk", filter=Q(status=1)),
    )
    return list(report_count.keys()), list(report_count.values())
def aggregate_reports_or_case_bydate(date_type: str, model) -> List:
    """按月和周统计报告创建数量"""
    create_time = get_sql_dateformat(date_type)
    qs = (
        model.objects.extra(select={"create_time": create_time})
        .values(
            "create_time",
        )
        .annotate(counts=Count("id"))
        .values("create_time", "counts")
    )
    # qs = [{"create_time": xxx, "counts": xxx}]
    # qs = list(qs)
    # 查询结果是按时间升序,取最后6条
    # 没有的补0
    values = complete_list(arr=qs, date_type=date_type)
    return values
def aggregate_apis_bydate(date_type: str, is_yapi=False) -> List:
    """按照日,周,月统计项目中手动创建和从yapi导入的接口数量
    :param date_type: day | week | month
    :param is_yapi: False: 统计手动创建的接口数量,True: 统计yapi导入的接口数量
    :return: 返回统计结果
    """
    create_time = get_sql_dateformat(date_type)
    query = models.API.objects.filter(~Q(tag=4))
    if is_yapi:
        query = query.filter(creator__name="yapi")
    else:
        query = query.filter(~Q(creator__name="yapi"))
    count_data = (
        query.extra(select={"create_time": create_time})
        .values(
            "create_time",
        )
        .annotate(counts=Count("id"))
        .values("create_time", "counts")
    )
    # 查询结果是按时间升序,取最后6条
    # 没有的补0
    count = complete_list(arr=count_data, date_type=date_type)
    return count
def aggregate_data_by_date(
    date_type: str, model: Type[Model]
) -> Tuple[List[str], Dict[str, List[int]]]:
    """
    按照日,周,月统计不同模型中的数据项数量。
    返回创建人列表和每个创建人在每个时间段内创建的数据项数量。
    :param date_type: day | week | month
    :param model: 数据模型
    :return: (创建人列表, 创建人所创建的数据项数量字典)
    """
    create_time = get_sql_dateformat(date_type)
    if model == models.API:
        query = model.objects.filter(~Q(tag=4), ~Q(creator__name="yapi"))
    else:
        query = model.objects
    count_data = (
        query.annotate(
            creator_name=F("creator__name"),
        )
        .extra(select={"create_time": create_time})
        .values("creator_name", "create_time")
        .annotate(counts=Count("id"))
        .order_by("creator_name", "create_time")
    )
    # 获取最近6个时间段
    recent_six_date_list = get_recent_date(date_type)
    # 获取创建人在每个时间段内创建的接口数量,并找出前5名
    creators, counts_dict = extract_top_creators_and_counts(
        count_data, recent_six_date_list
    )
    return creators, counts_dict
def extract_top_creators_and_counts(
    count_data: QuerySet,
    recent_six_date_list: List[str],
) -> Tuple[List[str], Dict[str, List[int]]]:
    """
    从聚合数据中提取创建人列表和创建人在每个时间段内创建的数据项数量,并找出前5名
    :param count_data: 统计数据
    :param recent_six_date_list: 最近6个时间段
    :return:
    """
    # 初始化计数字典
    counts_dict = defaultdict(lambda: [0] * len(recent_six_date_list))
    # 填充计数字典
    for item in count_data:
        creator = item["creator_name"]
        create_time = item["create_time"]
        if create_time in recent_six_date_list:
            date_index = recent_six_date_list.index(create_time)
            counts_dict[creator][date_index] += item["counts"]
    # 计算每个创建人的总数并排序
    total_counts = {creator: sum(counts) for creator, counts in counts_dict.items()}
    sorted_creators = sorted(total_counts, key=total_counts.get, reverse=True)[:5]
    # 仅保留前5名创建人的数据
    top_counts_dict = {creator: counts_dict[creator] for creator in sorted_creators}
    return sorted_creators, top_counts_dict
def get_daily_count(project_id, model_name, start, end):
    # 生成日期list, ['08-13', '08-14', ...]
    recent_days = [get_day(n)[5:] for n in range(start, end)]
    models_mapping = {"api": models.API, "case": models.Case, "report": models.Report}
    model = models_mapping[model_name]
    query = model.objects
    if model_name == "api":
        query = query.filter(~Q(tag=4))
    # 统计给定日期范围内,每天创建的条数
    count_data: List = (
        query.filter(
            project_id=project_id, create_time__range=[get_day(start), get_day(end)]
        )
        .extra(select={"create_time": "DATE_FORMAT(create_time,'%%m-%%d')"})
        .values("create_time")
        .annotate(counts=Count("id"))
        .values("create_time", "counts")
    )
    # list转dict,key是日期,value是统计数
    create_time_count_mapping = {
        data["create_time"]: data["counts"] for data in count_data
    }
    # 日期为空的key, 补0
    count = [create_time_count_mapping.get(d, 0) for d in recent_days]
    return {"days": recent_days, "count": count}
def get_project_daily_create(project_id) -> Dict:
    """项目每天创建的api, case, report"""
    start = -6
    end = 1
    count_mapping = {}
    for model in ("api", "case", "report"):
        count_mapping[model] = get_daily_count(
            project_id=project_id,
            model_name=model,
            start=start,
            end=end,
        )
    return count_mapping
def get_project_detail_v2(pk) -> Dict:
    """统计项目api, case, report总数和每日创建"""
    api_create_type, api_create_type_count = get_project_apis(project_id=pk)
    case_tag, case_tag_count = aggregate_case_by_tag(project_id=pk)
    report_type, report_type_count = aggregate_reports_by_type(project_id=pk)
    daily_create_count = get_project_daily_create(project_id=pk)
    res = {
        "api_count_by_create_type": {
            "type": api_create_type,
            "count": api_create_type_count,
        },
        "case_count_by_tag": {"tag": case_tag, "count": case_tag_count},
        "report_count_by_type": {"type": report_type, "count": report_type_count},
        "daily_create_count": daily_create_count,
    }
    return res
# TODO 后面需要替换成TAPD case
def get_jira_core_case_cover_rate(pk) -> Dict:
    """
    :param pk: 主键id
    :return:
    """
    project_obj = models.Project.objects.get(pk=pk)
    jira_cases = []
    if project_obj.jira_bearer_token == "" or project_obj.jira_project_key == "":
        logger.info("jira token或jira project key没配置")
    else:
        base_url = "https://jira.xxx.com/rest/api/latest/search"
        data = {
            "jql": f"project = {project_obj.jira_project_key} AND issuetpye = '测试用例'",
            "maxResults": -1,
        }
        headers = {
            "Authorization": f"Bearer {project_obj.jira_bearer_token}",
            "Content-Type": "application/json",
        }
        try:
            # TODO 分页查找所有的核心case
            res = requests.post(url=base_url, headers=headers, json=data).json()
            err = res.get("errorMessage")
            if err:
                logger.error(err)
            else:
                jira_cases.extend(res["issues"])
        except Exception as e:
            logger.error(str(e))
    jira_core_case_count = 0
    for case in jira_cases:
        if pydash.get(case, "fields.customfield_11400.value") == "是":
            jira_core_case_count += 1
    covered_case_count = len(models.Case.objects.filter(project=pk, tag=4))
    if jira_core_case_count == 0:
        core_case_cover_rate = "0.00"
    else:
        core_case_cover_rate = "%.2f" % (
            (covered_case_count / jira_core_case_count) * 100
        )
    return {
        "jira_core_case_count": jira_core_case_count,
        "core_case_count": covered_case_count,
        "core_case_cover_rate": core_case_cover_rate,
    }
def tree_end(params, project):
    """
    :param params: {
        node: int,
        type: int
    }
    :param project: Project Model
    :return:
    """
    tree_type = params["type"]
    node = params["node"]
    if tree_type == 1:
        models.API.objects.filter(relation=node, project=project).delete()
    # remove node testcase
    elif tree_type == 2:
        case = models.Case.objects.filter(relation=node, project=project).values("id")
        for case_id in case:
            models.CaseStep.objects.filter(case__id=case_id["id"]).delete()
            models.Case.objects.filter(id=case_id["id"]).delete()
def generate_record_casestep(
    api_ids: List,
    body: List,
    case_obj,
    config_obj,
    user,
    is_generate_api: bool = False,
):
    """
    生成录制测试用例步骤
    :param api_ids: 录制的api id
    :param is_generate_api:
    :param body: 测试用例步骤,包含配置和接口
    :param case_obj: 用例模型对象
    :param config_obj: 配置模型对象
    :param user: 当前用户
    :param is_generate_api: 是否同步生成api
    :return:
    """
    case_steps: List = []  # index 为测试用例步骤的执行顺序
    for index, item in enumerate(body):
        if (
            item["body"].get("method") == "config"
            or item["body"].get("request") == "config"
        ):
            name = config_obj.name
            url = config_obj.base_url
            method = "config"
            new_body = literal_eval(config_obj.body)
            source_api_id = 0  # 如果是配置默认为0
        else:
            name = item["body"]["name"]
            url = item["body"]["request"]["url"]
            method = item["body"]["request"]["method"]
            new_body = item["body"]
            # 不同步生成api,设置为-1
            source_api_id = api_ids[index - 1] if is_generate_api else -1
        kwargs = {
            "name": name,  # api接口名称或配置名称
            "body": new_body,  # api接口或配置body
            "url": url,
            "method": method,
            "step": index,
            "case": case_obj,
            "source_api_id": source_api_id,
            "creator": user,
        }
        case_step = models.CaseStep(**kwargs)
        case_steps.append(case_step)
    models.CaseStep.objects.bulk_create(objs=case_steps)
def generate_casestep(
    step_body_list: List,
    case_obj,
    creator,
):
    """
    生成用例集步骤
    :param step_body_list: 测试用例步骤,包含配置和接口
    :param case_obj: 用例模型对象
    :param creator: 当前用户
    :return:
    """
    case_steps: List = []  # index 为测试用例步骤的执行顺序
    source_api_id = 0
    for index, item in enumerate(step_body_list):
        try:
            # 进入case step 修改保存后会生成newBody字段
            format_http = Format(item["newBody"])
            format_http.parse()
            name = format_http.name
            new_body = format_http.testcase
            url = format_http.url
            method = format_http.method
        except KeyError:
            if (
                item["body"].get("method") == "config"
                or item["body"].get("request") == "config"
            ):
                name = item["body"]["name"]
                method = item["body"]["method"]
                try:
                    config = models.Config.objects.get(
                        name=name, project=case_obj.project
                    )
                except ObjectDoesNotExist:
                    raise ConfigNotFound("指定的配置不存在")
                url = config.base_url
                new_body = literal_eval(config.body)
                source_api_id = 0  # config没有api, 默认为0
            else:
                name = item["body"]["name"]
                url = item["body"]["url"]
                method = item["body"]["method"]
                try:
                    api = models.API.objects.get(id=item["id"])
                except ObjectDoesNotExist:
                    raise ApiNotFound("指定的接口不存在")
                new_body = literal_eval(api.body)
                if api.name != name:
                    new_body["name"] = name
                source_api_id = item["id"]
        kwargs = {
            "name": name,  # api接口名称或配置名称
            "body": new_body,  # api接口或配置body
            "url": url,
            "method": method,
            "step": index,
            "case": case_obj,
            "source_api_id": source_api_id,
            "creator": creator,
        }
        case_step = models.CaseStep(**kwargs)
        case_steps.append(case_step)
    models.CaseStep.objects.bulk_create(objs=case_steps)
def update_casestep(
    step_body_list: List,
    case_obj,
    creator,
    updater,
):
    """
    更新测试用例步骤
    :param step_body_list: 测试用例步骤,包含配置和接口
    :param case_obj: 用例模型对象
    :param creator: 当前用户
    :param updater: 当前用户
    :return:
    """
    step_list = list(models.CaseStep.objects.filter(case=case_obj).values("id"))
    for index, item in enumerate(step_body_list):
        try:
            # 进入case step 修改保存后会生成newBody字段
            format_http = Format(item["newBody"])
            format_http.parse()
            name = format_http.name
            new_body = format_http.testcase
            url = format_http.url
            method = format_http.method
        except KeyError:
            if "case" in item.keys():
                try:
                    case_step = models.CaseStep.objects.get(id=item["id"])
                except ObjectDoesNotExist:
                    raise CaseStepNotFound("指定的用例步骤不存在")
            elif item["body"]["method"] == "config":
                try:
                    case_step = models.Config.objects.get(
                        name=item["body"]["name"], project_id=case_obj.project_id
                    )
                except ObjectDoesNotExist:
                    raise ConfigNotFound("指定的配置不存在")
            else:
                try:
                    case_step = models.API.objects.get(id=item["id"])
                except ObjectDoesNotExist:
                    raise ApiNotFound("指定的接口不存在")
            new_body = literal_eval(case_step.body)
            name = item["body"]["name"]
            if case_step.name != name:
                new_body["name"] = name
            if item["body"]["method"] == "config":
                url = ""
                method = "config"
                source_api_id = 0  # config没有source_api_id, 默认为0
            else:
                url = item["body"]["url"]
                method = item["body"]["method"]
                source_api_id = item.get("source_api_id", 0)
                # 新增的case_step没有source_api_id字段,需要重新赋值
                if source_api_id == 0:
                    source_api_id = item["id"]
        else:
            if item.get("source_api_id", 0) == 0:
                source_api_id = item["id"]
            else:
                source_api_id = item["source_api_id"]
        kwargs = {
            "name": name,
            "body": new_body,
            "url": url,
            "method": method,
            "step": index,
            "source_api_id": source_api_id,
        }
        # is_copy 为 True表示用例步骤是复制的
        if "case" in item.keys() and item.pop("is_copy", False) is False:
            models.CaseStep.objects.filter(id=item["id"]).update(
                **kwargs, updater=updater
            )
            step_list.remove({"id": item["id"]})
        else:
            kwargs["case"] = case_obj
            models.CaseStep.objects.create(**kwargs, creator=creator)
    # 删除多余的step,获取需要删除的所有CaseStep id
    step_id_list = [content["id"] for content in step_list]
    # 使用一次查询进行更新
    models.CaseStep.objects.filter(id__in=step_id_list).update(
        is_deleted=True, update_time=timezone.now(), updater=updater
    )
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/query_filters.py
New file
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
@File    : filter_by_time_range.py
@Time    : 2023/12/25 09:49
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from ast import literal_eval
from datetime import datetime, timedelta
from typing import Optional
from django.db.models import QuerySet
from apps.exceptions.error import RelationNotFound
from lunarlink.models import Relation
from lunarlink.utils.tree import find_all_children_ids
def filter_by_time_range(
    queryset: QuerySet,
    start_time: Optional[str],
    end_time: Optional[str],
) -> QuerySet:
    """
    根据提供的开始时间和结束时间过滤查询集
    :param queryset: 要过滤的原始查询集
    :param start_time: 过滤记录的开始时间(字符串格式,例如 'YYYY-MM-DD')
    :param end_time: 过滤记录的结束时间(字符串格式,例如 'YYYY-MM-DD'
    :return QuerySet: 过滤后的查询集
    """
    if start_time and end_time:
        end_time = (
            datetime.strptime(end_time, "%Y-%m-%d")
            + timedelta(days=1)
            - timedelta(seconds=1)
        )
        return queryset.filter(create_time__range=[start_time, end_time])
    elif start_time:
        return queryset.filter(create_time__gte=start_time)
    elif end_time:
        end_time = (
            datetime.strptime(end_time, "%Y-%m-%d")
            + timedelta(days=1)
            - timedelta(seconds=1)
        )
        return queryset.filter(create_time__lte=end_time)
    return queryset
def filter_by_node(
    queryset: QuerySet,
    project: int,
    node: int,
    tree_type: int,
) -> QuerySet:
    """
    根据提供的节点过滤查询集
    :param queryset: 要过滤的原始查询集
    :param project: 项目id
    :param node: 目录id
    :param tree_type: 树的类型(例如 TreeType.CASE 或 TreeType.API)
    :return QuerySet: 更新后的查询集,根据指定的节点进行了过滤
    """
    if node is not None:
        try:
            tree_obj = Relation.objects.get(project=project, type=tree_type)
        except Relation.DoesNotExist:
            raise RelationNotFound("指定的目录不存在")
        tree = literal_eval(tree_obj.tree) if tree_obj else []
        node_ids = find_all_children_ids(tree, node)
        node_ids.append(node)
        return queryset.filter(relation__in=node_ids)
    return queryset
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/qy_message.py
New file
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
@File    : qy_message.py
@Time    : 2023/3/13 14:04
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 企微机器人发送消息
"""
import logging
import json
from typing import Dict, Union
import requests
from django.conf import settings
from lunarlink.utils.message_template import parse_message, qy_msg_template
logger = logging.getLogger(__name__)
def send_message(summary: Union[Dict, str], webhook: str, **kwargs):
    """
    发送企业微信消息
    :param summary:
    :param webhook:
    :param kwargs:
    :return:
    """
    header = {"Content-Type": "application/json"}
    webhooks = webhook.split("\n")  # 多个机器人,换行分隔
    for webhook in webhooks:
        parsed_data = parse_message(summary=summary, **kwargs)
        message = qy_msg_template(**parsed_data)
        res = requests.post(
            url=webhook, headers=header, data=json.dumps(message).encode("utf-8")
        ).json()
        if res.get("errcode") == 0:
            logger.info(f"发送通知成功,请求的webhook是: {webhook}")
        else:
            logger.error(f"发送通知失败,请求的webhook是: {webhook}, 响应是:{res}")
def send(msg: Dict, mentioned_list=None, mentioned_mobile_list=None):
    """ """
    if mentioned_mobile_list is None:
        mentioned_mobile_list = []
    if mentioned_list is None:
        mentioned_list = []
    webhook = settings.QY_WEB_HOOK
    header = {"Content-Type": "application/json"}
    content = f"""<font color=\'info\'>**LunarLink平台预警**</font> \n
    >url: <font color=\'comment\'>{msg.get("url")}</font>
    >msg: <font color=\'comment\'>{msg.get("msg")}</font>
    >traceback: <font color=\'warning\'>{msg.get("traceback")}</font>"""
    data = {
        "msgtype": "markdown",
        "markdown": {
            "content": content,
            "mentioned_list": mentioned_list,
            "mentioned_mobile_list": mentioned_mobile_list,
        },
    }
    res = requests.post(url=webhook, headers=header, json=data).json()
    if res.get("errcode") == 0:
        logger.info(f"发送通知成功,请求的webhook是: {webhook}")
    else:
        logger.error(f"发送通知失败,请求的webhook是: {webhook}, 响应是:{res}")
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/9/11 15:46
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/convertor.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : convertor.py
@Time    : 2023/9/11 15:47
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/generator.py
New file
@@ -0,0 +1,917 @@
# -*- coding: utf-8 -*-
"""
@File    : generator.py
@Time    : 2023/9/11 15:47
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 根据RequestInfo生成测试用例
"""
import logging
import json
import re
from collections import defaultdict
from enum import IntEnum
from typing import Any, Dict, List, Tuple, Union, Set
from urllib.parse import urlparse, parse_qs
from apps.exceptions.convert import GenerateError
from apps.schema.request import RequestInfo
from apps.schema.api_schema import APIBody, APISchema
from apps.schema.testcase_schema import RecordCaseSchema, APIBodySchema
from lunarlink.models import API
from lunarlink.utils.enums.RequestBodyEnum import BodyType
from lunarlink.utils.parser import Format
logger = logging.getLogger(__name__)
class CaseTag(IntEnum):
    INTEGRATION_CASE = 2
# 忽略的字段
ignored_headers = {
    "content-type",
    "connection",
    "date",
    "content-length",
    "host",
    "access-control-allow-credentials",
    "access-control-allow-origin",
    "user-agent",
    "server",
}
class CaseGenerator:
    """
    根据RequestInfo数组生成测试用例
    """
    @staticmethod
    def ignore(key: str):
        """
        检查指定的HTTP头部字段名称是否在预定义的忽略列表中。
        :param key: 忽略字段 headers.Origin
        :return: 如果字段应被忽略,则返回True;否则返回False。
        """
        for ig in ignored_headers:
            if key.lower().endswith(ig):
                return True
        return False
    @staticmethod
    def get_body_type(headers: Dict):
        headers = {k.lower(): v for k, v in headers.items()}
        content_type = headers.get("content-type", "").lower()
        if "json" in content_type:
            return BodyType.json
        if "x-www-form" in content_type:
            return BodyType.x_form
        if "form" in content_type:
            return BodyType.form
        return BodyType.none
    @staticmethod
    def extract_variables(input_string: str):
        """
        使用正则表达式提取 ${http_res_INDEX_variable::path} 格式的变量、索引和路径
        :param input_string:
        :return: 返回http_res_INDEX, http_res_INDEX_variable, path
        """
        if isinstance(input_string, str):
            matches = re.findall(r"\$http_res_(\d+)_(.*?)::(.+)", input_string)
        else:
            matches = []
        if matches:
            idx, var_name, path = matches[0]
            full_variable = "http_res_" + idx + "_" + var_name
            return "http_res_" + idx, full_variable, path
        return None
    @staticmethod
    def generate_case(
        length: int,
        project_id: int,
        case_dir: int,
        api_dir: int,
        config: Dict,
        case_name: str,
        requests: List[RequestInfo],
        user: int,
    ) -> Tuple[RecordCaseSchema, List]:
        """
        根据录制接口生成测试用例
        :param api_dir: 接口目录
        :param length:
        :param project_id:
        :param case_dir: 用例目录
        :param config:
        :param case_name:
        :param requests:
        :param user: 当前用户
        :return:
        """
        record_case_body = [config]
        api_info_map = {}
        # 先将 requests 转成 faster 格式 api
        for r in range(len(requests)):
            api_info = APIBodySchema()  # 初始化 faster 格式的 api 模板
            api_name = f"http_res_{r + 1}"
            api_info.name = api_name
            api_info.method = requests[r].request_method
            # 请求头
            headers = requests[r].request_headers
            CaseGenerator.merge_headers(headers, api_info)
            # 请求参数为: form、json
            requests_body = requests[r].body
            if requests_body:
                CaseGenerator.merge_body(headers, api_info, requests_body)
            # 请求参数为: params
            CaseGenerator.merge_params(api_info, requests[r].url)
            api_info_map.update({api_name: api_info.dict(by_alias=True)})  # 保持别名
        # 遍历所有接口,将 htt_res_1_token 的值 content.token 加入 extract
        for api_name, request_data in api_info_map.items():
            for headers_key, headers_value in request_data["header"]["header"].items():
                CaseGenerator.append_extract(
                    request_data, api_info_map, headers_key, headers_value, "headers"
                )
            for k, v in request_data["request"]["form"]["data"].items():
                CaseGenerator.append_extract(request_data, api_info_map, k, v, "form")
            for k, v in request_data["request"]["json"].items():
                CaseGenerator.append_extract(request_data, api_info_map, k, v, "json")
            for k, v in request_data["request"]["params"]["params"].items():
                CaseGenerator.append_extract(request_data, api_info_map, k, v, "params")
        api_instances = []
        for _, v in api_info_map.items():
            api = Format(v)
            api.parse()
            if api_dir:
                api_body = APIBody(**api.testcase)
                api_schema = APISchema(
                    name=api.name,
                    body=api_body,
                    url=api.url,
                    method=api.method,
                    project_id=project_id,
                    relation=api_dir,
                    creator_id=user,
                ).dict(by_alias=True)
                api_instances.append(API(**api_schema))
            record_case_body.append({"body": api.testcase})
        return (
            RecordCaseSchema(
                length=length,
                project_id=project_id,
                relation=case_dir,
                name=case_name,
                tag=CaseTag.INTEGRATION_CASE.value,
                body=record_case_body,
            ),
            api_instances,
        )
    @staticmethod
    def merge_headers(headers: Dict, api_info: APIBodySchema):
        """
        将录制接口中的headers格式化后加入fastapi的headers中
        :param headers: 录制接口中的headers
        :param api_info: fastapi接口数据
        :return:
        """
        api_info.header.header.update(headers)
        api_info.header.desc.update({key: "" for key in headers.keys()})
    @staticmethod
    def merge_body(headers: Dict, api_info: APIBodySchema, body: str):
        """
        将录制接口中的body格式化后加入fastapi的body中
        :param headers: 录制接口中的headers
        :param api_info: fastapi接口数据
        :param body: 接口请求body
        :return:
        """
        try:
            body = json.loads(body)
        except json.JSONDecodeError:
            pass
        except Exception as e:
            logger.error(f"转换body变量失败: {e}")
        else:
            body_type = CaseGenerator.get_body_type(headers)
            if body_type == BodyType.x_form:
                api_info.request.form.data.update(body)
                api_info.request.form.desc.update({key: "" for key in body.keys()})
            elif body_type == BodyType.json:
                api_info.request.json_data.update(body)
    @staticmethod
    def merge_params(api_info: APIBodySchema, url: str):
        """
        将录制接口中的 query 参数提取后加入fastapi的params中
        :param api_info: fastapi接口数据
        :param url: 接口请求url
        :return:
        """
        parsed_url = urlparse(url)
        api_info.url = parsed_url.path
        query_params = parse_qs(parsed_url.query)
        params = {k: v[0] for k, v in query_params.items()}
        api_info.request.params.params.update(params)
        api_info.request.params.desc.update({key: "" for key in params.keys()})
    @staticmethod
    def append_extract(
        request_data: Dict, api_info_map: Dict, item_key, item_value, request_type: str
    ):
        """
        提取引用变量,将接口中引用此变量的请求数据替换为变量值
        :param request_data: 接口请求数据, 保存格式为{"header": {...}, "request": {...}}
        :param api_info_map: 全部接口的请求信息和接口名的映射,保存格式为{"api_name": {"header": {...}, "request": {...}}
        :param item_key: 键名
        :param item_value: 键值
        :param request_type: 消息请求类型(headers, json, form, params)
        :return:
        """
        variables = CaseGenerator.extract_variables(item_value)
        if variables:
            api_name = variables[0]
            extract_var_name = variables[1]
            extract_var_path = variables[2]
            if request_type == "headers":
                CaseGenerator.replace_faster_headers(request_data, item_key, variables)
            elif request_type == "form":
                CaseGenerator.replace_faster_form(request_data, item_key, variables)
            elif request_type == "json":
                CaseGenerator.replace_faster_json(request_data, item_key, variables)
            elif request_type == "params":
                CaseGenerator.replace_faster_params(request_data, item_key, variables)
            if api_name in api_info_map:
                if {extract_var_name: extract_var_path} not in api_info_map[api_name][
                    "extract"
                ]["extract"]:
                    api_info_map[api_name]["extract"]["extract"].append(
                        {extract_var_name: extract_var_path}
                    )
                    api_info_map[api_name]["extract"]["desc"].update(
                        {extract_var_name: ""}
                    )
    @staticmethod
    def replace_faster_form(request_data: Dict, var_name: str, variables: Tuple):
        """
        替换fastapi格式请求form参数
        :param request_data:
        :param var_name:
        :param variables:
        :return:
        """
        request_data["request"]["form"]["data"].update({var_name: f"${variables[1]}"})
    @staticmethod
    def replace_faster_json(request_data: Dict, var_name: str, variables: Tuple):
        """
        替换fastapi格式请求json参数
        :param request_data:
        :param var_name:
        :param variables:
        :return:
        """
        request_data["request"]["json"].update({var_name: f"${variables[1]}"})
    @staticmethod
    def replace_faster_params(request_data: Dict, var_name: str, variables: Tuple):
        """
        替换fastapi格式请求params查询参数
        :param request_data:
        :param var_name:
        :param variables:
        :return:
        """
        request_data["request"]["params"]["params"].update(
            {var_name: f"${variables[1]}"}
        )
    @staticmethod
    def replace_faster_headers(request_data: Dict, var_name: str, variables: Tuple):
        request_data["header"]["header"].update({var_name: f"${variables[1]}"})
    @staticmethod
    def extract_field(requests: List[RequestInfo]) -> List[str]:
        """遍历接口,提取其中的变量并替换
        :param requests: 录制流量接口信息
        :return:
        """
        value_to_path_map: Dict = defaultdict(list)  # 变量值到路径的映射 {变量值: [变量路径, ...], ...}
        replaced = []  # 替换记录
        for i, item in enumerate(requests):
            if "Content-Length" in item.request_headers:
                item.request_headers.pop("Content-Length")
            if "Content-Type" in item.response_headers:
                item.response_headers.pop("Content-Type")
            # 记录变量
            CaseGenerator.record_vars(
                request=item,
                value_to_path_map=value_to_path_map,
                var_name=f"http_res_{i + 1}",
            )
            if i > 0:
                # 接口变量替换
                CaseGenerator.replace_vars(item, value_to_path_map, replaced)
        return replaced
    @staticmethod
    def replace_vars(request: RequestInfo, value_to_path_map: Dict, replaced: List):
        """
        替换变量
        :param request: 录制流量接口
        :param value_to_path_map: 记录变量值, {变量值: [变量路径, ...], ...}
        :param replaced: 替换记录
        :return:
        """
        # 提取响应内容和响应头部中的所有值
        response_values = CaseGenerator.extract_response_values(request)
        CaseGenerator.replace_url(request, value_to_path_map, replaced, response_values)
        CaseGenerator.replace_headers(
            request,
            value_to_path_map,
            replaced,
            response_values,
        )
        CaseGenerator.replace_body(
            request,
            value_to_path_map,
            replaced,
            response_values,
        )
    @staticmethod
    def extract_response_values(request: RequestInfo) -> Set:
        """
        提取响应内容和响应头部中的所有值。
        :param request: 录制流量接口
        :return: 响应值集合
        """
        response_values = set()
        # 假设响应内容是 JSON 格式的字符串,提取所有值
        if request.response_content:
            try:
                response_data = json.loads(request.response_content)
                response_values.update(
                    CaseGenerator.get_values_from_json(response_data)
                )
            except json.JSONDecodeError:
                pass
        # 添加响应头部中的所有值,除了忽略的头部键
        for key, value in request.response_headers.items():
            if key.lower() not in ignored_headers:
                response_values.add(value)
        return response_values
    @staticmethod
    def get_values_from_json(data: Union[Dict, List]) -> Set:
        """
        从 JSON 数据中递归提取所有值。
        :param data: JSON 数据,可以是字典或列表
        :return: 包含所有值的集合
        """
        values = set()
        if isinstance(data, dict):
            for value in data.values():
                values.update(CaseGenerator.get_values_from_json(value))
        elif isinstance(data, list):
            for item in data:
                values.update(CaseGenerator.get_values_from_json(item))
        else:
            values.add(data)
        return values
    @staticmethod
    def parse_url(url: str):
        """
        解析完整的URL,提取协议、域名路径和查询参数。
        :param url: 要解析的完整URL字符串。
        :return: 一个包含协议、域名路径和查询参数列表的元组。
        """
        url_parts = url.split("?")
        base_url = url_parts[0]
        query_params = [param for param in url_parts[1].split("&") if param] if len(url_parts) > 1 else []
        protocol, path_with_domain = base_url.split("//")
        return protocol, path_with_domain, query_params
    @staticmethod
    def replace_path_segments(
        path_with_domain: str,
        value_to_path_map: Dict,
        replaced: List,
        response_values: Set,
    ):
        """
        替换URL路径中的各个段落。
        :param replaced:
        :param path_with_domain: 域名和路径组合的字符串。
        :param value_to_path_map: 一个映射字典,用于替换路径中的变量。
        :param replaced: 记录替换详情的列表
        :param response_values: request_headers和request_content响应值集合
        :return: 替换后的路径段落列表。
        """
        new_url = []
        domain_and_path_list = path_with_domain.split("/")
        for segment in domain_and_path_list:
            # 检查当前字段是否是自身响应中的值,如果是则跳过
            if segment.lower() in value_to_path_map and segment not in response_values:
                new_segment = value_to_path_map[segment.lower()][0]
                new_url.append(new_segment)
                replaced.append(f"{segment} => ${new_segment}")
            else:
                new_url.append(segment)
        return new_url
    @staticmethod
    def replace_query_parameters(
        query_params: List[str],
        value_to_path_map: Dict,
        replaced: List,
        response_values: Set,
    ):
        """
        替换查询参数中的值
        :param query_params: 查询参数列表。
        :param value_to_path_map: 一个映射字典,用于替换查询参数的值。
        :param replaced: 替换后的路径段落列表。
        :return: 替换后的查询参数字符串列表。
        :param response_values: request_headers和request_content响应值集合
        """
        new_query = []
        for param in query_params:
            param_name, param_value = param.split("=")
            # 检查当前查询参数值是否是自身响应中的值,如果是则跳过
            if (
                param_value.lower() in value_to_path_map
                and param_value not in response_values
            ):
                new_param_value = value_to_path_map[param_value.lower()][0]
                new_query.append(f"{param_name}=${new_param_value}")
                replaced.append(f"{param_value} => ${new_param_value}")
            else:
                new_query.append(param)
        return new_query
    @staticmethod
    def reconstruct_url(protocol: str, new_url: List[str], new_query: List[str]):
        """
        重构URL,将协议、替换后的路径和查询参数组合成完整的URL。
        :param protocol: URL的协议部分,如 http 或 https。
        :param new_url: 经过替换的路径段落列表。
        :param new_query: 经过替换的查询参数字符串列表。
        :return: 重构后的完整URL字符串。
        """
        return f"{protocol}//{'/'.join(new_url)}{'?' + '&'.join(new_query) if new_query else ''}"
    @staticmethod
    def replace_url(
        request: RequestInfo,
        value_to_path_map: Dict,
        replaced: List,
        response_values: Set,
    ):
        """
        替换请求对象中的URL路径和查询参数。
        :param request: 包含原始URL的请求对象。
        :param value_to_path_map: 一个映射字典,用于替换URL的路径和查询参数中的值。
        :param replaced: 记录替换详情的列表。
        :param response_values: request_headers和request_content响应值集合
        :return: None。函数直接修改传入的请求对象中的URL属性。
        """
        protocol, path_with_domain, query_params = CaseGenerator.parse_url(request.url)
        new_url = CaseGenerator.replace_path_segments(
            path_with_domain,
            value_to_path_map,
            replaced,
            response_values,
        )
        new_query = CaseGenerator.replace_query_parameters(
            query_params,
            value_to_path_map,
            replaced,
            response_values,
        )
        request.url = CaseGenerator.reconstruct_url(protocol, new_url, new_query)
    @staticmethod
    def replace_headers(
        request: RequestInfo,
        value_to_path_map: Dict,
        replaced: List,
        response_values: Set,
    ):
        """
        替换请求接口中的request_headers。
        :param request: 包含原始request_headers的请求对象。
        :param value_to_path_map: 一个映射字典,用于替换request_headers的值。
        :param replaced: 记录替换详情的列表。
        :param response_values: 从响应内容和头部中提取的所有值的集合。
        :return: None。函数直接修改传入的请求对象中的request_headers属性。
        """
        for header_key, header_value in list(request.request_headers.items()):
            if header_value not in response_values:
                replacement = value_to_path_map.get(header_value)
                if replacement:
                    new_header_value = replacement[0]
                    request.request_headers[header_key] = f"${new_header_value}"
                    replaced.append(f"{header_key} => ${new_header_value}")
    @staticmethod
    def replace_body(
        request: RequestInfo,
        value_to_path_map: Dict,
        replaced: List,
        response_values: Set,
    ):
        """
        替换请求接口中的body。
        :param request: 包含原始body的请求对象。
        :param value_to_path_map: 一个映射字典,用于替换body的值。,{变量值: [变量路径, ...],...}
        :param replaced: 记录替换详情的列表。
        :param response_values: 从响应内容和头部中提取的所有值的集合。
        :return: None。函数直接修改传入的请求对象中的body属性。
        """
        if request.body:
            try:
                request_body = json.loads(request.body)
                replaced_non_str_variables = []  # 非字符串变量替换路径
                CaseGenerator.dfs_replace(
                    request_body,
                    value_to_path_map,
                    replaced_non_str_variables,
                    replaced,
                    response_values,
                )
                result = json.dumps(request_body, ensure_ascii=False)
                for v in replaced_non_str_variables:
                    result = result.replace(f"'{v}'", f"{v}")
                request.body = result
            except json.JSONDecodeError:
                pass
            except Exception as e:
                logger.error(f"转换body变量失败: {e}")
    @staticmethod
    def _dfs_replace_dict(
        request_dict: Dict,
        value_to_path_map: Dict,
        replaced_non_str_variables: List,
        replaced: List,
        response_values: Set,
    ) -> None:
        """
        递归地替换字典中的变量。
        :param request_dict: 要处理的请求体字典。
        :param value_to_path_map: 存储变量替换信息的字典。
        :param replaced_non_str_variables: 存储被替换的非字符串变量。
        :param replaced: 存储所有替换操作的列表。
        :param response_values: 从响应内容和头部中提取的所有值的集。
        :return:
        """
        for key, value in request_dict.items():
            is_str, new_value = CaseGenerator.dfs_replace(
                value,
                value_to_path_map,
                replaced_non_str_variables,
                replaced,
                response_values,
            )
            # 如果得到的替换值不是None,并且这个值不在响应值中,进行替换
            if new_value is not None and new_value not in response_values:
                request_dict[key] = f"${new_value}"
                if not is_str:
                    replaced_non_str_variables.append(f"${new_value}")
    @staticmethod
    def _dfs_replace_list(
        request_list: List,
        value_to_path_map: Dict,
        replaced_non_str_variables: List,
        replaced: List,
        response_values: Set,
    ) -> None:
        """
        递归地替换列表中的变量。
        :param request_list: 要处理的请求体列表。
        :param value_to_path_map: 存储变量替换信息的字典。
        :param replaced_non_str_variables: 存储被替换的非字符串变量。
        :param replaced: 存储所有替换操作的列表。
        :param response_values: 响应体中的值,这些值不应被替换。
        :return:
        """
        for i, item in enumerate(request_list):
            is_str, new_value = CaseGenerator.dfs_replace(
                item,
                value_to_path_map,
                replaced_non_str_variables,
                replaced,
                response_values,
            )
            if new_value is not None and new_value not in response_values:
                request_list[i] = f"${new_value}"
                if not is_str:
                    replaced_non_str_variables.append(f"${new_value}")
    @staticmethod
    def _dfs_replace_value(value, value_to_path_map, replaced, response_values):
        """
        替换基本数据类型的值。
        :param value: 要替换的值。
        :param value_to_path_map: 存储变量替换信息的字典。
        :param replaced: 存储所有替换操作的列表。
        :param response_values: 响应体中的值,这些值不应被替换。
        :return: 替换结果和是否为字符串的标志。
        """
        # 将非字符串类型的值转换为字符串以进行比较
        value_str = str(value) if not isinstance(value, str) else value
        # 如果这个值在映射中并且不在响应值中,使用映射中的新值进行替换
        if value_str in value_to_path_map and value_str not in response_values:
            new_value = value_to_path_map[value_str][0]
            replaced.append(f"{value_str} => ${new_value}")
            # 返回替换状态和新值,非字符串类型的变量也记录在replaced_non_str_variables中
            return not isinstance(value, str), new_value
        return None, None
    @staticmethod
    def dfs_replace(
        request_body: Any,
        value_to_path_map: Dict,
        replaced_non_str_variables: List,
        replaced: List,
        response_values: Set,
    ):
        """
        对请求体进行深度优先遍历,替换其中的变量。
        :param request_body: 请求体,可能是字典、列表或基本数据类型。
        :param value_to_path_map: 存储变量替换信息的字典。
        :param replaced_non_str_variables: 存储被替换的非字符串变量。
        :param replaced: 存储所有替换操作的列表。
        :param response_values: 从响应内容和头部中提取的所有值的集。
        :return:
        """
        # 处理字典类型的请求体
        if isinstance(request_body, dict):
            CaseGenerator._dfs_replace_dict(
                request_body,
                value_to_path_map,
                replaced_non_str_variables,
                replaced,
                response_values,
            )
        # 处理列表类型的请求体
        elif isinstance(request_body, list):
            CaseGenerator._dfs_replace_list(
                request_body,
                value_to_path_map,
                replaced_non_str_variables,
                replaced,
                response_values,
            )
        # 处理基本数据类型的请求体
        else:
            return CaseGenerator._dfs_replace_value(
                request_body,
                value_to_path_map,
                replaced,
                response_values,
            )
    @staticmethod
    def record_vars(request: RequestInfo, value_to_path_map: Dict, var_name: str):
        """
        记录变量
        :param request: 录制流量的接口信息
        :param value_to_path_map: 变量值到路径的映射 {变量值: [变量路径, ...], ...}
        :param var_name: http_res_{i + 1}
        :return:
        """
        CaseGenerator.split_headers(
            request,
            value_to_path_map,
            var_name,
        )
        CaseGenerator.split_body(
            request,
            value_to_path_map,
            var_name,
        )
    @staticmethod
    def split_headers(
        request: RequestInfo,
        value_to_path_map: Dict,
        var_name: str = "",
        header_path_prefix: str = "headers",
    ):
        """
        分析和记录请求头中的变量及其对应的路径。
        :param request: 包含响应头的请求信息对象。
        :param value_to_path_map: 一个映射,将变量值映射到其在请求中的路径。
        :param var_name: 用于变量提取的基本名称。
        :param header_path_prefix: 用于变量路径的前缀。
        :return:
        """
        try:
            CaseGenerator.dfs(
                request.response_headers,
                var_name,
                header_path_prefix,
                value_to_path_map,
                headers=True,
            )
        except Exception as e:
            raise GenerateError(f"解析接口headers变量出错:{e}")
    @staticmethod
    def split_body(
        request: RequestInfo,
        value_to_path_map: Dict,
        var_name: str = "",
        content_path_prefix: str = "content",
    ):
        """
        分析和记录请求体中的变量及其对应的路径。
        :param request: 包含响应内容的请求信息对象。
        :param value_to_path_map: 一个映射,将变量值映射到其在请求中的路径。
        :param var_name: 用于变量提取的基本名称。
        :param content_path_prefix: 用于变量路径的前缀。
        :return:
        """
        if request.body:
            try:
                body = json.loads(request.response_content)
                CaseGenerator.dfs(
                    body=body,
                    var_name=var_name,
                    var_path=content_path_prefix,
                    value_to_path_map=value_to_path_map,
                )
            except json.JSONDecodeError:
                # body不是JSON,跳过
                pass
            except Exception as e:
                raise GenerateError(f"解析接口body变量出错:{e}")
    @staticmethod
    def dfs_dict(
        body_dict,
        var_name,
        var_path,
        value_to_path_map,
        headers,
    ):
        """
        递归地遍历响应的头部或内容,提取并记录变量及其路径。
        :param body_dict:
        :param var_name:
        :param var_path:
        :param value_to_path_map:
        :param headers:
        :return:
        """
        for key, value in body_dict.items():
            c_name = f"{var_name}_{key}"
            c_path = f"{var_path}.{key}"
            CaseGenerator.dfs(value, c_name, c_path, value_to_path_map, headers)
    @staticmethod
    def dfs_list(body_list, var_name, var_path, value_to_path_map, headers):
        """
        递归地遍历响应的头部或内容,提取并记录变量及其路径。
        :param body_list:
        :param var_name:
        :param var_path:
        :param value_to_path_map:
        :param headers:
        :return:
        """
        for i, item in enumerate(body_list):
            c_name = f"{var_name}_{i}"
            c_path = f"{var_path}.{i}"
            CaseGenerator.dfs(item, c_name, c_path, value_to_path_map, headers)
    @staticmethod
    def process_basic_type(
        value,
        var_name,
        var_path,
        value_to_path_map,
        headers,
    ):
        """
        处理基本数据类型的值。
        :param value:
        :param var_name:
        :param var_path:
        :param value_to_path_map:
        :param headers:
        :return:
        """
        if not headers or not CaseGenerator.ignore(var_path):
            var_name_path = f"{var_name}::{var_path}"
            # 如果是bool值,需要特殊处理一下,因为Python get False/True会变成get 0 1
            if value is not None:
                if isinstance(value, bool):
                    value_to_path_map[str(value)].append(var_name_path)
                else:
                    value_to_path_map[value].append(var_name_path)
    @staticmethod
    def dfs(
        body: Any,
        var_name: str,
        var_path: str,
        value_to_path_map: Dict,
        headers: bool = False,
    ):
        """
        递归地遍历响应的头部或内容,提取并记录变量及其路径。
        :param body: 可能是响应头或响应内容的部分,可以是字典或列表。
        :param var_name: 当前变量的名称。
        :param var_path: 当前变量的提取路径。
        :param value_to_path_map: 变量到路径的映射。
        :param headers: 指示当前处理的是否是响应头。
        :return:
        """
        if isinstance(body, list):
            CaseGenerator.dfs_list(
                body,
                var_name,
                var_path,
                value_to_path_map,
                headers,
            )
        elif isinstance(body, dict):
            CaseGenerator.dfs_dict(
                body,
                var_name,
                var_path,
                value_to_path_map,
                headers,
            )
        else:
            CaseGenerator.process_basic_type(
                body,
                var_name,
                var_path,
                value_to_path_map,
                headers,
            )
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/request/har_convertor.py
New file
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
@File    : har_convertor.py
@Time    : 2023/9/11 15:48
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : request转化器,支持har
"""
from typing import List
from apps.schema.request import RequestInfo
class Convertor:
    @staticmethod
    def convert(file, regex: str = None) -> List[RequestInfo]:
        raise NotImplementedError
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/response.py
New file
@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
"""
@File    : response.py
@Time    : 2023/1/14 16:37
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 响应错误码
"""
from typing import Generic, TypeVar
from pydantic import BaseModel
from pydantic.generics import GenericModel
class ErrorMsg(BaseModel):
    code: str = "0001"
    msg: str = "成功"
    success: bool = True
GenericResultsType = TypeVar("GenericResultsType")
class StandResponse(ErrorMsg, GenericModel, Generic[GenericResultsType]):
    data: GenericResultsType
RECORD_START_SUCCESS = {
    "code": "0001",
    "success": True,
    "msg": "开始录制,可以在浏览器/app上操作啦!",
}
RECORD_IS_RUNNING = {"code": "0005", "success": False, "msg": "此IP正在录制中"}
RECORD_STOP_SUCCESS = {
    "code": "0002",
    "success": True,
    "msg": "停止成功,快去生成用例吧~",
}
RECORD_REMOVE_SUCCESS = {"code": "0003", "success": True, "msg": "删除成功"}
RECORD_DATA_ERROR = {"code": "0101", "success": False, "msg": "无http请求,请检查参数"}
API_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "接口添加成功"}
API_GET_SUCCESS = {"code": "0012", "success": True, "msg": "获取数据成功"}
API_NOT_FOUND = {"code": "0102", "success": False, "msg": "未查询到该接口"}
API_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "接口修改成功"}
API_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "接口删除成功"}
CASE_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "用例添加成功"}
CASE_GENERATOR_SUCCESS = {
    "code": "0001",
    "success": True,
    "msg": "用例生成成功, 快去列表查看吧~",
}
CASE_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "用例修改成功"}
TAG_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "用例标记成功"}
CASE_EXISTS = {
    "code": "0101",
    "success": False,
    "msg": "此节点下已存在该用例, 请重新命名",
}
CASE_NOT_EXISTS = {"code": "0102", "success": False, "msg": "此用例不存在"}
CASE_IS_USED = {
    "code": "0104",
    "success": False,
    "msg": "用例被定时任务使用中, 无法删除",
}
CASE_SPILT_SUCCESS = {"code": "0001", "success": True, "msg": "用例切割成功"}
CASE_DELETE_SUCCESS = {"code": "0003", "success": True, "msg": "用例删除成功"}
CONFIG_EXISTS = {"code": "0101", "success": False, "msg": "此配置名已存在, 请重新命名"}
CONFIG_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "配置添加成功"}
CONFIG_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的配置不存在"}
CONFIG_MISSING = {"code": "0103", "success": False, "msg": "缺少配置文件"}
CONFIG_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "配置修改成功"}
CONFIG_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "配置删除成功"}
CONFIG_IS_USED = {
    "code": "0104",
    "success": False,
    "msg": "配置文件被用例使用中, 无法删除",
}
DEBUGTALK_NOT_EXISTS = {"code": "0102", "success": False, "msg": "debugtalk不存在"}
DEBUGTALK_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "debugtalk更新成功"}
DATA_TO_LONG = {"code": "0100", "success": False, "msg": "数据信息过长"}
REPORT_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "报告删除成功"}
REPORT_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的报告不存在"}
TREE_GET_SUCCESS = {"code": "0001", "success": True, "msg": "目录获取成功"}
TREE_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "目录更新成功"}
TREE_NOT_EXISTS = {"code": "0102", "success": False, "msg": "目录不存在"}
PROJECT_EXISTS = {"code": "0101", "success": False, "msg": "项目名称已存在, 请重新命名"}
PROJECT_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "项目添加成功"}
SYSTEM_ERROR = {"code": "9999", "success": False, "msg": "System Error"}
KEY_MISS = {"code": "0100", "success": False, "msg": "请求数据非法"}
PROJECT_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "项目修改成功"}
PROJECT_DELETE_SUCCESS = {"code": "0003", "success": True, "msg": "项目删除成功"}
PROJECT_NOT_EXISTS = {"code": "0102", "success": False, "msg": "项目不存在"}
CASE_STEP_SYNC_SUCCESS = {"code": "0002", "success": True, "msg": "用例步骤同步成功"}
CASE_STEP_NOT_EXIST = {"code": "0102", "success": False, "msg": "指定的用例步骤不存在"}
TASK_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "任务新增成功"}
TASK_ADD_FAILURE = {"code": "0101", "success": False, "msg": "任务新增失败"}
TASK_COPY_FAILURE = {
    "code": "0103",
    "success": False,
    "msg": "任务复制失败, 任务名已存在",
}
TASK_COPY_SUCCESS = {"code": "0003", "success": True, "msg": "任务复制成功"}
TASK_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "任务删除成功"}
TASK_RUN_SUCCESS = {
    "code": "0001",
    "success": True,
    "msg": "用例运行中, 请稍后查看报告",
}
TASK_TIME_ILLEGAL = {"code": "0101", "success": False, "msg": "时间表达式非法"}
TASK_HAS_EXISTS = {
    "code": "0102",
    "success": False,
    "msg": "无法添加, 该任务名称已被使用",
}
TASK_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的任务不存在"}
TASK_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "任务修改成功"}
TASK_UPDATE_FAILURE = {"code": "0102", "success": False, "msg": "任务修改失败"}
TASK_DUPLICATE_NAME = {"code": "0104", "success": False, "msg": "相同任务名已存在"}
TASK_CI_PROJECT_IDS_EXIST = {
    "code": "0103",
    "success": False,
    "msg": "Gitlab项目id已存在其他项目",
}
VARIABLES_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "全局变量添加成功"}
VARIABLES_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "全局变量删除成功"}
VARIABLES_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "全局变量修改成功"}
VARIABLES_EXISTS = {"code": "0101", "success": False, "msg": "此变量已存在, 请重新命名"}
VARIABLES_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的全局变量不存在"}
YAPI_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "导入YAPI接口添加成功"}
IMPORT_YAPI = {
    "code": "0001",
    "success": True,
    "msg": "如果是首次导入,可能时间稍长,请耐心等待~",
}
YAPI_ADD_FAILED = {"code": "0103", "success": False, "msg": "导入YAPI接口失败"}
YAPI_NOT_NEED_CREATE_OR_UPDATE = {
    "code": "0002",
    "success": True,
    "msg": "没有需要新增和更新的接口",
}
PERMISSION_DENIED = {"code": "0403", "success": False, "msg": "权限不足"}
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/runner.py
New file
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
"""
@File    : runner.py
@Time    : 2023/1/16 15:24
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 在线调试Python代码
"""
import shutil
import sys
import os
import subprocess
import tempfile
from lunarlink.utils import loader
from backend.settings import BASE_DIR
EXEC = sys.executable
if "uwsgi" in EXEC:
    # 修复虚拟环境下,用uwsgi执行时,PYTHONPATH还是用系统默认的
    EXEC = EXEC.replace("uwsgi", "python")
class DebugCode:
    """调试Python代码"""
    def __init__(self, code):
        self.__code = code
        self.resp = None
        self.temp = tempfile.mkdtemp(prefix="LunarLink")
    def run(self):
        """
        dumps debugtalk.py and run
        :return:
        """
        try:
            os.chdir(self.temp)
            file_path = os.path.join(self.temp, "debugtalk.py")
            # 将code写入debugtalk.py
            loader.FileLoader.dump_python_file(file_path, self.__code)
            # 修复驱动代码运行时,找不到配置httprunner包
            run_path = [BASE_DIR]
            run_path.extend(sys.path)
            env = {"PYTHONPATH": ":".join(run_path)}
            self.resp = decode(
                subprocess.check_output(
                    [EXEC, file_path], stderr=subprocess.STDOUT, timeout=60, env=env
                )
            )
        except subprocess.CalledProcessError as e:
            self.resp = decode(e.output)
        except subprocess.TimeoutExpired:
            self.resp = "RunnerTimeOut"
        os.chdir(BASE_DIR)
        shutil.rmtree(self.temp)
def decode(s: bytes) -> str:
    """
    将字节串转换成字符串
    :return:
    """
    try:
        return s.decode("utf-8")
    except UnicodeDecodeError:
        return s.decode("gbk")
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/task.py
New file
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
"""
@File    : task.py
@Time    : 2023/3/9 10:57
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import json
import logging
from django_celery_beat import models as celery_models
from apps.exceptions.error import TaskNotFound
from lunarlink.utils.parser import format_json
logger = logging.getLogger(__name__)
class Task:
    """
    定时任务操作
    """
    def __init__(self, **body):
        """
        数据初始化
        :param body: 请求体数据
        """
        logger.info(f"before process task data:\n {format_json(body)}")
        self.__name = body["name"]
        self.__data = body["data"]
        self.__crontab = body["crontab"]
        self.__switch = body["switch"]
        self.__task = "lunarlink.tasks.schedule_debug_suite"
        self.__project = body["project"]
        self.__email = {
            "strategy": body["strategy"],
            "mail_cc": body.get("mail_cc"),
            "receiver": body.get("receiver"),
            "crontab": self.__crontab,
            "project": self.__project,
            "task_name": self.__name,
            "webhook": body.get("webhook"),
            "updater": body.get("updater"),
            "creator": body.get("creator"),
            "ci_project_ids": body.get("ci_project_ids", []),
            "ci_env": body.get("ci_env", "请选择"),
            "is_parallel": body.get("is_parallel", False),
            "config": body.get("config", "请选择"),
        }
        self.__crontab_time = None
    def format_crontab(self):
        """
        格式化时间
        """
        cron_fields = self.__crontab.split(" ")
        self.__crontab_time = {
            "day_of_week": cron_fields[4],
            "month_of_year": cron_fields[3],
            "day_of_month": cron_fields[2],
            "hour": cron_fields[1],
            "minute": cron_fields[0],
        }
    def add_task(self):
        """
        add tasks
        """
        self.format_crontab()
        crontab = celery_models.CrontabSchedule.objects.filter(
            **self.__crontab_time
        ).first()
        if crontab is None:
            crontab = celery_models.CrontabSchedule.objects.create(
                **self.__crontab_time
            )
        celery_models.PeriodicTask.objects.create(
            name=f"{self.__project}_{self.__name}",  # 兼容定时任务名称必须唯一
            task=self.__task,
            args=json.dumps(self.__data, ensure_ascii=False),
            kwargs=json.dumps(self.__email, ensure_ascii=False),
            enabled=self.__switch,
            description=self.__project,
            crontab=crontab,
        )
    def update_task(self, task_id):
        """
        update task
        :param task_id:
        :return:
        """
        self.format_crontab()
        try:
            task_obj = celery_models.PeriodicTask.objects.get(id=task_id)
        except celery_models.PeriodicTask.DoesNotExist:
            raise TaskNotFound(f"task {task_id} not found")
        crontab = celery_models.CrontabSchedule.objects.filter(
            **self.__crontab_time
        ).first()
        if crontab is None:
            crontab = celery_models.CrontabSchedule.objects.create(
                **self.__crontab_time
            )
        task_obj.name = f"{self.__project}_{self.__name}"
        task_obj.crontab = crontab
        task_obj.enabled = self.__switch
        task_obj.args = json.dumps(self.__data, ensure_ascii=False)
        task_obj.kwargs = json.dumps(self.__email, ensure_ascii=False)
        task_obj.save()
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/tree.py
New file
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
"""
@File    : tree.py
@Time    : 2023/1/14 16:46
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import collections
from typing import Dict, List
# 默认分组id=1
label_id = 1
def get_tree_label(value, search_label):
    """
    根据分组名成查找分组id,默认为1
    :param value:
    :param search_label:
    :return:
    """
    global label_id
    if not value:
        return label_id
    if isinstance(value, list):
        for content in value:  # content -> dict
            if content["label"] == search_label:
                label_id = content[id]
            children = content.get("children")
            if children:
                get_tree_label(children, search_label)
    return label_id
def get_all_ycatid(value, list_id: List = None) -> List:
    """
    获取所有yapi的分组目录id
    :param value:
    :param list_id:
    :return:
    """
    if not value:
        return []  # the first node id
    if isinstance(value, list):
        for content in value:  # content -> dict
            yapi_catid = content.get("yapi_catid")
            if yapi_catid:
                list_id.append(yapi_catid)
            children = content.get("children")
            if children:
                get_all_ycatid(children)
    return list_id
def get_tree_ycatid_mapping(value, mapping: Dict = {}) -> Dict:
    """
    获取yapi分组id和faster api分组id的映射关系
    :param value:
    :param mapping: {"yapi_catid": "node_id"}
    :return:
    """
    if not value:
        return {}
    if isinstance(value, list):
        for content in value:  # content -> dict
            yapi_catid = content.get("yapi_catid")
            if yapi_catid:
                mapping.update({yapi_catid: content.get("id")})
            children = content.get("children")
            if children:
                get_tree_ycatid_mapping(children, mapping)
    return mapping
label = ""
def get_tree_relation_name(value, relation_id):
    """
    根据节点的id查找出节点的名字
    :param value:
    :param relation_id:
    :return:
    """
    global label
    if not value:
        return label
    if isinstance(value, list):
        for content in value:  # content -> dict
            if content["id"] == relation_id:
                label = content["label"]
            children = content.get("children")
            if children:
                get_tree_relation_name(children, relation_id)
    return label
def get_tree_max_id(tree: List) -> int:
    """
    广度优先遍历树,得到最大Tree max id
    :param tree:
    :return:
    """
    queue = collections.deque()
    queue.append(tree)
    max_id = 0
    while len(queue) != 0:
        sub_tree: List = queue.popleft()
        for node in sub_tree:
            children: List = node.get("children")
            max_id = max(max_id, node["id"])
            # 有子节点
            if len(children) > 0:
                queue.append(children)
    return max_id
def find_all_children_ids(tree: List, target_id: int) -> List:
    """
    从树形结构中查找目标节点的所有子节点 ID
    :param tree: 树形结构
    :param target_id: 目标节点 ID
    :return:
    """
    queue = tree.copy()
    while queue:
        node = queue.pop(0)
        if node["id"] == target_id:
            # 找到目标节点,开始收集其所有子节点的 ID
            child_ids = []
            children_queue = node.get("children", []).copy()
            while children_queue:
                child = children_queue.pop(0)
                child_ids.append(child["id"])
                children_queue.extend(child.get("children", []))
            return child_ids
        queue.extend(node.get("children", []))
    return []
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views.py
New file
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py
@Time    : 2023/1/14 15:40
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/api.py
New file
@@ -0,0 +1,444 @@
# -*- coding: utf-8 -*-
"""
@File    : api.py
@Time    : 2023/2/1 16:57
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : API视图
"""
from enum import IntEnum
from typing import List
from ast import literal_eval
from django.core.exceptions import ObjectDoesNotExist
from django.db import DataError
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.utils import timezone
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework import status
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from apps.exceptions.error import RelationNotFound
from lunarlink import models, serializers
from lunarlink.utils import response
from lunarlink.utils.decorator import request_log
from lunarlink.utils.query_filters import filter_by_time_range, filter_by_node
from lunarlink.utils.parser import Format, Parse
from lunarlink.utils.enums.TreeTypeEnum import TreeType
class APITag(IntEnum):
    DEPRECATED = 4  # 已废弃
class APITemplateView(GenericViewSet):
    """
    API操作视图
    """
    serializer_class = serializers.APISerializer
    queryset = models.API.objects.filter(~Q(tag=APITag.DEPRECATED.value))
    @swagger_auto_schema(query_serializer=serializers.AssertSerializer())
    @method_decorator(request_log(level="DEBUG"))
    def list(self, request):
        """
        api-获取api列表
        支持多种条件搜索
        """
        ser = serializers.AssertSerializer(data=request.query_params)
        if ser.is_valid():
            node = ser.validated_data.get("node")
            project = ser.validated_data.get("project")
            search: str = ser.validated_data.get("search")
            tag = ser.validated_data.get("tag")
            rig_env = ser.validated_data.get("rigEnv")
            showYAPI = ser.validated_data.get("showYAPI")
            creator = ser.validated_data.get("creator")
            start_time = ser.validated_data.get("start_time")
            end_time = ser.validated_data.get("end_time")
            queryset = (
                self.get_queryset().filter(project=project).order_by("-create_time")
            )
            # 根据创建时间过滤
            queryset = filter_by_time_range(queryset, start_time, end_time)
            if creator:
                queryset = queryset.filter(creator__name=creator)
            if showYAPI is False:
                queryset = queryset.filter(~Q(creator__name="yapi"))
            if search != "":
                search: List = search.split()
                for key in search:
                    queryset = queryset.filter(
                        Q(name__contains=key) | Q(url__contains=key)
                    )
            try:
                queryset = filter_by_node(queryset, project, node, TreeType.API.value)
            except RelationNotFound:
                return Response(response.TREE_NOT_EXISTS)
            if tag != "":
                queryset = queryset.filter(tag=tag)
            if rig_env != "":
                queryset = queryset.filter(rig_env=rig_env)
            pagination_queryset = self.paginate_queryset(queryset)
            serializer = self.get_serializer(pagination_queryset, many=True)
            paginated_response = self.get_paginated_response(serializer.data)
            response_data = {
                **response.API_GET_SUCCESS,
                "data": paginated_response.data,
            }
            return Response(response_data)
        else:
            return Response(ser.errors, status=status.HTTP_400_BAD_REQUEST)
    @method_decorator(request_log(level="INFO"))
    def add(self, request):
        """
        api-新增一个api
        前端按照格式组装好,注意body
        """
        api = Format(request.data)
        api.parse()
        api_body = {
            "name": api.name,
            "body": api.testcase,
            "url": api.url,
            "method": api.method,
            "project": models.Project.objects.get(id=api.project),
            "relation": api.relation,
            "creator": request.user,
        }
        try:
            models.API.objects.create(**api_body)
        except DataError:
            return Response(response.DATA_TO_LONG)
        return Response(response.API_ADD_SUCCESS)
    @swagger_auto_schema(
        request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={
                "header": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "header": openapi.Schema(
                            type=openapi.TYPE_OBJECT, description="Header 对象"
                        ),
                        "desc": openapi.Schema(
                            type=openapi.TYPE_OBJECT, description="Header 描述"
                        ),
                    },
                ),
                "request": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "form": openapi.Schema(
                            type=openapi.TYPE_OBJECT,
                            properties={
                                "data": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Form data 对象"
                                ),
                                "desc": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Form data 描述"
                                ),
                            },
                        ),
                        "json": openapi.Schema(
                            type=openapi.TYPE_OBJECT, description="JSON 对象"
                        ),
                        "params": openapi.Schema(
                            type=openapi.TYPE_OBJECT,
                            properties={
                                "params": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Params 对象"
                                ),
                                "desc": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Params 描述"
                                ),
                            },
                        ),
                        "files": openapi.Schema(
                            type=openapi.TYPE_OBJECT,
                            properties={
                                "files": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Files 对象"
                                ),
                                "desc": openapi.Schema(
                                    type=openapi.TYPE_OBJECT, description="Files 描述"
                                ),
                            },
                        ),
                    },
                ),
                "extract": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "extract": openapi.Schema(
                            type=openapi.TYPE_ARRAY,
                            items=openapi.Schema(type=openapi.TYPE_OBJECT),
                            description="Extract 数组",
                        ),
                        "desc": openapi.Schema(
                            type=openapi.TYPE_OBJECT, description="Extract 描述"
                        ),
                    },
                ),
                "validate": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "validate": openapi.Schema(
                            type=openapi.TYPE_ARRAY,
                            items=openapi.Schema(type=openapi.TYPE_OBJECT),
                            description="Validate 数组",
                        ),
                    },
                ),
                "variables": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "variables": openapi.Schema(
                            type=openapi.TYPE_ARRAY,
                            items=openapi.Schema(type=openapi.TYPE_OBJECT),
                            description="Variables 数组",
                        ),
                        "desc": openapi.Schema(
                            type=openapi.TYPE_OBJECT, description="Variables 描述"
                        ),
                    },
                ),
                "hooks": openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        "setup_hooks": openapi.Schema(
                            type=openapi.TYPE_ARRAY,
                            items=openapi.Schema(type=openapi.TYPE_STRING),
                            description="Setup hooks 数组",
                        ),
                        "teardown_hooks": openapi.Schema(
                            type=openapi.TYPE_ARRAY,
                            items=openapi.Schema(type=openapi.TYPE_STRING),
                            description="Teardown hooks 数组",
                        ),
                    },
                ),
                "url": openapi.Schema(type=openapi.TYPE_STRING, description="URL"),
                "method": openapi.Schema(type=openapi.TYPE_STRING, description="请求方法"),
                "name": openapi.Schema(type=openapi.TYPE_STRING, description="名称"),
                "times": openapi.Schema(type=openapi.TYPE_INTEGER, description="次数"),
                # 添加其他自定义字段
            },
        )
    )
    @method_decorator(request_log(level="INFO"))
    def update(self, request, pk):
        """
        api-更新单个api
        更新单个api的内容
        """
        api = Format(request.data)
        api.parse()
        api_body = {
            "name": api.name,
            "body": api.testcase,
            "url": api.url,
            "method": api.method,
            "updater": request.user.id,
            "update_time": timezone.now(),
        }
        objs = models.API.objects.filter(id=pk)
        if objs:
            objs.update(**api_body)
        else:
            return Response(response.API_NOT_FOUND)
        return Response(response.API_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def move(self, request):
        """
        api-批量更新api的目录
        移动api到指定目录
        """
        project: int = request.data.get("project")
        relation: int = request.data.get("relation")
        apis: List = request.data.get("api")
        ids = [api["id"] for api in apis]
        objs = models.API.objects.filter(project=project, id__in=ids)
        if objs:
            objs.update(
                relation=relation,
                updater=request.user.id,
                update_time=timezone.now(),
            )
        else:
            return Response(response.API_NOT_FOUND)
        return Response(response.API_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def copy(self, request, pk):
        """
        api-复制api
        复制一个api
        """
        name = request.data.get("name")
        try:
            api = models.API.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.API_NOT_FOUND)
        body = literal_eval(api.body)
        body["name"] = name
        api.body = body
        api.id = None
        api.name = name
        api.creator = request.user
        api.updater = request.user.id
        api.save()
        return Response(response.API_ADD_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def destroy(self, request, pk):
        """
        api-删除单个api
        pk: api id
        """
        obj = models.API.objects.filter(id=pk)
        if not obj:
            return Response(response.API_NOT_FOUND)
        obj.update(
            is_deleted=True,
            updater=request.user.id,
            update_time=timezone.now(),
        )
        return Response(response.API_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def bulk_destroy(self, request):
        """
        批量删除api
        [{id:int}]
        """
        ids = [content["id"] for content in request.data]
        objs = models.API.objects.filter(Q(id__in=ids) & Q(is_deleted=False))
        if not objs:
            return Response(response.API_NOT_FOUND)
        objs.update(
            is_deleted=True,
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.API_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def add_tag(self, request):
        """
        api-更新api的tag, 暂时默认为调式成功, 接口文档参数与实际传参不一致需优化
        更新api的tag类型
        """
        api_ids: List = request.data.get("api_ids", [])
        if api_ids:
            updated_rows = models.API.objects.filter(pk__in=api_ids).update(
                tag=request.data.get("tag"),
                update_time=timezone.now(),
                updater=request.user.id,
            )
            if updated_rows > 0:
                return Response(response.API_UPDATE_SUCCESS)
        return Response(response.API_NOT_FOUND)
    @method_decorator(request_log(level="INFO"))
    def sync_case(self, request, pk):
        """
        api-同步api到case_step
        1.根据api_id查出("name", "body", "url", "method")
        2.根据api_id更新case_step中的("name", "body", "url", "method", "updater")
        3.更新case的update_time, updater
        """
        source_api = (
            models.API.objects.filter(pk=pk)
            .values("name", "body", "url", "method", "project")
            .first()
        )
        # 根据api反向查出project
        project = source_api.pop("project")
        project_case_ids = models.Case.objects.filter(project=project).values_list(
            "id", flat=True
        )
        # 限制case_step只在当前项目
        case_steps = models.CaseStep.objects.filter(
            source_api_id=pk, case_id__in=project_case_ids
        )
        case_steps.update(
            **source_api,
            updater=request.user.id,
            update_time=timezone.now(),
        )
        case_ids = case_steps.values_list("case", flat=True)
        # 限制case只在当前项目
        models.Case.objects.filter(pk__in=list(case_ids), project=project).update(
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.CASE_STEP_SYNC_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def single(self, request, pk):
        """
        api-获取单个api详情,返回body信息
        获取单个api的详细情况
        """
        try:
            api = models.API.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.API_NOT_FOUND)
        parse = Parse(literal_eval(api.body))
        parse.parse_http()
        resp = {
            "id": api.id,
            "body": parse.testcase,
            "success": True,
            "creator": api.creator.name,
            "relation": api.relation,
            "project": api.project.id,
        }
        return Response(resp)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/ci.py
New file
@@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
"""
@File    : CI.py
@Time    : 2023/3/14 11:25
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 持续集成CI视图
"""
import datetime
import json
import re
import time
from ast import literal_eval
from typing import Dict
import xmltodict
from django.http import HttpResponse
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django_celery_beat.models import PeriodicTask
from drf_yasg.utils import swagger_auto_schema
from lunarlink import models
from lunarlink.utils import loader, qy_message
from lunarlink.utils.decorator import request_log
from django.utils.decorators import method_decorator
from rest_framework import status
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from lunarlink.serializers import CISerializer, CIReportSerializer
from lunarlink.utils.loader import save_summary
from lunarlink.utils import response
def summary2junit(summary: Dict) -> Dict:
    """初始化JUnit的数据结构
    :param summary:
    :return:
    """
    res = {
        "testsuites": {
            "testsuite": {
                "errors": 0,
                "failures": 0,
                "hostname": "",
                "name": "",
                "skipped": 0,
                "tests": 0,
                "time": "0",
                "timestamp": "20210524T18:04:50.941913",
                "testcase": [],
            }
        }
    }
    time_info = summary.get("time")
    res["testsuites"]["testsuite"]["time"] = time_info.get("duration")
    start_at: str = time_info.get("start_at")
    timestamp = datetime.datetime.fromtimestamp(int(float(start_at))).strftime(
        "%Y-%m-%dT%H:%M:%S.%f"
    )
    res["testsuites"]["testsuite"]["timestamp"] = timestamp
    details = summary.get("details", [])
    res["testsuites"]["testsuite"]["tests"] = len(details)
    for detail in details:
        test_case = build_testcase(detail, res)
        res["testsuites"]["testsuite"]["testcase"].append(test_case)
    return res
def build_testcase(detail, res):
    """
    构建Junit Testcase
    :param detail:
    :param res:
    :return:
    """
    test_case = {"classname": "", "file": "", "line": "", "name": "", "time": ""}
    name = detail.get("name")
    test_case["classname"] = name  # 对应junit的Suite
    records = detail.get("records")
    test_case["line"] = len(records)
    test_case["time"] = detail["time"]["duration"]
    result = detail.get("success")
    step_names = []
    for index, record in enumerate(records):
        step_names.append(f"{index}-{record['name']}")
    test_case["name"] = "\n".join(step_names)  # 对应junit的每个case的Name
    # 记录错误case的详细信息
    if result is False:
        case_error, failure_details = build_failure_detail(records)
        if case_error:
            res["testsuites"]["testsuite"]["errors"] += 1
        else:
            res["testsuites"]["testsuite"]["failures"] += 1
        failure = {"message": "断言或者抽取失败", "#text": "\n".join(failure_details)}
        test_case["failure"] = failure
    return test_case
def build_failure_detail(records):
    """记录错误case的详细信息
    :param records:
    :return:
    """
    case_error = False
    failure_details = []
    for index, record in enumerate(records):
        step_status = record.get("status")
        if step_status == "failure":
            failure_details.append(
                f"{index}-{record['name']}\n{record.get('attachment')}\n{'*' * 68}"
            )
        elif step_status == "error":
            case_error = True
    return case_error, failure_details
class CIView(GenericViewSet):
    authentication_classes = []
    serializer_class = CISerializer
    pagination_class = None
    @swagger_auto_schema(operation_summary="gitlab-ci触发自动化用例运行")
    @method_decorator(request_log(level="INFO"))
    def run_ci_tests(self, request):
        """
        gitlab-ci发送请求
        测试平台解析参数,定时任务中的ci_project_ic和ci_env同时匹配时,返回触发执行任务
        """
        ser = CISerializer(data=request.data)
        if ser.is_valid():
            task_name = "lunarlink.tasks.schedule_debug_suite"
            ci_project_id: int = ser.validated_data.get("ci_project_id")
            ci_env: str = ser.validated_data.get("env")
            query = PeriodicTask.objects.filter(enabled=1, task=task_name)
            pk_kwargs_list = query.values("pk", "kwargs", "description")
            enabled_task_ids = []
            project = None
            for pk_kwargs in pk_kwargs_list:
                pk: int = pk_kwargs["pk"]
                kwargs: dict = json.loads(pk_kwargs["kwargs"])
                ci_project_ids: list = literal_eval(
                    kwargs.get("ci_project_ids") or "[]"
                )
                if isinstance(ci_project_ids, int):
                    ci_project_ids = [ci_project_ids]
                # 定时任务中的ci_project_id和ci_env同时匹配
                if (
                    ci_project_id in ci_project_ids
                    and ci_env
                    and ci_env == kwargs.get("ci_env")
                ):
                    enabled_task_ids.append(pk)
                    project = pk_kwargs["description"]
            # 没有匹配用例,直接返回
            if not enabled_task_ids:
                timestamp = datetime.datetime.fromtimestamp(int(time.time())).strftime(
                    "%Y-%m-%dT%H:%M:%S.%f"
                )
                not_found_case_res = {
                    "testsuites": {
                        "testsuite": {
                            "errors": 0,
                            "failures": 0,
                            "hostname": "",
                            "name": "没有找到匹配的用例",
                            "skipped": 0,
                            "tests": 0,
                            "time": "0",
                            "timestamp": timestamp,
                            "testcase": [],
                        }
                    }
                }
                xml_data = xmltodict.unparse(not_found_case_res)
                return HttpResponse(xml_data, content_type="text/xml")
            test_sets = []
            suite_list = []
            config_list = []
            config = None
            webhook_set = set()
            override_config_body = None
            for task_id in enabled_task_ids:
                task_obj = query.filter(id=task_id).first()
                # 如果task中存在重载配置,就覆盖用例中的配置
                override_config = json.loads(task_obj.kwargs).get("config")
                if (
                    override_config_body is None
                    and override_config
                    and override_config != "请选择"
                ):
                    try:
                        override_config_body = literal_eval(
                            models.Config.objects.get(
                                name=override_config, project__id=project
                            ).body
                        )
                    except ObjectDoesNotExist:
                        return Response(response.CONFIG_NOT_EXISTS)
                if task_obj:
                    # 判断webhook是否合法
                    case_ids = task_obj.args
                    url = json.loads(task_obj.kwargs).get("webhook")
                    url_pattern = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
                    if re.match(url_pattern, url):
                        webhook_set.add(url)
                else:
                    continue
                # 反查出一个task中包含的所有用例
                suite = list(
                    models.Case.objects.filter(pk__in=literal_eval(case_ids))
                    .order_by("id")
                    .values("id", "name")
                )
                for case in suite:
                    case_step_list = (
                        models.CaseStep.objects.filter(case__id=case["id"])
                        .order_by("step")
                        .values("body")
                    )
                    testcase_list = []
                    for case_step in case_step_list:
                        body = literal_eval(case_step["body"])
                        if body["request"].get("url"):
                            testcase_list.append(body)
                        elif config is None and body["request"].get("base_url"):
                            # 当前步骤时配置
                            # 如果task中存在重载配置,就覆盖用例中的配置
                            if override_config_body:
                                config = override_config_body
                            else:
                                try:
                                    config = literal_eval(
                                        models.Config.objects.get(
                                            name=body["name"], project__id=project
                                        ).body
                                    )
                                except ObjectDoesNotExist:
                                    return Response(response.CONFIG_NOT_EXISTS)
                    config_list.append(config)
                    test_sets.append(testcase_list)
                    config = None
                suite_list.extend(suite)
                override_config_body = None
            # 同步运行用例
            summary, _ = loader.debug_suite(
                suite=test_sets,
                project=project,
                obj=suite_list,
                config=config_list,
                save=False,
            )
            ci_project_namespace = ser.validated_data["ci_project_namespace"]
            ci_project_name = ser.validated_data["ci_project_name"]
            ci_job_id = ser.validated_data["ci_job_id"]
            summary["name"] = f"{ci_project_namespace}_{ci_project_name}_job{ci_job_id}"
            report_id = save_summary(
                name=summary.get("name"),
                summary=summary,
                project=project,
                report_type=4,
                user=ser.validated_data["start_job_user"],
                ci_metadata=ser.validated_data,
            )
            junit_results = summary2junit(summary)
            xml_data = xmltodict.unparse(junit_results)
            summary["task_name"] = "gitlab-ci_" + summary.get("name")
            summary["report_id"] = report_id
            for webhook in webhook_set:
                # TODO: 还需要优化企微发送,加入ci集成参数
                qy_message.send_message(
                    summary=summary,
                    webhook=webhook,
                    ci_job_url=ser.validated_data["ci_job_url"],
                    ci_pipeline_url=ser.validated_data["ci_pipeline_url"],
                    case_count=junit_results["testsuites"]["testsuite"]["tests"],
                )
            return HttpResponse(xml_data, content_type="text/xml")
        else:
            return Response(ser.errors, status=status.HTTP_400_BAD_REQUEST)
    @swagger_auto_schema(
        query_serializer=CIReportSerializer(),
        operation_summary="获取gitlab-ci运行的报告url",
    )
    def get_ci_report_url(self, request):
        """获取gitlab-ci运行的报告url"""
        ser = CIReportSerializer(data=request.query_params)
        if ser.is_valid():
            ci_job_id = ser.validated_data["ci_job_id"]
            report_obj = models.Report.objects.filter(ci_job_id=ci_job_id).first()
            if report_obj:
                report_url = f"{settings.BASE_REPORT_URL}/{report_obj.id}/"
            else:
                return Response(data=f"查找的ci_job_id: {ci_job_id}不存在")
            return Response(data=report_url)
        else:
            return Response(data=ser.errors, status=status.HTTP_400_BAD_REQUEST)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/config.py
New file
@@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
"""
@File    : config.py
@Time    : 2023/2/20 14:37
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 配置管理视图
"""
from ast import literal_eval
from django.core.exceptions import ObjectDoesNotExist
from django.utils.decorators import method_decorator
from django.utils import timezone
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from lunarlink import models, serializers
from lunarlink.utils import response
from lunarlink.utils.decorator import request_log
from lunarlink.utils.parser import Format
class ConfigView(GenericViewSet):
    """
    配置管理视图
    """
    serializer_class = serializers.ConfigSerializer
    queryset = models.Config.objects
    @action(detail=False, methods=["get"])
    @swagger_auto_schema(
        manual_parameters=[
            openapi.Parameter(
                "project",
                openapi.IN_QUERY,
                description="project id",
                type=openapi.TYPE_INTEGER,
                required=True,
            ),
            openapi.Parameter(
                "search",
                openapi.IN_QUERY,
                description="配置名称",
                type=openapi.TYPE_STRING,
                required=False,
            ),
        ],
    )
    @method_decorator(request_log(level="INFO"))
    def list(self, request):
        """
        获取项目管理配置
        query string - project, search
        """
        project = request.query_params.get("project")
        search = request.query_params.get("search")
        queryset = (
            self.get_queryset().filter(project__id=project).order_by("-update_time")
        )
        if search:
            queryset = queryset.filter(name__contains=search)
        pagination_queryset = self.paginate_queryset(queryset)
        serializer = self.get_serializer(pagination_queryset, many=True)
        return self.get_paginated_response(serializer.data)
    @method_decorator(request_log(level="DEBUG"))
    def all(self, request, pk):
        """
        获取所有的配置
        """
        queryset = (
            self.get_queryset()
            .filter(project__id=pk)
            .order_by("-update_time")
            .values("id", "name", "is_default", "base_url")
        )
        return Response(queryset)
    @method_decorator(request_log(level="INFO"))
    def add(self, request):
        """
        添加项目配置
        {
            name: str
            project: int
            body: dict
        }
        """
        config = Format(body=request.data, level="config")
        config.parse()
        try:
            config.project = models.Project.objects.get(id=config.project)
        except ObjectDoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        if models.Config.objects.filter(
            name=config.name, project=config.project
        ).first():
            return Response(response.CONFIG_EXISTS)
        config_body = {
            "name": config.name,
            "base_url": config.base_url,
            "body": config.testcase,
            "project": config.project,
        }
        models.Config.objects.create(**config_body, creator=request.user)
        return Response(response.CONFIG_ADD_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def update(self, request, pk):
        """
        更新配置
        {
            name: str,
            base_url: str,
            variables: [],
            parameters: [],
            request: [],
        }
        """
        try:
            config = models.Config.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.CONFIG_NOT_EXISTS)
        format_obj = Format(body=request.data, level="config")
        format_obj.parse()
        if (
            models.Config.objects.exclude(id=pk)
            .filter(name=format_obj.name, project=config.project_id)
            .first()
        ):
            return Response(response.CONFIG_EXISTS)
        case_step = models.CaseStep.objects.filter(
            method="config", name=config.name, case__project_id=config.project_id
        )
        for case in case_step:
            case.name = format_obj.name
            case.body = format_obj.testcase
            case.save()
        config.name = format_obj.name
        config.body = format_obj.testcase
        config.base_url = format_obj.base_url
        if format_obj.is_default is True:
            models.Config.objects.filter(
                project=config.project_id, is_default=True
            ).update(
                is_default=False,
                updater=request.user.id,
                update_time=timezone.now(),
            )
        config.is_default = format_obj.is_default
        config.updater = request.user.id
        config.update_time = timezone.now()
        config.save()
        return Response(response.CONFIG_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def copy(self, request, pk):
        """复制配置
        pk: int
        {
            name: str
        }
        """
        try:
            config = models.Config.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.CONFIG_NOT_EXISTS)
        if models.Config.objects.filter(**request.data, project=config.project).first():
            return Response(response.CONFIG_EXISTS)
        config.id = None
        config.is_default = False
        body = literal_eval(config.body)
        try:
            name = request.data["name"]
        except KeyError:
            return Response(response.KEY_MISS)
        body["name"] = name
        config.name = name
        config.body = body
        config.creator = request.user
        config.updater = request.user.id
        config.save()
        return Response(response.CONFIG_ADD_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def destroy(self, request, pk):
        """
        单个删除
        pk: config id
        """
        try:
            config = models.Config.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.CONFIG_NOT_EXISTS)
        if models.CaseStep.objects.filter(
            method="config",
            name=config.name,
            case__project=config.project,
        ).exists():
            return Response(response.CONFIG_IS_USED)
        config.is_deleted = True
        config.updater = request.user.id
        config.update_time = timezone.now()
        config.save()
        return Response(response.CONFIG_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def bulk_destroy(self, request):
        """
        批量删除配置
        [{id:int}]
        """
        ids = [content["id"] for content in request.data]
        configs = models.Config.objects.filter(id__in=ids)
        if not configs:
            return Response(response.CONFIG_NOT_EXISTS)
        unused_ids = []
        for config in configs:
            if models.CaseStep.objects.filter(
                method="config",
                name=config.name,
                case__project=config.project,
            ).exists():
                continue
            else:
                unused_ids.append(config.id)
        if not unused_ids:
            return Response(response.CONFIG_IS_USED)
        models.Config.objects.filter(id__in=unused_ids).update(
            is_deleted=True,
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.CONFIG_DEL_SUCCESS)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/debugtalk.py
New file
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""
@File    : debugtalk.py
@Time    : 2023/2/22 15:07
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 驱动代码视图
"""
from django.core.exceptions import ObjectDoesNotExist
from django.utils.decorators import method_decorator
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from lunarlink import models
from lunarlink import serializers
from lunarlink.utils import response
from lunarlink.utils.decorator import request_log
from lunarlink.utils.runner import DebugCode
class DebugTalkView(GenericViewSet):
    """
    DebugTalk update
    """
    serializer_class = serializers.DebugTalkSerializer
    queryset = models.Debugtalk.objects
    @method_decorator(request_log(level="INFO"))
    def debugtalk(self, request, pk):
        """
        获取debugtalk code
        """
        try:
            queryset = models.Debugtalk.objects.get(project__id=pk)
        except ObjectDoesNotExist:
            return Response(response.DEBUGTALK_NOT_EXISTS)
        serializer = self.get_serializer(queryset, many=False)
        return Response(serializer.data)
    @method_decorator(request_log(level="INFO"))
    def update(self, request):
        """
        编辑debugtalk.py代码并保存
        {
            id: int # debugtalk id
            code: str
        }
        """
        try:
            debugtalk_id = request.data["id"]
            debugtalk_code = request.data["code"]
        except KeyError:
            return Response(response.KEY_MISS)
        try:
            models.Debugtalk.objects.get(id=debugtalk_id)
            models.Debugtalk.objects.filter(id=debugtalk_id).update(
                code=debugtalk_code,
                updater=request.user.id,
                update_time=timezone.now(),
            )
        except ObjectDoesNotExist:
            return Response(response.DEBUGTALK_NOT_EXISTS)
        return Response(response.DEBUGTALK_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def run(self, request):
        """在线运行"""
        try:
            code = request.data["code"]
        except KeyError:
            return Response(response.KEY_MISS)
        debug = DebugCode(code)
        debug.run()
        resp = {"msg": debug.resp, "success": True, "code": "0001"}
        return Response(resp)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/project.py
New file
@@ -0,0 +1,343 @@
# -*- coding: utf-8 -*-
"""
@File    : project.py
@Time    : 2023/1/14 15:54
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 项目视图
"""
from typing import Dict
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils.decorators import method_decorator
from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
from backend.utils import pagination, permissions
from lunarlink import models
from lunarlink import serializers
from lunarlink.dto.tree_dto import TreeOut, TreeUniqueIn, TreeUpdateIn
from lunarlink.services.tree_service_impl import tree_service
from lunarlink.utils import day, prepare, response
from lunarlink.utils.day import get_day, get_month_format, get_week_format
from lunarlink.utils.decorator import request_log
from lunarlink.utils.response import StandResponse
class ProjectView(GenericViewSet):
    """项目增删查改"""
    serializer_class = serializers.ProjectSerializer
    pagination_class = pagination.MyCursorPagination
    def get_queryset(self):
        # 如果是超级管理员,返回所有项目
        if self.request.user.is_superuser:
            return models.Project.objects.all()
        # 获取当前用户所在的所有分组
        user_groups = self.request.user.groups.all()
        # 返回这些分组相关的项目
        return models.Project.objects.filter(groups__in=user_groups).distinct()
    def get_permissions(self):
        # 如果是 delete 方法,确保用户是管理员
        if self.action == "delete":
            return [permissions.CustomIsAdminUser()]
        return super().get_permissions()
    @method_decorator(request_log(level="DEBUG"))
    def list(self, request):
        """
        查询项目信息
        """
        projects = self.get_queryset()
        page_projects = self.paginate_queryset(projects)
        serializer = self.get_serializer(page_projects, many=True)
        return self.get_paginated_response(serializer.data)
    @method_decorator(request_log(level="INFO"))
    def add(self, request):
        """
        添加项目
        """
        try:
            name = request.data["name"]
        except KeyError:
            return Response(response.KEY_MISS)
        if models.Project.objects.filter(name=name).first():
            return Response(response.PROJECT_EXISTS)
        # 反序列化
        serializer = serializers.ProjectSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(creator=request.user, updater=request.user.id)
            project = models.Project.objects.get(name=name)
            prepare.project_init(project=project, creator=request.user)
            return Response(response.PROJECT_ADD_SUCCESS)
        return Response(response.SYSTEM_ERROR)
    @method_decorator(request_log(level="INFO"))
    def update(self, request):
        """
        编辑项目
        """
        project_id = request.data.get("id")
        project_name = request.data.get("name")
        try:
            project = models.Project.objects.get(id=project_id)
        except ObjectDoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        if project_name != project.name:
            if models.Project.objects.filter(name=project_name).exists():
                return Response(response.PROJECT_EXISTS)
        serializer = self.get_serializer(project, data=request.data, partial=True)
        if not serializer.is_valid():
            return Response(response.KEY_MISS)
        # 调用save方法update_time字段才会自动更新
        serializer.save(updater=request.user.id)
        return Response(response.PROJECT_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def delete(self, request):
        """
        删除项目
        """
        try:
            project = models.Project.objects.get(id=request.data["id"])
            project.is_deleted = True
            project.updater = request.user.id
            project.update_time = timezone.now()
            with transaction.atomic():
                project.save()
        except models.Project.DoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        except (IntegrityError, ValidationError) as e:
            return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            return Response(
                {"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )
        return Response(response.PROJECT_DELETE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def single(self, request, pk):
        """
        获取单个项目相关统计信息
        """
        try:
            queryset = models.Project.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        serializer = self.get_serializer(queryset, many=False)
        project_info = prepare.get_project_detail_v2(pk=pk)
        # TODO: 屏蔽jira核心用例统计,考虑接入TAPD
        jira_core_case_cover_rate: Dict = prepare.get_jira_core_case_cover_rate(pk=pk)
        project_info.update(jira_core_case_cover_rate)
        project_info.update(serializer.data)
        return Response(project_info)
    @method_decorator(request_log(level="INFO"))
    def yapi_info(self, request, pk):
        """获取项目的yapi地址和token"""
        try:
            obj = models.Project.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        ser = self.get_serializer(obj, many=False)
        return Response(ser.data)
class DashBoardView(APIView):
    """项目看板"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        _, report_status = prepare.aggregate_reports_by_status(project_id=0)
        _, report_type = prepare.aggregate_reports_by_type(project_id=0)
        report_day = prepare.aggregate_reports_or_case_bydate(
            date_type="day", model=models.Report
        )
        report_week = prepare.aggregate_reports_or_case_bydate(
            date_type="week", model=models.Report
        )
        report_month = prepare.aggregate_reports_or_case_bydate(
            date_type="month", model=models.Report
        )
        api_day = prepare.aggregate_apis_bydate(date_type="day")
        api_week = prepare.aggregate_apis_bydate(date_type="week")
        api_month = prepare.aggregate_apis_bydate(date_type="month")
        (
            daily_top_api_creators,
            daily_api_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="day",
            model=models.API,
        )
        (
            weekly_top_api_creators,
            weekly_api_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="week",
            model=models.API,
        )
        (
            monthly_top_api_creators,
            monthly_api_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="month",
            model=models.API,
        )
        yapi_day = prepare.aggregate_apis_bydate(date_type="day", is_yapi=True)
        yapi_week = prepare.aggregate_apis_bydate(date_type="week", is_yapi=True)
        yapi_month = prepare.aggregate_apis_bydate(date_type="month", is_yapi=True)
        case_day = prepare.aggregate_reports_or_case_bydate(
            date_type="day", model=models.Case
        )
        case_week = prepare.aggregate_reports_or_case_bydate(
            date_type="week", model=models.Case
        )
        case_month = prepare.aggregate_reports_or_case_bydate(
            date_type="month", model=models.Case
        )
        (
            daily_top_case_creators,
            daily_case_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="day",
            model=models.Case,
        )
        (
            weekly_top_case_creators,
            weekly_case_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="week",
            model=models.Case,
        )
        (
            monthly_top_case_creators,
            monthly_case_creator_counts,
        ) = prepare.aggregate_data_by_date(
            date_type="month",
            model=models.Case,
        )
        res = {
            "report": {
                "status": report_status,
                "type": report_type,
                "week": report_week,
                "month": report_month,
                "day": report_day,
            },
            "case": {
                "week": case_week,
                "month": case_month,
                "day": case_day,
                "daily_top_creators": daily_top_case_creators,
                "daily_creator_counts": daily_case_creator_counts,
                "weekly_top_creators": weekly_top_case_creators,
                "weekly_creator_counts": weekly_case_creator_counts,
                "monthly_top_creators": monthly_top_case_creators,
                "monthly_creator_counts": monthly_case_creator_counts,
            },
            "api": {
                "week": api_week,
                "month": api_month,
                "day": api_day,
                "daily_top_creators": daily_top_api_creators,
                "daily_creator_counts": daily_api_creator_counts,
                "weekly_top_creators": weekly_top_api_creators,
                "weekly_creator_counts": weekly_api_creator_counts,
                "monthly_top_creators": monthly_top_api_creators,
                "monthly_creator_counts": monthly_api_creator_counts,
            },
            "yapi": {"week": yapi_week, "month": yapi_month, "day": yapi_day},
            # 包含今天的前6天
            "recent_days": [get_day(n)[5:] for n in range(-5, 1)],
            "recent_months": [get_month_format(n) for n in range(-5, 1)],
            "recent_weeks": [get_week_format(n) for n in range(-5, 1)],
        }
        return Response(res)
class TreeView(APIView):
    """
    树形结构视图
    """
    @method_decorator(request_log(level="INFO"))
    def get(self, request, pk):
        """
        获取树形结构
        如果没有节点存在,创建一个默认的节点
        """
        tree_type = request.query_params["type"]
        resp: StandResponse[TreeOut] = tree_service.get_or_create(
            TreeUniqueIn(project_id=pk, type=tree_type)
        )
        return Response(resp.dict())
    @method_decorator(request_log(level="INFO"))
    def patch(self, request, pk):
        """
        更新树形结构
        """
        res = tree_service.patch(tree_id=pk, payload=TreeUpdateIn(**request.data))
        return Response(res.dict())
class VisitView(GenericViewSet):
    serializer_class = serializers.VisitSerializer
    queryset = models.Visit.objects
    def list(self, request):
        project = request.query_params.get("project")
        # 查询项目前7天的访问记录
        # 根据日期分组
        # 统计每天的条数
        recent7days = [day.get_day(d)[5:] for d in range(-7, 0)]
        count_data = (
            self.get_queryset()
            .filter(
                project=project, create_time__range=(day.get_day(-7), day.get_day())
            )
            .extra(select={"create_time": "DATE_FORMAT(create_time, '%%m-%%d')"})
            .values("create_time")
            .annotate(counts=Count("id"))
            .values("create_time", "counts")
        )
        create_time_report_map = {
            data["create_time"]: data["counts"] for data in count_data
        }
        report_count = [create_time_report_map.get(d, 0) for d in recent7days]
        return Response({"recent7days": recent7days, "report_count": report_count})
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/report.py
New file
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
"""
@File    : report.py
@Time    : 2023/2/13 09:42
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 测试报告视图
"""
import json
import re
from ast import literal_eval
from shlex import quote
from typing import Dict
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.utils import timezone
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from backend.utils import pagination
from lunarlink import models, serializers
from lunarlink.utils import response
from lunarlink.utils.convert2hrp import Hrp
from lunarlink.utils.decorator import request_log
class ConvertRequest:
    @classmethod
    def _to_curl(cls, request, compressed: bool = False, verify: bool = True):
        """Return string with curl command by provided request object
        :param request:
        :param compressed: If `True` then `--compressed` argument will be added to result
        :param verify:
        :return:
        """
        parts = [
            ("curl", None),
            ("-X", request.method),
        ]
        parts += [
            (None, request.url),
        ]
        for k, v in sorted(request.headers.items()):
            parts += [("-H", "{0}: {1}".format(k, v))]
        if request.body:
            body = request.body
            if isinstance(body, bytes):
                body = body.decode("utf-8")
            if isinstance(body, dict):
                body = json.dumps(body)
            parts += [("-d", body)]
        if compressed:
            parts += [("--compressed", None)]
        if not verify:
            parts += [("--insecure", None)]
        flat_parts = []
        for k, v in parts:
            if k:
                flat_parts.append(quote(k))
            if v:
                flat_parts.append(quote(v))
                if k == "-H":
                    flat_parts.append(" \\\n")
        return " ".join(flat_parts)
    @classmethod
    def _make_fake_req(cls, request_meta_dict):
        class RequestMeta:
            ...
        req = RequestMeta()
        setattr(req, "method", request_meta_dict["method"])
        setattr(req, "url", request_meta_dict["url"])
        setattr(req, "headers", request_meta_dict["headers"])
        body = request_meta_dict.get("body") or request_meta_dict.get("data")
        setattr(req, "body", body)
        return req
    @classmethod
    def to_curl(cls, req: Dict) -> str:
        _req = cls._make_fake_req(req)
        return cls._to_curl(_req, compressed=True, verify=False)
    @classmethod
    def to_hrp(cls, req: Dict) -> Dict:
        hrp = Hrp(faster_req_json=req)
        return hrp.get_testcase().dict()
    @classmethod
    def generate_curl(cls, report_details, convert_type=("curl",)):
        for detail in report_details:
            for record in detail["records"]:
                meta_data = record["meta_data"]
                for t in convert_type:
                    req = meta_data["request"]
                    method_name = f"to_{t}"
                    method = getattr(ConvertRequest, method_name)
                    record["meta_data"][t] = method(req)
class ReportView(GenericViewSet):
    """报告视图"""
    queryset = models.Report.objects
    serializer_class = serializers.ReportSerializer
    pagination_class = pagination.MyPageNumberPagination
    def get_authenticators(self):
        # 查看报告详情不需要鉴权
        pattern = re.compile(r"/api/lunarlink/reports/\d+")
        if (
            self.request.method == "GET"
            and re.search(pattern, self.request.path) is not None
        ):
            return []  # 不需要任何鉴权
        return super().get_authenticators()  # 默认所有鉴权
    def get_permissions(self):
        # 如果是look方法,不需要任何权限
        if self.action == "look":
            return [AllowAny()]
        return super().get_permissions()
    @method_decorator(request_log(level="DEBUG"))
    def list(self, request):
        """获取测试报告列表"""
        project = request.query_params.get("project")
        search = request.query_params.get("search")
        report_type = request.query_params.get("reportType")
        report_status = request.query_params.get("reportStatus")
        only_me = request.query_params.get("onlyMe")
        queryset = (
            self.get_queryset().filter(project__id=project).order_by("-update_time")
        )
        # 前端传过来是小写的字符串,不是python的True
        if only_me == "true":
            queryset = queryset.filter(creator=request.user)
        if search != "":
            queryset = queryset.filter(name__contains=search)
        if report_type != "":
            queryset = queryset.filter(type=report_type)
        if report_status != "":
            queryset = queryset.filter(status=report_status)
        page_report = self.paginate_queryset(queryset)
        serializer = self.get_serializer(page_report, many=True)
        return self.get_paginated_response(serializer.data)
    @method_decorator(request_log(level="INFO"))
    def look(self, request, pk):
        """
        查看报告
        查看报告详情
        """
        try:
            report = models.Report.objects.get(id=pk)
            report_detail = models.ReportDetail.objects.get(report__id=pk)
        except ObjectDoesNotExist:
            return Response(response.REPORT_NOT_EXISTS)
        summary = json.loads(report.summary)
        summary["details"] = literal_eval(report_detail.summary_detail)
        ConvertRequest.generate_curl(summary["details"], convert_type=("curl",))
        summary["html_report_name"] = report.name
        return render(request, template_name="report_template.html", context=summary)
    @method_decorator(request_log(level="INFO"))
    def destroy(self, request, pk):
        """
        单个删除
        pk: id
        """
        report_obj = models.Report.objects.filter(id=pk).first()
        if not report_obj:
            return Response(response.REPORT_NOT_EXISTS)
        report_obj.is_deleted = True
        report_obj.updater = request.user.id
        report_obj.update_time = timezone.now()
        report_obj.save()  # 这将触发 pre_save 信号,并执行 delete_related_report_detail 处理器函数
        return Response(response.REPORT_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def bulk_destroy(self, request):
        """
        批量删除报告
        [{id:int}]
        """
        ids = [content["id"] for content in request.data]
        # TODO: 如果有大量的数据,考虑性能问题
        objs = list(models.Report.objects.filter(id__in=ids))  # 将QuerySet转换为list
        if not objs:
            return Response(response.REPORT_NOT_EXISTS)
        try:
            with transaction.atomic():
                models.Report.objects.filter(id__in=ids).update(
                    is_deleted=True,
                    update_time=timezone.now(),
                    updater=request.user.id,
                )
                models.ReportDetail.objects.filter(report__in=objs).update(
                    is_deleted=True
                )
        except Exception as e:
            return Response({"error": str(e)}, status=400)
        return Response(response.REPORT_DEL_SUCCESS)
    # TODO: 下载报告 待实现
    @method_decorator(request_log(level="INFO"))
    def download(self, request, **kwargs):
        """下载报告"""
        pass
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/run.py
New file
@@ -0,0 +1,471 @@
# -*- coding: utf-8 -*-
"""
@File    : run.py
@Time    : 2023/2/6 10:51
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 运行API
"""
import logging
from ast import literal_eval
from django.core.exceptions import ObjectDoesNotExist
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import api_view
from rest_framework.response import Response
from lunarlink.utils import loader, response
from lunarlink import tasks
from lunarlink.utils.decorator import request_log
from lunarlink.utils.parser import Format
from lunarlink import models
from apps.exceptions.error import (
    ApiNotFound,
    ConfigNotFound,
    CaseStepNotFound,
)
logger = logging.getLogger(__name__)
"""运行方式
"""
config_err = {
    "success": False,
    "msg": "指定的配置文件不存在",
    "code": "9999",
}
@api_view(["GET"])
@request_log(level="INFO")
def run_api_pk(request, pk):
    """
    运行单个接口
    """
    config_name = request.query_params.get("config", "请选择")
    try:
        api = models.API.objects.get(id=pk)
    except ObjectDoesNotExist:
        return Response(response.API_NOT_FOUND)
    config = (
        None
        if config_name == "请选择"
        else literal_eval(
            models.Config.objects.get(name=config_name, project=api.project).body
        )
    )
    summary = loader.debug_api(
        api=literal_eval(api.body),
        project=api.project.id,
        name=api.name,
        config=config,
        user=request.user.id,
    )
    return Response(summary)
@api_view(["POST"])
@request_log(level="INFO")
def run_api(request):
    """run api by body"""
    config_name = request.data.pop("config", "请选择")
    api = Format(request.data)
    api.parse()
    config = None
    if config_name != "请选择":
        try:
            config = literal_eval(
                models.Config.objects.get(
                    name=config_name, project__id=api.project
                ).body
            )
        except ObjectDoesNotExist:
            logger.error(f"指定配置文件不存在:{config_name}")
            return Response(config_err)
    summary = loader.debug_api(
        api=api.testcase,
        project=api.project,
        name=api.name,
        config=config,
        user=request.user.id,
    )
    return Response(summary)
@swagger_auto_schema(
    method="get",
    manual_parameters=[
        openapi.Parameter(
            "project",
            openapi.IN_QUERY,
            description="project id",
            type=openapi.TYPE_INTEGER,
            required=True,
        ),
        openapi.Parameter(
            "name",
            openapi.IN_QUERY,
            description="case name",
            type=openapi.TYPE_STRING,
            required=True,
        ),
        openapi.Parameter(
            "async",
            openapi.IN_QUERY,
            description="async",
            type=openapi.TYPE_STRING,
        ),
    ],
    operation_summary="执行单个测试用例集",
)
@api_view(["GET"])
@request_log(level="INFO")
def run_testsuite_pk(request, **kwargs):
    """
    执行测试用例集
    URL参数 query string:
        project
        name
    """
    pk = kwargs.get("pk")
    project = request.query_params["project"]
    name = request.query_params["name"]
    back_async = request.query_params.get("async", False)
    test_case = []
    config = None
    test_list = (
        models.CaseStep.objects.filter(case__id=pk).order_by("step").values("body")
    )
    for content in test_list:
        body = literal_eval(content["body"])
        if "base_url" in body["request"].keys():
            try:
                config = literal_eval(
                    models.Config.objects.get(
                        name=body["name"], project__id=project
                    ).body
                )
            except ObjectDoesNotExist:
                return Response(response.CONFIG_NOT_EXISTS)
            else:
                continue
        test_case.append(body)
    # 异步执行
    if back_async:
        tasks.async_debug_api.delay(
            api=test_case,
            project=project,
            name=name,
            config=config,
            user=request.user.id,
        )
        summary = response.TASK_RUN_SUCCESS
    else:  # 同步
        summary = loader.debug_api(
            api=test_case,
            project=project,
            name=name,
            config=config,
            user=request.user.id,
        )
    return Response(summary)
@swagger_auto_schema(
    method="post",
    request_body=openapi.Schema(
        type=openapi.TYPE_OBJECT,
        required=[
            "body",
            "project",
            "name",
        ],
        properties={
            "body": openapi.Schema(
                type=openapi.TYPE_ARRAY,
                items=openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                ),
                description="body",
            ),
            "project": openapi.Schema(type=openapi.TYPE_INTEGER, description="project"),
            "name": openapi.Schema(type=openapi.TYPE_STRING, description="name"),
        },
    ),
)
@api_view(["POST"])
@request_log(level="INFO")
def run_testsuite(request):
    """
    调式测试用例集
    {
        name: str,
        body: dict,
        project: int,
    }
    """
    try:
        body = request.data["body"]
        project = request.data["project"]
        name = request.data["name"]
    except ObjectDoesNotExist:
        return Response(response.KEY_MISS)
    test_case = []
    config = None
    for test in body:
        try:
            test = loader.load_test(test=test, project=project)
        except ConfigNotFound:
            return Response(response.CONFIG_NOT_EXISTS)
        except ApiNotFound:
            return Response(response.API_NOT_FOUND)
        except CaseStepNotFound:
            return Response(response.CASE_STEP_NOT_EXIST)
        if "base_url" in test["request"].keys():
            config = test
            continue
        test_case.append(test)
    summary = loader.debug_api(
        api=test_case,
        project=project,
        name=name,
        config=config,
        user=request.user.id,
    )
    return Response(summary)
@api_view(["POST"])
@request_log(level="INFO")
def run_test(request):
    """
    测试用例中调式单个接口
    入参:
    {
        body: dict,
        project: int,
        config: null or dict,
    }
    """
    body = request.data.get("body")
    config = request.data.get("config", None)
    project = request.data.get("project")
    if config:
        try:
            config_obj = models.Config.objects.get(project=project, name=config["name"])
        except ObjectDoesNotExist:
            return Response(response.CONFIG_NOT_EXISTS)
        config = literal_eval(config_obj.body)
    try:
        test = loader.load_test(test=body)
    except ConfigNotFound:
        return Response(response.CONFIG_NOT_EXISTS)
    except ApiNotFound:
        return Response(response.API_NOT_FOUND)
    except CaseStepNotFound:
        return Response(response.CASE_STEP_NOT_EXIST)
    summary = loader.debug_api(
        api=test,
        project=project,
        name=body.get("name", None),
        config=config,
        user=request.user.id,
    )
    return Response(summary)
@api_view(["POST"])
@request_log(level="INFO")
def run_suite_tree(request):
    """
    选择目录节点执行用例集
    {
        project: int
        relation: list
        name: str
        async: bool
    }
    """
    config = None
    try:
        project = request.data["project"]
        relation = request.data["relation"]
        back_async = request.data["async"]
        report = request.data["name"]
        config_id = request.data.get("config_id")
    except KeyError:
        return Response(response.KEY_MISS)
    if config_id:
        # 前端有指定config, 会覆盖用例本身的config
        config = literal_eval(models.Config.objects.get(id=config_id).body)
    test_sets = []
    suite_list = []
    config_list = []
    for relation_id in relation:
        case_name_id_mapping_list = list(
            models.Case.objects.filter(project__id=project, relation=relation_id)
            .order_by("id")
            .values("id", "name")
        )
        for content in case_name_id_mapping_list:
            test_list = (
                models.CaseStep.objects.filter(case__id=content["id"])
                .order_by("step")
                .values("body")
            )
            testcase_list = []
            for test in test_list:
                body = literal_eval(test["body"])
                if body["request"].get("url"):
                    testcase_list.append(body)
                elif config is None and body["request"].get("base_url"):
                    config = literal_eval(
                        models.Config.objects.get(
                            name=body["name"], project__id=project
                        ).body
                    )
            config_list.append(config)
            test_sets.append(testcase_list)
            config = None
        suite_list.extend(case_name_id_mapping_list)
    if back_async:  # 异步
        tasks.async_debug_suite.delay(
            suite=test_sets,
            project=project,
            obj=suite_list,
            report=report,
            config=config_list,
            user=request.user.id,
        )
        summary = loader.TEST_NOTE_EXISTS
        summary["msg"] = "用例运行中,请稍后查看报告"
    else:
        summary, _ = loader.debug_suite(
            suite=test_sets,
            project=project,
            obj=suite_list,
            config=config_list,
            save=True,
            user=request.user.id,
        )
    return Response(summary)
@api_view(["POST"])
@request_log(level="INFO")
def run_multi_tests(request):
    """
    通过指定id, 运行多个指定用例
    {
        "name": "批量运行2条用例",  # 报告名
        "project": 11,
        "case_config_mapping_list": [
            {
                "config_name": "config_name1,
                "id": 153,  # 用例id
                "name": "case_name1"  # 用例名
            }
        ]
    }
    """
    try:
        project = request.data["project"]
        report_name = request.data["name"]
    except KeyError:
        return Response(response.KEY_MISS)
    # 默认同步运行用例
    back_async = request.data.get("async") or False
    case_config_mapping_list = request.data["case_config_mapping_list"]
    config_body_mapping = {}
    # 解析用例列表中的配置
    for config in case_config_mapping_list:
        config_name = config["config_name"]
        if not config_body_mapping.get(config_name):
            config_body_mapping[config_name] = literal_eval(
                models.Config.objects.get(
                    name=config["config_name"], project__id=project
                ).body
            )
    test_sets = []
    suite_list = []
    config_list = []
    for case_config_mapping in case_config_mapping_list:
        case_id = case_config_mapping["id"]
        config_name = case_config_mapping["config_name"]
        # 获取用例的所有步骤
        case_step_list = (
            models.CaseStep.objects.filter(case__id=case_id)
            .order_by("step")
            .values("body")
        )
        parsed_case_step_list = []
        for case_step in case_step_list:
            body = literal_eval(case_step["body"])
            if body["request"].get("url"):
                parsed_case_step_list.append(body)
        config_body = config_body_mapping[config_name]
        # 记录当前用例的配置信息
        config_list.append(config_body)
        # 记录已经解析好的用例
        test_sets.append(parsed_case_step_list)
    # 用例和配置的映射关系
    suite_list.extend(case_config_mapping_list)
    if back_async:  # 异步
        tasks.async_debug_suite.delay(
            suite=test_sets,
            project=project,
            obj=suite_list,
            report=report_name,
            config=config_list,
            user=request.user.id,
        )
        summary = loader.TEST_NOTE_EXISTS
        summary["msg"] = "用例运行中,请稍后查看报告"
    else:
        summary, _ = loader.debug_suite(
            suite=test_sets,
            project=project,
            obj=suite_list,
            config=config_list,
            save=True,
            user=request.user,
            report_name=report_name,
        )
    return Response(summary)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/schedule.py
New file
@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
"""
@File    : schedule.py
@Time    : 2023/3/9 10:42
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 定时任务视图
"""
import logging
import json
from ast import literal_eval
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.decorators import method_decorator
from django_celery_beat import models
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from apps.exceptions.error import TaskNotFound
from backend.utils import pagination
from backend.celery import app
from lunarlink import serializers
from lunarlink.utils import response
from lunarlink.utils.decorator import request_log
from lunarlink.utils.task import Task
logger = logging.getLogger(__name__)
class ScheduleView(GenericViewSet):
    """
    定时任务增删改查
    """
    queryset = models.PeriodicTask.objects
    serializer_class = serializers.PeriodicTaskSerializer
    pagination_class = pagination.MyPageNumberPagination
    @method_decorator(request_log(level="INFO"))
    def list(self, request):
        """
        获取定时任务列表
        query string
        """
        project = request.query_params.get("project")
        task_name = request.query_params.get("task_name")
        creator = request.query_params.get("creator")
        schedule = (
            self.get_queryset().filter(description=project).order_by("-date_changed")
        )
        if task_name:
            schedule = schedule.filter(name__contains=task_name)
        if creator:
            schedule = schedule.filter(kwargs__contains=f'"creator": "{creator}"')
        page_schedule = self.paginate_queryset(schedule)
        serializer = self.get_serializer(page_schedule, many=True)
        return self.get_paginated_response(serializer.data)
    @method_decorator(request_log(level="INFO"))
    def add(self, request):
        """
        新增定时任务
        {
            name: str
            crontab: str
            switch: bool
            data: [int, int]
            strategy: str
            receiver: str
            copy: str
            project: int
        }
        """
        project = request.data.get("project")
        name = request.data.get("name")
        if models.PeriodicTask.objects.filter(
            name=f"{project}_{name}", description=project
        ).exists():
            return Response(response.TASK_HAS_EXISTS)
        ser = serializers.ScheduleDeSerializer(data=request.data)
        if ser.is_valid():
            request.data.update({"creator": request.user.name})
            task = Task(**request.data)
            try:
                task.add_task()
            except Exception as e:
                logger.error(f"An unexpected error occurred: {e}", exc_info=True)
                return Response({"error": "An unexpected error occurred"}, status=500)
            return Response(response.TASK_ADD_SUCCESS)
        else:
            return Response({**response.TASK_ADD_FAILURE, "msg": ser.errors})
    @method_decorator(request_log(level="INFO"))
    def update(self, request, pk):
        """
        更新定时任务
        """
        project = request.data.get("project")
        name = request.data.get("name")
        if models.PeriodicTask.objects.filter(
            ~Q(id=pk), name=f"{project}_{name}"
        ).exists():
            return Response(response.TASK_DUPLICATE_NAME)
        ser = serializers.ScheduleDeSerializer(data=request.data)
        if ser.is_valid():
            task = Task(**request.data)
            try:
                task.update_task(task_id=pk)
            except TaskNotFound:
                return Response(response.TASK_NOT_EXISTS)
            return Response(response.TASK_UPDATE_SUCCESS)
        else:
            return Response({**response.TASK_UPDATE_FAILURE, "msg": ser.errors})
    @method_decorator(request_log(level="INFO"))
    def patch(self, request, pk):
        """
        更新任务的状态
        {"switch": bool}
        """
        try:
            switch = request.data["switch"]
        except KeyError:
            return Response(response.KEY_MISS)
        try:
            task_obj = self.get_queryset().get(pk=pk)
        except ObjectDoesNotExist:
            return Response(response.TASK_NOT_EXISTS)
        task_obj.enabled = switch
        kwargs = json.loads(task_obj.kwargs)
        kwargs["updater"] = request.user.name
        task_obj.kwargs = json.dumps(kwargs, ensure_ascii=False)
        task_obj.save()
        return Response(response.TASK_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def delete(self, request, pk):
        """
        删除任务
        query string
        """
        try:
            task = models.PeriodicTask.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.TASK_NOT_EXISTS)
        task.enabled = False  # 关闭任务
        task.delete()
        return Response(response.TASK_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def copy(self, request, pk):
        """
        复制定时任务
        {"name": string}
        """
        try:
            task_name = request.data["name"]
        except KeyError:
            return Response(response.KEY_MISS)
        try:
            task_obj = self.get_queryset().get(pk=pk)
        except ObjectDoesNotExist:
            return Response(response.TASK_NOT_EXISTS)
        if task_obj.name == task_name:
            return Response(response.TASK_COPY_FAILURE)
        if (
            self.get_queryset()
            .filter(name=f"{task_obj.description}_{task_name}")
            .exists()
        ):
            return Response(response.TASK_COPY_FAILURE)
        task_obj.id = None
        task_obj.name = f"{task_obj.description}_{task_name}"
        task_obj.total_run_count = 0
        kwargs = json.loads(task_obj.kwargs)
        kwargs["creator"] = request.user.name
        kwargs["updater"] = ""
        task_obj.kwargs = json.dumps(kwargs, ensure_ascii=False)
        task_obj.save()
        return Response(response.TASK_COPY_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def run(self, request, pk):
        """
        手动执行定时任务
        query string
        """
        try:
            task_obj = models.PeriodicTask.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.TASK_NOT_EXISTS)
        task_name = "lunarlink.tasks.schedule_debug_suite"
        args = literal_eval(task_obj.args)
        kwargs = json.loads(task_obj.kwargs)
        kwargs["task_id"] = task_obj.id
        app.send_task(
            name=task_name,
            args=args,
            kwargs=kwargs,
            queue="beat_tasks",
        )
        return Response(response.TASK_RUN_SUCCESS)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/suite.py
New file
@@ -0,0 +1,666 @@
# -*- coding: utf-8 -*-
"""
@File    : suite.py
@Time    : 2023/2/23 15:50
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 测试用例
"""
import json
import logging
from enum import Enum
from typing import List
from django.core.exceptions import ObjectDoesNotExist
from django_celery_beat import models as celery_models
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.decorators import method_decorator
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.views import APIView
from rest_framework.mixins import DestroyModelMixin
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from apps.exceptions.error import (
    ApiNotFound,
    CaseStepNotFound,
    ConfigNotFound,
    RelationNotFound,
)
from apps.schema.request import RequestInfo
from backend.utils.redis_manager import RedisHelper
from backend.utils.request_util import get_request_ip
from lunarlink import models, serializers
from lunarlink.utils import response
from lunarlink.utils import prepare
from lunarlink.utils.decorator import request_log
from lunarlink.utils.enums.TreeTypeEnum import TreeType
from lunarlink.utils.query_filters import filter_by_time_range, filter_by_node
from lunarlink.utils.request.generator import CaseGenerator
logger = logging.getLogger(__name__)
class SearchType(Enum):
    API = "2"
    CASE = "1"
tag_options = {
    "冒烟用例": 1,
    "集成用例": 2,
    "监控用例": 3,
    "核心用例": 4,
}
class TestCaseView(GenericViewSet, DestroyModelMixin):
    queryset = models.Case.objects
    serializer_class = serializers.CaseSerializer
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        """
        查询指定CASE列表
        {
            "project": int
            "node": int
        }
        """
        ser = serializers.CaseSearchSerializer(data=request.query_params)
        if ser.is_valid():
            node = ser.validated_data.get("node")
            project = ser.validated_data.get("project")
            search = ser.validated_data.get("search")
            search_type = ser.validated_data.get("searchType")
            case_type = ser.validated_data.get("caseType")
            only_me = ser.validated_data.get("onlyMe")
            creator = ser.validated_data.get("creator")
            start_time = ser.validated_data.get("start_time")
            end_time = ser.validated_data.get("end_time")
            queryset = (
                self.get_queryset().filter(project__id=project).order_by("-create_time")
            )
            # 根据创建时间过滤
            queryset = filter_by_time_range(queryset, start_time, end_time)
            if only_me is True:
                queryset = queryset.filter(creator=request.user.id)
            if creator:
                queryset = queryset.filter(creator__name=creator)
            try:
                queryset = filter_by_node(queryset, project, node, TreeType.CASE.value)
            except RelationNotFound:
                return Response(response.TREE_NOT_EXISTS)
            if case_type != "":
                queryset = queryset.filter(tag=case_type)
            if search != "":
                # 用例名称搜索
                if search_type == SearchType.CASE.value:
                    queryset = queryset.filter(name__contains=search)
                # API名称或API URL搜索
                elif search_type == SearchType.API.value:
                    case_id = self.case_step_search(search)
                    queryset = queryset.filter(pk__in=case_id)
            pagination_query = self.paginate_queryset(queryset)
            serializer = self.get_serializer(pagination_query, many=True)
            return self.get_paginated_response(serializer.data)
        else:
            return Response(ser.errors, status=HTTP_400_BAD_REQUEST)
    @method_decorator(request_log(level="INFO"))
    def post(self, request):
        """
        新增测试用例集
        {
            length: int,
            name: str,
            project: int,
            relation: int,
            tag: str,
            body: [{
                id: int,
                name: str,
                url: str,
                method: str,
                project: int,
                relation: int,
                body: {},
                rig_env: int,
                tag: int,
                tag_name: str,
                update_time: datetime,
                is_deleted: bool,
                creator: str,
            }],
            cases: [{
                case_name: str,
                case_id: str,
            }],
            relation_name: str,
        }
        """
        project_id = int(request.data.pop("project", 0))
        if not models.Project.objects.filter(id=project_id).exists():
            return Response(response.PROJECT_NOT_EXISTS)
        request.data["tag"] = tag_options[request.data["tag"]]
        step_body_list = request.data.pop("body", None)
        # 数据库事务处理
        try:
            with transaction.atomic():
                case_obj = models.Case.objects.create(
                    **request.data,
                    project_id=project_id,
                    creator=request.user,
                )
                prepare.generate_casestep(
                    step_body_list=step_body_list,
                    case_obj=case_obj,
                    creator=request.user,
                )
        except ConfigNotFound:
            return Response(response.CONFIG_NOT_EXISTS)
        except ApiNotFound:
            return Response(response.API_NOT_FOUND)
        except Exception as e:
            logger.error(f"An unexpected error occurred: {e}", exc_info=True)
            return Response({"error": "An unexpected error occurred"}, status=500)
        return Response({"test_id": case_obj.id, **response.CASE_ADD_SUCCESS})
    @staticmethod
    def case_step_search(search: str):
        """
        搜索case_step的url或name
        返回对应的case_id
        """
        case_id = models.CaseStep.objects.filter(
            Q(name__contains=search) | Q(url__contains=search)
        ).values("case_id")
        case_id = set(item["case_id"] for _, item in enumerate(case_id))
        return case_id
    @method_decorator(request_log(level="INFO"))
    def patch(self, request, pk):
        """
        更新测试用例集
        {
            name: str
            id: int
            body: []
            project: int
        }
        """
        step_body_list = request.data.pop("body", None)
        try:
            case_obj = models.Case.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.CASE_NOT_EXISTS)
        try:
            prepare.update_casestep(
                step_body_list=step_body_list,
                case_obj=case_obj,
                creator=request.user,
                updater=request.user.id,
            )
        except ConfigNotFound:
            return Response(response.CONFIG_NOT_EXISTS)
        except ApiNotFound:
            return Response(response.API_NOT_FOUND)
        except CaseStepNotFound:
            return Response(response.CASE_STEP_NOT_EXIST)
        except Exception as e:
            logger.error(f"An unexpected error occurred: {e}", exc_info=True)
            return Response({"error": "An unexpected error occurred"}, status=500)
        request.data["tag"] = tag_options[request.data["tag"]]
        models.Case.objects.filter(id=pk).update(
            **request.data,
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.CASE_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def copy(self, request, pk):
        """复制测试用例
        pk int: test id
        {
            name: test name
            relation: int
            project: int
        }
        """
        try:
            name = request.data["name"]
        except KeyError:
            return Response(response.KEY_MISS)
        username = request.user.id
        if "|" in name:
            resp = self.split(pk, name)
        else:
            try:
                case = models.Case.objects.get(id=pk)
            except ObjectDoesNotExist:
                return Response(response.CASE_NOT_EXISTS)
            case.id = None
            case.name = name
            case.creator = request.user
            case.updater = username
            case.save()
            case_step = models.CaseStep.objects.filter(case__id=pk)
            for step in case_step:
                step.id = None
                step.case = case
                step.creator = request.user
                step.updater = username
                step.save()
            resp = response.CASE_ADD_SUCCESS
        return Response(resp)
    @method_decorator(request_log(level="INFO"))
    def destroy(self, request, pk):
        """
        单个删除
        pk: test id
        """
        case_obj = models.Case.objects.filter(id=pk).first()
        if not case_obj:
            return Response(response.CASE_NOT_EXISTS)
        # description为项目id, args为用例id
        if celery_models.PeriodicTask.objects.filter(
            args__contains=str(case_obj.id)
        ).exists():
            return Response(response.CASE_IS_USED)
        case_obj.is_deleted = True
        case_obj.updater = request.user.id
        case_obj.update_time = timezone.now()
        case_obj.save()  # 这将触发 pre_save 信号,并执行 delete_related_steps 处理器函数
        return Response(response.CASE_DELETE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def bulk_destroy(self, request, *args, **kwargs):
        """
        批量删除用例
        [
            {
                id:int
            }
        ]
        """
        ids = [content["id"] for content in request.data]
        objs = list(models.Case.objects.filter(id__in=ids))
        if not objs:
            return Response(response.CASE_NOT_EXISTS)
        unused_ids = []
        try:
            with transaction.atomic():
                for obj in objs:
                    if celery_models.PeriodicTask.objects.filter(
                        args__contains=str(obj.id)
                    ).exists():
                        continue
                    else:
                        unused_ids.append(obj.id)
                if not unused_ids:
                    return Response(response.CASE_IS_USED)
                # 批量更新待删除的用例
                models.Case.objects.filter(id__in=unused_ids).update(
                    is_deleted=True,
                    update_time=timezone.now(),
                    updater=request.user.id,
                )
                # 批量更新待删除用例的CaseStep
                models.CaseStep.objects.filter(case__in=unused_ids).update(
                    is_deleted=True,
                    update_time=timezone.now(),
                    updater=request.user.id,
                )
        except Exception as e:
            return Response({"error": str(e)}, status=400)
        return Response(response.CASE_DELETE_SUCCESS)
    @staticmethod
    def split(pk, name: str):
        """切割用例
        :param pk: 用例id
        :param name: 用例名称
        :return:
        """
        split_case_name = name.split("|")[0]
        split_condition = name.split("|")[1]
        # 更新原有case长度
        case = models.Case.objects.get(id=pk)
        case_step = models.CaseStep.objects.filter(
            case__id=pk, name__contains=split_condition
        )
        case_step_length = len(case_step)
        case.length -= case_step_length
        case.save()
        new_case = models.Case.objects.filter(name=split_case_name).last()
        if new_case:
            new_case.length += case_step_length
            new_case.save()
            case_step.update(case=new_case)
        else:
            # 创建一条新的case
            case.id = None
            case.name = split_case_name
            case.length = case_step_length
            case.save()
            # 把原来的case_step中的case_id改成新的case_id
            case_step.update(case=case)
        return response.CASE_SPILT_SUCCESS
    @method_decorator(request_log(level="INFO"))
    def move(self, request):
        """
        移动测试用例到其它目录
        """
        project: int = request.data.get("project")
        relation: int = request.data.get("relation")
        cases: list = request.data.get("case")
        ids = [case["id"] for case in cases]
        objs = models.Case.objects.filter(project=project, id__in=ids)
        if objs:
            objs.update(relation=relation)
        else:
            return Response(response.CASE_NOT_EXISTS)
        return Response(response.CASE_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def update_tag(self, request):
        """批量更新用例类型"""
        case_ids: list = request.data.get("case_ids", [])
        project_id: int = request.data.get("project_id", 0)
        tag: list = request.data.get("tag")
        objs = models.Case.objects.filter(project_id=project_id, pk__in=case_ids)
        if objs:
            objs.update(tag=tag)
        else:
            return Response(response.CASE_NOT_EXISTS)
        return Response(response.TAG_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def put(self, request, **kwargs):
        """
        用例步骤同步成功
        将api最新的name, body, url, method更新到用例case_step中
        pk: case_id
        """
        # case_id
        pk = kwargs.get("pk")
        # 在case_step表中找出case_id对应的所有记录,并且排除config
        api_id_list_of_dict = (
            models.CaseStep.objects.filter(case_id=pk)
            .exclude(method="config")
            .values("source_api_id", "step")
        )
        if api_id_list_of_dict:
            # 通过source_api_id找到原来的api
            # 把原来的api的name, body, url, method更新到case_step中
            for item in api_id_list_of_dict:
                source_api_id: int = item["source_api_id"]
                # 不存在api_id的直接跳过
                if source_api_id == 0:
                    continue
                step: int = item["step"]
                source_api = (
                    models.API.objects.filter(pk=source_api_id)
                    .values("name", "body", "url", "method")
                    .first()
                )
                if source_api is not None:
                    models.CaseStep.objects.filter(
                        case_id=pk, source_api_id=source_api_id, step=step
                    ).update(**source_api)
            models.Case.objects.filter(pk=pk).update(update_time=timezone.now())
        else:
            return Response(response.CASE_NOT_EXISTS)
        return Response(response.CASE_STEP_SYNC_SUCCESS)
class CaseStepView(APIView):
    """测试用例step操作视图"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request, **kwargs):
        """
        获取用例集信息
        pk: case_id
        """
        pk = kwargs.get("pk")
        queryset = models.CaseStep.objects.filter(case__id=pk).order_by("step")
        serializer = serializers.CaseStepSerializer(instance=queryset, many=True)
        try:
            case_obj = models.Case.objects.get(id=pk)
        except ObjectDoesNotExist:
            return Response(response.CASE_NOT_EXISTS)
        resp = {
            "case": serializers.CaseSerializer(instance=case_obj, many=False).data,
            "step": serializer.data,
        }
        return Response(resp)
class RecordStartView(APIView):
    """开始录制接口请求"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        regex = request.query_params.get("regex")
        client_ip = request.query_params.get("ip")
        is_local_str = request.query_params.get("local", "true")  # 默认为"true"字符串
        is_local = is_local_str.lower() == "true"  # 转换字符串为布尔值
        user_id = request.user.id
        if not client_ip:
            client_ip = get_request_ip(request)
        record = RedisHelper.get_address_record(client_ip)
        if record:
            record_data = json.loads(record)
            if record_data.get("user_id", "") != user_id:
                return Response(response.RECORD_IS_RUNNING)
        RedisHelper.set_address_record(user_id, client_ip, regex, is_local)
        return Response(response.RECORD_START_SUCCESS)
class RecordStopView(APIView):
    """停止录制接口请求"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        client_ip = request.query_params.get("ip")
        if not client_ip:
            client_ip = get_request_ip(request)
        record = RedisHelper.get_address_record(client_ip)
        if record:
            record_data = json.loads(record)
            RedisHelper.remove_user_record(record_data.get("user_id", ""))
        RedisHelper.remove_address_record(client_ip)
        return Response(response.RECORD_STOP_SUCCESS)
class RecordStatusView(APIView):
    """获取录制接口请求状态"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        client_ip = request.query_params.get("ip")
        user_id = request.user.id
        if not client_ip:
            client_ip = get_request_ip(request)
        address_record = RedisHelper.get_address_record(client_ip)
        user_record = RedisHelper.get_user_record(user_id)
        is_recording = False
        regex = ""
        ip = ""
        is_local = True
        if address_record:
            record_data = json.loads(address_record)
            if record_data.get("user_id", "") == user_id:
                is_recording = True
                regex = record_data.get("regex", "")
                ip = record_data.get("ip", "")
                is_local = record_data.get("local", "")
            else:
                is_recording = False
        if user_record:
            record_data = json.loads(user_record)
            is_recording = True
            regex = record_data.get("regex", "")
            ip = record_data.get("ip", "")
            is_local = record_data.get("local", "")
        data = RedisHelper.list_record_data(user_id)
        return Response(
            dict(results=data, regex=regex, ip=ip, status=is_recording, local=is_local)
        )
class RecordRemoveView(APIView):
    """删除录制数据"""
    @method_decorator(request_log(level="INFO"))
    def get(self, request):
        index = request.query_params.get("index")
        user_id = request.user.id
        RedisHelper.remove_record_data(user_id, index)
        return Response(response.RECORD_REMOVE_SUCCESS)
class GenerateCaseView(APIView):
    """录制生成用例"""
    @method_decorator(request_log(level="INFO"))
    def post(self, request):
        case_name = request.data.get("name")
        length = request.data.get("length")
        project_id = request.data.get("project")
        case_dir = request.data.get("case_dir")
        api_dir = request.data.get("api_dir")
        config = request.data.get("config")
        raw_requests = request.data.get("requests")
        if not raw_requests:
            return Response(response.RECORD_DATA_ERROR)
        if not models.Project.objects.filter(id=project_id).exists():
            return Response(response.PROJECT_NOT_EXISTS)
        config_name = config.get("body").get("name")
        try:
            config_obj = models.Config.objects.get(
                name=config_name, project_id=project_id
            )
        except models.Config.DoesNotExist:
            return Response(response.CONFIG_NOT_EXISTS)
        requests = [RequestInfo(**item) for item in raw_requests]
        CaseGenerator.extract_field(requests)
        record_case, api_instances = CaseGenerator.generate_case(
            length=length,
            project_id=project_id,
            case_dir=case_dir,
            api_dir=api_dir,
            config=config,
            case_name=case_name,
            requests=requests,
            user=request.user.id,
        )
        record_case_dict = record_case.dict()
        step_body_list = record_case_dict.pop("body", None)
        try:
            with transaction.atomic():
                case_obj = models.Case.objects.create(
                    **record_case_dict, creator=request.user
                )
                api_ids = self.save_api_instances(api_instances)
                prepare.generate_record_casestep(
                    api_ids=api_ids,
                    body=step_body_list,
                    case_obj=case_obj,
                    config_obj=config_obj,
                    user=request.user,
                    is_generate_api=bool(api_dir),
                )
        except Exception as e:
            logger.error(f"An unexpected error occurred: {e}", exc_info=True)
            return Response({"error": "An unexpected error occurred"}, status=500)
        return Response({"test_id": case_obj.id, **response.CASE_GENERATOR_SUCCESS})
    @staticmethod
    def save_api_instances(api_instances: List) -> List:
        """
        保存API实例, 提取主键id并返回
        :param api_instances:
        :return:
        """
        api_ids = []
        if api_instances:
            for instance in api_instances:
                instance.save()
                api_ids.append(instance.id)
        return api_ids
class ConvertCaseView(APIView):
    """导入har或其他用例数据文件生成用例"""
    pass
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/variables.py
New file
@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
"""
@File    : variables.py
@Time    : 2023/2/22 10:42
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 全局变量视图
"""
from copy import deepcopy
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.utils import timezone
from rest_framework.viewsets import GenericViewSet
from rest_framework.response import Response
from lunarlink import models, serializers
from lunarlink.utils import response
from lunarlink.utils.decorator import request_log
class VariablesView(GenericViewSet):
    serializer_class = serializers.VariablesSerializer
    queryset = models.Variables.objects
    @method_decorator(request_log(level="DEBUG"))
    def list(self, request):
        """查询全局变量"""
        project = request.query_params.get("project")
        search = request.query_params.get("search")
        queryset = (
            self.get_queryset().filter(project__id=project).order_by("-update_time")
        )
        if search:
            queryset = queryset.filter(
                Q(key__contains=search)
                | Q(value__contains=search)
                | Q(description__contains=search)
            )
        pagination_queryset = self.paginate_queryset(queryset)
        serializer = self.get_serializer(pagination_queryset, many=True)
        return self.get_paginated_response(serializer.data)
    @method_decorator(request_log(level="INFO"))
    def add(self, request):
        """添加全局变量
        {
            key: str
            value: str
            project: int
        }
        """
        ser = self.serializer_class(data=request.data)
        if ser.is_valid():
            try:
                project = models.Project.objects.get(id=request.data["project"])
            except ObjectDoesNotExist:
                return Response(response.PROJECT_NOT_EXISTS)
            if models.Variables.objects.filter(
                key=request.data["key"], project=project
            ).filter():
                return Response(response.VARIABLES_EXISTS)
            request.data["project"] = project
            models.Variables.objects.create(
                **request.data,
                creator=request.user,
            )
            return Response(response.VARIABLES_ADD_SUCCESS)
        else:
            res = deepcopy(response.PROJECT_NOT_EXISTS)
            res["msg"] = ser.errors
            return Response(res)
    @method_decorator(request_log(level="INFO"))
    def update(self, request, pk):
        """更新全局变量
        pk: int - 项目id
        {
            id: int - 变量id
            key: str
            value: str
            description: str
        }
        """
        try:
            variable_id = request.data["id"]
            variable_key = request.data["key"]
            variable_value = request.data["value"]
            variable_description = request.data["description"]
        except KeyError:
            return Response(response.KEY_MISS)
        try:
            variables = models.Variables.objects.get(id=variable_id)
        except ObjectDoesNotExist:
            return Response(response.VARIABLES_NOT_EXISTS)
        if (
            models.Variables.objects.exclude(id=variable_id)
            .filter(
                project_id=pk,
                key=variable_key,
            )
            .first()
        ):
            return Response(response.VARIABLES_EXISTS)
        variables.key = variable_key
        variables.value = variable_value
        variables.description = variable_description
        variables.updater = request.user.id
        variables.update_time = timezone.now()
        variables.save()
        return Response(response.VARIABLES_UPDATE_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def destroy(self, request, pk):
        """
        单个删除
        pk: id
        """
        obj = models.Variables.objects.filter(id=pk)
        if not obj:
            return Response(response.VARIABLES_NOT_EXISTS)
        obj.update(
            is_deleted=True,
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.VARIABLES_DEL_SUCCESS)
    @method_decorator(request_log(level="INFO"))
    def bulk_destroy(self, request):
        """批量删除全局变量
        [{id:int}]
        :param request:
        :return:
        """
        ids = [content["id"] for content in request.data]
        objs = models.Variables.objects.filter(id__in=ids)
        if not objs:
            return Response(response.VARIABLES_NOT_EXISTS)
        objs.update(
            is_deleted=True,
            update_time=timezone.now(),
            updater=request.user.id,
        )
        return Response(response.VARIABLES_DEL_SUCCESS)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/yapi.py
New file
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
@File    : yapi.py
@Time    : 2023/2/15 17:51
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : Yapi视图
"""
import logging
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework.response import Response
from lunarlink import tasks
from lunarlink.utils import response
from lunarlink import models
logger = logging.getLogger(__name__)
class YAPIView(APIView):
    def post(self, request, pk):
        try:
            obj = models.Project.objects.get(pk=pk)
        except ObjectDoesNotExist:
            return Response(response.PROJECT_NOT_EXISTS)
        yapi_token = obj.yapi_openapi_token
        yapi_base_url = obj.yapi_base_url
        task = tasks.async_import_yapi_api.delay(
            yapi_base_url,
            yapi_token,
            pk,
        )
        response.IMPORT_YAPI.update({"task_id": task.id})
        return Response(response.IMPORT_YAPI)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/测试发送.py
New file
@@ -0,0 +1,11 @@
from lunarlink.utils.dingtalk_helper import DingTalkHelper
# 填写你的钉钉机器人 access_token 和 secret
ACCESS_TOKEN = '4625f6690acd9347fae5b3a05af598be63e73d604b933a9b3902425b8f136d4d'
SECRET = 'SEC3b6937550bd297b5491855f6f40c2ff1b41bc8c495e118ba9848742b1ddf8f19'
# 创建 DingTalkHelper 实例
dingtalk_helper = DingTalkHelper(ACCESS_TOKEN, SECRET)
# 发送消息
dingtalk_helper.send_message("你好,这是来自Python的测试消息!")
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/__init__.py
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/admin.py
New file
@@ -0,0 +1,73 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from lunarlink.models import Project
# Register your models here.
User = get_user_model()
@admin.register(User)
class UserAdmin(BaseUserAdmin):
    def accessible_projects(self, obj):
        user_groups = obj.groups.all()
        # 用多对多的关系来过滤出属于用户所在分组的项目。实际应用中可能会有更复杂的逻辑。
        return ", ".join(
            Project.objects.filter(groups__in=user_groups)
            .distinct()
            .values_list("name", flat=True)
        )
    accessible_projects.short_description = "可访问项目"
    list_display = (
        "username",
        "name",
        "is_active",
        "belong_groups",
        "accessible_projects",
    )
    # 编辑资料的时候显示的字段
    fieldsets = (
        (None, {"fields": ("username", "name", "password")}),
        (
            _("Permissions"),
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                ),
            },
        ),
    )
    # 新增用户需要填写的字段
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": (
                    "username",
                    "name",
                    "password1",
                    "password2",
                    "is_active",
                    "is_staff",
                    "groups",
                ),
            },
        ),
    )
    filter_horizontal = ("groups",)
    def belong_groups(self, obj):
        return ", ".join([g.name for g in obj.groups.all()])
    belong_groups.short_description = "所属分组"
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/apps.py
New file
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FastuserConfig(AppConfig):
    name = 'lunaruser'
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/common/response.py
New file
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
@File    : response.py
@Time    : 2023/1/13 11:34
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 响应错误码信息
"""
KEY_MISS = {
    "code": "0100",
    "success": False,
    "msg": "请求数据非法"
}
REGISTER_USERNAME_EXIST = {
    "code": "0101",
    "success": False,
    "msg": "用户名已被注册"
}
REGISTER_EMAIL_EXIST = {
    "code": "0101",
    "success": False,
    "msg": "邮箱已被注册"
}
SYSTEM_ERROR = {
    "code": "9999",
    "success": False,
    "msg": "System Error"
}
REGISTER_SUCCESS = {
    "code": "0001",
    "success": True,
    "msg": "register success"
}
LOGIN_FAILED = {
    "code": "0103",
    "success": False,
    "msg": "用户名或密码错误"
}
USER_NOT_EXISTS = {
    "code": "0104",
    "success": False,
    "msg": "该用户未注册"
}
USER_BLOCKED = {
    "code": "0105",
    "success": False,
    "msg": "用户被禁用"
}
LOGIN_SUCCESS = {
    "code": "0001",
    "success": True,
    "msg": "login success"
}
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0001_initial.py
New file
@@ -0,0 +1,46 @@
# Generated by Django 3.2.1 on 2023-06-15 21:29
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
    initial = True
    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]
    operations = [
        migrations.CreateModel(
            name='MyUser',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('phone', models.CharField(help_text='手机号码', max_length=11, null=True, unique=True, verbose_name='手机号码')),
                ('show_hosts', models.BooleanField(default=False, help_text='是否显示Hosts相关的信息', verbose_name='是否显示Hosts相关的信息')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': 'user',
                'verbose_name_plural': 'users',
                'abstract': False,
            },
            managers=[
                ('objects', django.contrib.auth.models.UserManager()),
            ],
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0002_myuser_name.py
New file
@@ -0,0 +1,18 @@
# Generated by Django 3.2.1 on 2023-11-14 14:48
from django.db import migrations, models
class Migration(migrations.Migration):
    dependencies = [
        ('lunaruser', '0001_initial'),
    ]
    operations = [
        migrations.AddField(
            model_name='myuser',
            name='name',
            field=models.CharField(blank=True, help_text='姓名', max_length=40, null=True, verbose_name='姓名'),
        ),
    ]
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/__init__.py
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/models.py
New file
@@ -0,0 +1,32 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class MyUser(AbstractUser):
    """
    使用AbstractUser可以对User进行扩展使用,添加用户自定义的属性
    """
    phone = models.CharField(
        verbose_name="手机号码",
        unique=True,
        null=True,
        max_length=11,
        help_text="手机号码",
    )
    show_hosts = models.BooleanField(
        verbose_name="是否显示Hosts相关的信息",
        default=False,
        help_text="是否显示Hosts相关的信息",
    )
    name = models.CharField(
        verbose_name="姓名",
        max_length=40,
        blank=True,
        null=True,
        help_text="姓名",
    )
    class Meta(AbstractUser.Meta):
        pass
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py
New file
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""
@File    : serializers.py
@Time    : 2023/1/13 11:34
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 序列化&反序列化
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from lunarlink.models import LoginLog
User = get_user_model()
class UserLoginSerializer(serializers.Serializer):
    """
    用户登录序列化
    """
    username = serializers.CharField(required=True)
    password = serializers.CharField(required=True)
class UserModelSerializer(serializers.ModelSerializer):
    """
    访问统计序列化
    """
    class Meta:
        model = User
        fields = [
            "id",
            "is_superuser",
            "username",
            "name",
            "is_staff",
            "is_active",
            "groups",
        ]
        depth = 1
class LoginLogSerializer(serializers.ModelSerializer):
    """
    登录日志权限-序列化器
    """
    class Meta:
        model = LoginLog
        fields = "__all__"
        read_only_fields = ["id"]
# 在 UserLoginSerializer 下方添加
class ChangePasswordSerializer(serializers.Serializer):
    """
    修改密码序列化
    """
    old_password = serializers.CharField(required=True, max_length=128)
    new_password = serializers.CharField(required=True, min_length=6, max_length=128)
    confirm_password = serializers.CharField(required=True, max_length=128)
    def validate(self, attrs):
        if attrs['new_password'] != attrs['confirm_password']:
            raise serializers.ValidationError("两次输入的新密码不一致")
        return attrs
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/tests.py
New file
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py
New file
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
@File    : urls.py
@Time    : 2023/1/13 16:09
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 接口路径
"""
from django.urls import path
from lunaruser import views
urlpatterns = [
    path("login", views.LoginView.as_view()),
    path("list", views.UserView.as_view()),
    path(
        "login_log",
        views.LoginLogView.as_view({"get": "list"}),
    ),
    path('change_password/', views.ChangePasswordView.as_view(), name='change_password'),
]
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py
New file
@@ -0,0 +1,146 @@
from django.contrib.auth import authenticate, get_user_model, logout
from django.db.models import Q
from django.utils.decorators import method_decorator
from rest_framework_jwt.settings import api_settings
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from backend.utils.auth import MyJWTAuthentication
from backend.utils.request_util import save_login_log
from lunarlink.models import LoginLog
from lunaruser.common import response
from lunaruser import serializers
from lunarlink.utils.decorator import request_log
User = get_user_model()
class LoginView(APIView):
    """
    登录视图,用户名与密码匹配返回token
    """
    authentication_classes = ()
    permission_classes = ()
    @swagger_auto_schema(request_body=serializers.UserLoginSerializer)
    def post(self, request):
        """
        用户名密码一致返回token
        {
            username: str
            password: str
        }
        """
        try:
            username = request.data["username"]
            password = request.data["password"]
        except KeyError:
            return Response(response.KEY_MISS)
        user = authenticate(username=username, password=password)
        if user is None:
            return Response(response.LOGIN_FAILED)
        # 0后面还需要优化定义明确
        if user.is_active == 0:
            return Response(response.USER_BLOCKED)
        # JWT token creation
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        response.LOGIN_SUCCESS.update(
            {
                "id": user.id,
                "user": user.username,
                "name": user.name,
                "is_superuser": user.is_superuser,
                "show_hosts": user.show_hosts,
                "token": token,
            }
        )
        request.user = user
        save_login_log(request=request)
        return Response(response.LOGIN_SUCCESS)
class UserView(APIView):
    def get(self, request):
        users = User.objects.filter(is_active=1)
        ser = serializers.UserModelSerializer(instance=users, many=True)
        return Response(ser.data)
class LoginLogView(GenericViewSet):
    """
    登录日志接口
    list:查询
    """
    queryset = LoginLog.objects.all()
    serializer_class = serializers.LoginLogSerializer
    @method_decorator(request_log(level="DEBUG"))
    def list(self, request):
        search = request.query_params.get("search")
        queryset = self.get_queryset().order_by("-create_time")
        if search:
            queryset = queryset.filter(
                Q(ip__contains=search) | Q(name__contains=search)
            )
        pagination_queryset = self.paginate_queryset(queryset)
        serializer = self.get_serializer(pagination_queryset, many=True)
        return self.get_paginated_response(serializer.data)
# 在 LoginView 下方添加
class ChangePasswordView(APIView):
    authentication_classes = [MyJWTAuthentication]
    permission_classes = [IsAuthenticated]
    @swagger_auto_schema(request_body=serializers.ChangePasswordSerializer)
    def post(self, request):
        serializer = serializers.ChangePasswordSerializer(data=request.data)
        if not serializer.is_valid():
            return Response({
                "code": "0100",
                "success": False,
                "msg": serializer.errors
            }, status=status.HTTP_400_BAD_REQUEST)
        user = request.user
        if not user.check_password(serializer.validated_data['old_password']):
            return Response({
                "code": "0106",
                "success": False,
                "msg": "旧密码不正确"
            }, status=status.HTTP_400_BAD_REQUEST)
        try:
            user.set_password(serializer.validated_data['new_password'])
            user.save()
            # 先执行登出再返回响应
            logout(request)
            return Response({
                "code": "0000",  # 确保前后端使用统一成功码
                "success": True,
                "msg": "密码修改成功,请重新登录"  # 统一提示消息
            })
        except Exception as e:
            return Response({
                "code": "9999",
                "success": False,
                "msg": str(e)
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
测试组/Test_platform/Interface_automation/backend/apps/schema/api_schema.py
New file
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
@File    : api_schema.py
@Time    : 2023/12/14 11:16
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : API数据模板
"""
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class APIBody(BaseModel):
    name: str
    rig_id: int
    times: int
    request: Dict
    desc: Dict
    extract: Optional[List] = []
    check: List = Field([], alias="validate")
class APISchema(BaseModel):
    name: str
    body: APIBody
    url: str
    method: str
    project_id: int
    relation: int
    creator_id: int
测试组/Test_platform/Interface_automation/backend/apps/schema/request.py
New file
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
@File    : request.py
@Time    : 2023/9/8 15:06
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 转化mitmproxy请求和响应数据(request and response data)
"""
import json
from typing import TypeVar
from pydantic import BaseModel
from loguru import logger
body = TypeVar("body", bytes, str)
class RequestInfo(BaseModel):
    url: str
    body: str
    request_method: str
    request_headers: dict
    response_headers: dict
    cookies: dict
    request_cookies: dict
    response_content: str
    status_code: int
    def __init__(self, flow=None, **kwargs):
        if flow:
            kwargs.update(
                dict(
                    status_code=flow.response.status_code,
                    url=flow.request.url,
                    request_method=flow.request.method,
                    request_headers=dict(flow.request.headers),
                    response_headers=dict(flow.response.headers),
                    response_content=self.get_response(flow.response),
                    body=self.get_body(flow.request),
                    cookies=dict(flow.response.cookies),
                    request_cookies=dict(flow.request.cookies),
                )
            )
        super().__init__(**kwargs)
    @classmethod
    def translate_json(cls, request_data):
        """
        将请求数据转换为json格式
        :param request_data: 接口请求数据
        :return: 返回json格式字符串
        """
        try:
            if isinstance(request_data, dict):
                return json.dumps(request_data, indent=4, ensure_ascii=False)
            else:
                return json.dumps(
                    json.loads(request_data), indent=4, ensure_ascii=False
                )
        except (TypeError, json.JSONDecodeError) as e:
            logger.bind(name=None).warning(f"解析json格式失败: {e}")
            return request_data
    @classmethod
    def get_response(cls, response):
        content_type = response.headers.get("Content-Type").lower()
        if "json" in content_type:
            return cls.translate_json(response.text)
        if "text" in content_type or "xml" in content_type:
            return response.text
        return response.data.decode("utf-8")
    @classmethod
    def get_body(cls, request):
        if len(request.content) == 0:
            return ""
        content_type = request.headers.get("Content-Type").lower()
        if "json" in content_type:
            return cls.translate_json(request.text)
        if "text" in content_type or "xml" in content_type:
            return request.text
        if "x-www-form-urlencoded" in content_type:
            return json.dumps(dict(request.urlencoded_form))
        return request.data.decode("utf-8")
    def dumps(self):
        try:
            return self.json(ensure_ascii=False)
        except Exception as e:
            logger.error(f"序列化错误: {e}")
            return None
测试组/Test_platform/Interface_automation/backend/apps/schema/testcase_schema.py
New file
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
@File    : testcase_schema.py
@Time    : 2023/9/11 16:05
@Author  : geekbing
@LastEditTime : 2023/10/09 16:05
@LastEditors : geekbing
@Description : API、Case数据模板
"""
from typing import Dict, List
from pydantic import BaseModel, Field
class HeadersData(BaseModel):
    header: Dict = {}
    desc: Dict = {}
class RequestFormSchema(BaseModel):
    data: Dict = {}
    desc: Dict = {}
class RequestParamsSchema(BaseModel):
    params: Dict = {}
    desc: Dict = {}
class FilesSchema(BaseModel):
    files: Dict = {}
    desc: Dict = {}
class RequestData(BaseModel):
    form: RequestFormSchema = RequestFormSchema()
    params: RequestParamsSchema = RequestParamsSchema()
    files: FilesSchema = FilesSchema()
    json_data: Dict = Field({}, alias="json")
class VariablesData(BaseModel):
    variables: List = []
    desc: Dict = {}
class HooksData(BaseModel):
    setup_hooks: List = []
    teardown_hooks: List = []
class ValidateData(BaseModel):
    check: List[Dict] = Field(
        [{"equals": ["status_code", 200, "默认断言"]}], alias="validate"
    )
class ExtractData(BaseModel):
    extract: List = []
    desc: Dict = {}
class APIBodySchema(BaseModel):
    header: HeadersData = HeadersData()
    request: RequestData = RequestData()
    extract: ExtractData = ExtractData()
    check: ValidateData = Field(ValidateData(), alias="validate")
    variables: VariablesData = VariablesData()
    hooks: HooksData = HooksData()
    name: str = ""
    url: str = ""
    method: str = ""
    times: int = 1
class RecordCaseSchema(BaseModel):
    length: int
    project_id: int
    relation: int
    name: str
    tag: int
    body: List
测试组/Test_platform/Interface_automation/backend/backend/__init__.py
New file
@@ -0,0 +1 @@
测试组/Test_platform/Interface_automation/backend/backend/asgi.py
New file
@@ -0,0 +1,13 @@
"""
ASGI config for backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
application = get_asgi_application()
测试组/Test_platform/Interface_automation/backend/backend/celery.py
New file
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
@File    : mycelery.py.py
@Time    : 2023/3/9 16:16
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 设置环境变量,配置Celery应用程序
"""
import logging
import os
from celery import Celery
from celery.signals import after_setup_logger
from django.conf import settings
# 设置Django的环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
app = Celery("backend")
# 配置Celery应用程序
app.config_from_object("django.conf:settings", namespace="CELERY")
# 自动检测每个已注册应用中的异步任务
app.autodiscover_tasks(settings.INSTALLED_APPS)
app.conf.update(
    CELERY_QUEUES={
        "beat_tasks": {
            "exchange": "beat_tasks",
            "exchange_type": "direct",
            "binding_key": "beat_tasks",
        },
        "work_queue": {
            "exchange": "work_queue",
            "exchange_type": "direct",
            "binding_key": "work_queue",
        },
    },  # 定义任务队列
    CELERY_DEFAULT_QUEUE="work_queue",  # 默认的任务队列
    CELERY_FORCE_EXECV=True,  # 有些情况下可以防止死锁
    task_reject_on_worker_lost=True,  # 任务丢失后拒绝执行
    task_acks_late=True,  # 任务执行完后,不立即删除任务结果
    CELERY_WORKER_CONCURRENCY=2,  # 并发数, 设置得接近于你的CPU核心数,或者稍微高一点
    CELERY_MAX_TASKS_PER_CHILD=50,  # 每个worker最多执行50个任务便自我销毁释放内存
    CELERY_PREFETCH_MULTIPLIER=1,  # 每次从任务队列取任务的数量
    CELERY_ACCEPT_CONTENT=[
        "application/json",  # 指定接受的内容类型
    ],
    CELERY_TASK_SERIALIZER="json",  # 指定任务序列化类型
    CELERY_RESULT_SERIALIZER="json",  # 指定任务结果序列化类型
)
@after_setup_logger.connect
def setup_logger(logger, *args, **kwargs):
    fh = logging.FileHandler("logs/celery.log", "a", encoding="utf-8")
    fh.setLevel(logging.INFO)
    # 再创建一个handler, 用于输出到控制台
    ch = logging.StreamHandler()
    ch.setLevel(logging.INFO)
    # 定义handler的输出格式
    formatter = logging.Formatter(
        "%(asctime)s  %(levelname)s  [pid:%(process)d] [%(name)s %(filename)s->%(funcName)s:%(lineno)s] %(message)s"
    )
    fh.setFormatter(formatter)
    ch.setFormatter(formatter)
    # 给logger添加handler
    logger.addHandler(fh)
    logger.addHandler(ch)
测试组/Test_platform/Interface_automation/backend/backend/settings.py
New file
@@ -0,0 +1,324 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 2.2.17.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import datetime
import os
import sys
from loguru import logger
from conf.env import *
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)
sys.path.insert(0, os.path.join(BASE_DIR, "apps"))
sys.path.insert(0, os.path.join(BASE_DIR, "extra_apps"))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "^v68x$m2x($7!z@8lt548otbgev)@on&tntu3qts^s2z3xx(_a"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = locals().get("DEBUG", True)
ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"])
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Application definition
INSTALLED_APPS = [
    "simpleui",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "drf_yasg",
    "django_celery_beat",
    "django_celery_results",
    "lunaruser",
    "lunarlink",
]
MIDDLEWARE = [
    "backend.utils.middleware.PerformanceAndExceptionLoggerMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.gzip.GZipMiddleware",
    "log_request_id.middleware.RequestIDMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "backend.utils.middleware.VisitTimesMiddleware",
]
ROOT_URLCONF = "backend.urls"
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(BASE_DIR, "./templates")],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
ASGI_APPLICATION = "backend.asgi.application"
WSGI_APPLICATION = "backend.wsgi.application"
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": DATABASE_NAME,
        "USER": DATABASE_USER,
        "PASSWORD": DATABASE_PASSWORD,
        "HOST": DATABASE_HOST,
        "PORT": DATABASE_PORT,
        "OPTIONS": {
            "charset": "utf8mb4",  # 设置字符集为utf8mb4
        },
    }
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = "zh-Hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_L10N = True
USE_TZ = False
# ================================================= #
# *************** REST_FRAMEWORK配置 *************** #
# ================================================= #
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "backend.utils.auth.MyJWTAuthentication",  # 设置全局的默认身份验证方案
    ],
    "UNAUTHENTICATED_USER": None,
    "UNAUTHENTICATED_TOKEN": None,
    # json form 渲染
    # 设置全局默认的解析器集
    "DEFAULT_PARSER_CLASSES": [
        "rest_framework.parsers.JSONParser",  # 解析 JSON 请求内容
        "rest_framework.parsers.FormParser",  # 解析 HTML 表单内容
        "rest_framework.parsers.MultiPartParser",  # 解析多部分HTML表单内容,支持文件上传
        "rest_framework.parsers.FileUploadParser",  # 解析原始文件上传内容
    ],
    "DEFAULT_PAGINATION_CLASS": "backend.utils.pagination.MyPageNumberPagination",  # 默认分页器
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",  # 有经过身份认证确定用户身份才能访问
    ),
}
# ================================================= #
# ****************** simplejwt配置 ***************** #
# ================================================= #
JWT_AUTH = {
    "JWT_EXPIRATION_DELTA": datetime.timedelta(days=1),
    "JWT_ALLOW_REFRESH": True,
}
AUTH_USER_MODEL = "lunaruser.MyUser"
# ====================================#
# ****************swagger************#
# ====================================#
SWAGGER_SETTINGS = {
    "DEFAULT_AUTO_SCHEMA_CLASS": "backend.utils.swagger.CustomSwaggerAutoSchema",
    "SECURITY_DEFINITIONS": {
        "Token": {"type": "apiKey", "name": "Authorization", "in": "header"},
    },
    # 接口文档中方法列表以首字母升序排列
    "APIS_SORTER": "alpha",
    # 如果支持json提交, 则接口文档中包含json输入框
    "JSON_EDITOR": True,
    # 方法列表字母排序
    "OPERATIONS_SORTER": "alpha",
    "VALIDATOR_URL": None,
    "USE_SESSION_AUTH": False,
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = "/django_static/"
# 指定static文件的路径,缺少这个配置,collectstatic无法加载extent.js
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
# collectstatic 之后的文件存放路径
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
for level in ["INFO", "WARNING", "ERROR"]:
    logger.add(
        f"logs/{level.lower()}.log",
        format="{time:YYYY-MM-DD HH:mm:ss.SSS}"
               " [pid:{process} -> thread:{thread.name}]"
               " {level}"
               " [{name}:{function}:{line}]"
               " {message}",
        level=level,
        rotation="00:00",
        retention="14 days",
    )
LOGGING = {
    "version": 1,
    "disable_existing_loggers": True,
    "formatters": {
        "standard": {
            "format": "%(levelname)-2s [%(asctime)s] [%(request_id)s] %(name)s: %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
        "color": {
            "()": "colorlog.ColoredFormatter",
            "format": "%(green)s%(asctime)s [%(request_id)s] %(name)s %(log_color)s%(levelname)s [pid:%(process)d] "
                      "[%(filename)s->%(funcName)s:%(lineno)s] %(cyan)s%(message)s",
            "log_colors": {
                "DEBUG": "black",
                "INFO": "white",
                "WARNING": "yellow",
                "ERROR": "red",
                "CRITICAL": "bold_red",
            },
        }
        # 日志格式
    },
    "filters": {
        "request_id": {"()": "log_request_id.filters.RequestIDFilter"},
        "require_debug_true": {
            "()": "django.utils.log.RequireDebugTrue",  # 过滤器,只有当setting的DEBUG = True时生效
        },
    },
    "handlers": {
        "mail_admins": {
            "level": "ERROR",
            "class": "django.utils.log.AdminEmailHandler",
            "include_html": True,
        },
        "default": {
            "level": "DEBUG",
            "class": "logging.handlers.RotatingFileHandler",
            "filename": os.path.join(BASE_DIR, "logs/info.log"),
            "maxBytes": 1024 * 1024 * 50,
            "backupCount": 5,
            "formatter": "color",
            "filters": ["request_id"],
        },
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "color",
            "filters": ["request_id"],
        },
    },
    "loggers": {
        "django": {
            "handlers": ["default", "console"],
            "level": "INFO",
            "propagate": True,
        },
        "lunarlink": {
            "handlers": ["default", "console"],
            "level": "INFO",
            "propagate": True,
        },
        "httprunner": {
            "handlers": ["default", "console"],
            "level": "INFO",
            "propagate": True,
        },
    },
}
# celery 配置
CELERY_BROKER_URL = MQ_URL  # 消息队列地址
CELERY_RESULT_BACKEND = "django-db"  # celery结果存储到数据库中
CELERY_TIMEZONE = TIME_ZONE  # celery 时区问题
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"  # Backend数据库
DJANGO_CELERY_BEAT_TZ_AWARE = False  # 时区设置
CELERY_ENABLE_UTC = False  # 时区设置
# 设置请求体的最大大小
DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800  # 50M
LOG_REQUEST_ID_HEADER = "HTTP_X_REQUEST_ID"
GENERATE_REQUEST_ID_IF_NOT_IN_HEADER = True
REQUEST_ID_RESPONSE_HEADER = "RESPONSE_HEADER_NAME"
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = []
CORS_ALLOW_METHODS = (
    "DELETE",
    "GET",
    "OPTIONS",
    "PATCH",
    "POST",
    "PUT",
    "VIEW",
)
CORS_ALLOW_HEADERS = (
    "accept",
    "accept-encoding",
    "authorization",
    "content-type",
    "dnt",
    "origin",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
    "Project",
)
测试组/Test_platform/Interface_automation/backend/backend/test.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : test.py
@Time    : 2023/1/13 11:08
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/backend/urls.py
New file
@@ -0,0 +1,62 @@
"""backend URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include
from django.urls import path
from django.urls import re_path
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework_jwt.views import obtain_jwt_token
schema_view = get_schema_view(
    openapi.Info(
        title="Snippets API",
        default_version="v1",
        description="测试平台接口文档",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@snippets.local"),
        license=openapi.License(name="BSD License"),
    ),
    public=True,
    permission_classes=[
        permissions.AllowAny,
    ],
    authentication_classes=[],
)
urlpatterns = [
    path("login/", obtain_jwt_token),
    path("admin/", admin.site.urls),
    path("api/user/", include("lunaruser.urls")),
    path("api/lunarlink/", include("lunarlink.urls")),
    # swagger
    re_path(
        r"^swagger(?P<format>\.json|\.yaml)$",
        schema_view.without_ui(cache_timeout=0),
        name="schema-json",
    ),
    re_path(
        r"^swagger/$",
        schema_view.with_ui("swagger", cache_timeout=0),
        name="schema-swagger-ui",
    ),
    re_path(
        r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"
    ),
]
测试组/Test_platform/Interface_automation/backend/backend/utils/__init__.py
New file
@@ -0,0 +1 @@
测试组/Test_platform/Interface_automation/backend/backend/utils/auth.py
New file
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""
@File    : auth.py
@Time    : 2023/1/13 15:06
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 登录认证
"""
import jwt
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_get_username_from_payload
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class MyJWTAuthentication(JSONWebTokenAuthentication):
    def authenticate(self, request):
        """
        Returns a two-tuple of `User` and token if a valid signature has been
        supplied using JWT-based authentication.  Otherwise, returns `None`.
        """
        jwt_value = request.META.get("HTTP_AUTHORIZATION", None)
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = "签名过期"
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = "签名解析失败"
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
        user = self.authenticate_credentials(payload)
        return user, jwt_value
    def authenticate_credentials(self, payload):
        """
        Returns an active user that matches the payload's user id and email.
        """
        User = get_user_model()
        username = jwt_get_username_from_payload(payload)
        if not username:
            msg = _("Invalid payload.")
            raise exceptions.AuthenticationFailed(msg)
        try:
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            msg = "用户不存在"
            raise exceptions.AuthenticationFailed(msg)
        if not user.is_active:
            msg = "用户已禁用"
            raise exceptions.AuthenticationFailed(msg)
        return user
测试组/Test_platform/Interface_automation/backend/backend/utils/middleware.py
New file
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
"""
@File    : middleware.py
@Time    : 2023/3/20 15:14
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 记录用户访问网站的行为和数据,并存入数据库
"""
import logging
import time
import traceback
from rest_framework.response import Response
from sentry_sdk import capture_exception
from lunarlink.models import Visit
from lunarlink.utils import qy_message
logger = logging.getLogger(__name__)
class VisitTimesMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        self.process_request(request)
        response = self.get_response(request)
        return self.process_response(request, response)
    def process_request(self, request):
        # 复制一份body的内容,因为原生的body不能被多次访问
        request._body = request.body
    def process_response(self, request, response):
        body = request._body
        if body == b"":
            body = ""
        else:
            body = str(body, encoding="utf-8")
        if request.user is None:
            # 报告页面不需要登录,获取不到用户名
            user = "AnonymousUser"
        else:
            user = request.user
        ip: str = request.META.get(
            "HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR")
        )
        # 前端请求头没传project, 就默认为0
        project = request.META.get("HTTP_PROJECT", 0)
        url: str = request.path
        # 去除测试报告页字体相关的访问
        if "/fonts/roboto/" in url:
            return response
        if request.GET != {}:
            query_params = "?"
            # <QueryDict: {'page': ['1'], 'node': [''], 'project': ['11'], 'search': [''], 'tag': ['']}>
            for k, v in request.GET.items():
                query_params += f"{k}={v}&"
            url += query_params[:-1]
        else:
            query_params = ""
        Visit.objects.create(
            user=user,
            url=url,
            request_method=request.method,
            request_body=body,
            ip=ip.split(",")[0],  # 有时候会有多个ip,取第一个
            path=request.path,
            request_params=query_params[1:-1],
            project=project,
        )
        return response
class PerformanceAndExceptionLoggerMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.
    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        start_time = time.time()
        response = self.get_response(request)
        duration = time.time() - start_time
        response["X-Page-Duration-ms"] = int(duration * 1000)
        logger.info(
            "duration:%s url:%s parameters:%s",
            duration,
            request.path,
            request.GET.dict(),
        )
        # Code to be executed for each request/response after
        # the view is called.
        return response
    def process_exception(self, request, exception):
        if exception:
            message_str = f"url: {request.build_absolute_uri()} ** msg: {repr(exception)} ````{traceback.format_exc()}````"
            message_dict = {
                "url": request.build_absolute_uri(),
                "msg": repr(exception),
                "traceback": traceback.format_exc(),
            }
            logger.warning(message_str)
            # send WeChat Work message
            qy_message.send(msg=message_dict)
            # capture exception to sentry:
            capture_exception(exception)
        return Response(
            "Error processing the request, please contact the system administrator.",
            status=500,
        )
测试组/Test_platform/Interface_automation/backend/backend/utils/pagination.py
New file
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
@File    : pagination.py
@Time    : 2023/1/13 16:06
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 分页查询
"""
from rest_framework import pagination
class MyCursorPagination(pagination.CursorPagination):
    """
    Cursor 光标分页 性能高 安全
    """
    page_size = 10
    ordering = "-create_time"
    page_size_query_param = "pages"
    max_page_size = 40
class MyPageNumberPagination(pagination.PageNumberPagination):
    """
    普通分页,数据量越大性能越差
    """
    page_size = 10
    page_size_query_param = "size"
    page_query_param = "page"
    max_page_size = 40
测试组/Test_platform/Interface_automation/backend/backend/utils/permissions.py
New file
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
@File    : permissions.py
@Time    : 2023/8/23 14:46
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 自定义权限类
"""
from rest_framework import permissions
from rest_framework.permissions import BasePermission, IsAdminUser
from lunarlink import models
# class HasProjectAccess(BasePermission):
#     def has_object_permission(self, request, view, obj):
#         # 检查请求的对象是否属于用户的组
#         return obj.groups.filter(id__in=request.user.groups.all()).exists()
class HasProjectAccess(BasePermission):
    message = "您没有执行此操作的权限"
    def has_permission(self, request, view):
        # 如果用户是超级管理员,允许访问所有项目
        if request.user.is_superuser:
            return True
        # 获取当前用户所在的所有分组
        user_groups = request.user.groups.all()
        # 获取请求参数中的项目ID
        project_id = view.kwargs.get("pk") or request.query_params.get("project")
        # 如果项目ID存在,检查项目是否属于用户的分组
        if project_id:
            return models.Project.objects.filter(
                pk=project_id, groups__in=user_groups
            ).exists()
        # 如果没有项目ID且视图方法是 'list' 或 'create',则不需要进一步检查,因为 get_queryset 已经做了筛选
        if view.action in ["list", "add"]:
            return True
        # 对于其他方法,可能需要检查对象级的权限
        return False
class IsCreatorOrReadOnly(BasePermission):
    """
    自定义权限,确保只有对象的创建者才可以修改或删除对象。
    """
    def has_object_permission(self, request, view, obj):
        # 对于GET、HEAD或OPTIONS请求,任何请求都允许读取权限,因此我们始终允许这些请求。
        if request.method in permissions.SAFE_METHODS:
            return True
        # 判断请求的用户是否是对象的创建者。
        return obj.creator == request.user
class CustomIsAdminUser(IsAdminUser):
    message = "您没有执行此操作的权限"
测试组/Test_platform/Interface_automation/backend/backend/utils/redis_manager.py
New file
@@ -0,0 +1,469 @@
# -*- coding: utf-8 -*-
"""
@File    : redis_manager.py
@Time    : 2023/9/7 18:42
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : redis客户端Manager
"""
import asyncio
import functools
import inspect
import json
import pickle
from random import Random
from typing import Tuple
from awaits.awaitable import awaitable
from loguru import logger
from redis import ConnectionPool, StrictRedis
from rediscluster import RedisCluster, ClusterConnectionPool
from backend import settings
from apps.exceptions.error import RedisError
class RedisManager:
    """非线程安全,可能存在问题"""
    _cluster_pool = dict()
    _pool = dict()
    @property
    def client(self):
        pool = ConnectionPool(
            host=settings.REDIS_HOST,
            port=settings.REDIS_PORT,
            db=settings.REDIS_DB,
            max_connections=100,
            password=settings.REDIS_PASSWORD,
            decode_responses=True,
        )
        return StrictRedis(connection_pool=pool, decode_responses=True)
    @staticmethod
    def delete_client(redis_id: int, cluster: bool):
        """
        根据redis_id和是否是集群删除客户端
        :param redis_id:
        :param cluster:
        :return:
        """
        if cluster:
            RedisManager._cluster_pool.pop(redis_id)
        else:
            RedisManager._pool.pop(redis_id)
    @staticmethod
    def get_cluster_client(redis_id: int, address: str):
        """
        获取redis集群客户端
        :param redis_id:
        :param address:
        :return:
        """
        cluster = RedisManager._cluster_pool.get(redis_id)
        if cluster is not None:
            return cluster
        client = RedisManager.get_cluster(address)
        RedisManager._cluster_pool[redis_id] = client
        return client
    @staticmethod
    def get_single_node_client(redis_id: int, address: str, password: str, db: int):
        """
        获取redis单实例客户端
        :param redis_id:
        :param address:
        :param password:
        :param db:
        :return:
        """
        node = RedisManager._pool.get(redis_id)
        if node is not None:
            return node
        if ":" not in address:
            raise Exception("redis连接未包含端口号,请检查配置")
        host, port = address.split(":")
        pool = ConnectionPool(
            host=host,
            port=port,
            db=db,
            max_connections=100,
            password=password,
            decode_responses=True,
        )
        client = StrictRedis(connection_pool=pool)
        RedisManager._pool[redis_id] = client
        return client
    @staticmethod
    def refresh_redis_client(redis_id: int, address: str, password: str, db: str):
        """
        刷新redis客户端
        :param redis_id:
        :param address:
        :param password:
        :param db:
        :return:
        """
        host, port = address.split(":")
        pool = ConnectionPool(
            host=host,
            port=port,
            db=db,
            max_connections=100,
            password=password,
            decode_responses=True,
        )
        client = StrictRedis(connection_pool=pool, decode_responses=True)
        RedisManager._pool[redis_id] = client
    @staticmethod
    def refresh_redis_cluster(redis_id: int, addr: str):
        RedisManager._cluster_pool[redis_id] = RedisManager.get_cluster(addr)
    @staticmethod
    def get_cluster(address: str):
        """
        获取集群连接池
        :param address:
        :return:
        """
        try:
            nodes = address.split(",")
            startup_nodes = [
                {"host": n.split(":")[0], "port": n.split(":")[1]}
                for n in nodes
                if ":" in n
            ]
            if len(startup_nodes) == 0:
                raise Exception("找不到集群节点,请检查配置")
            pool = ClusterConnectionPool(
                startup_nodes=startup_nodes, max_connections=100, decode_responses=True
            )
            client = RedisCluster(connection_pool=pool, decode_responses=True)
            return client
        except Exception as e:
            raise RedisError(f"获取Redis连接失败, {e}")
class RedisHelper:
    prefix = "fastapi"
    redis_client = RedisManager().client
    @staticmethod
    @awaitable
    def execute_command(client, command, *args, **kwargs):
        return client.execute_command(command, *args, **kwargs)
    @staticmethod
    @awaitable
    def ping():
        """
        test redis client
        :return:
        """
        return RedisHelper.redis_client.ping()
    @staticmethod
    def get_address_record(address: str):
        """
        获取ip是否已经开启录制
        :param address:
        :return:
        """
        key = RedisHelper.get_key(f"record:ip:{address}")
        return RedisHelper.redis_client.get(key)
    @staticmethod
    def get_user_record(user_id: str):
        """
        获取当前用户是否已开启录制
        :param user_id:
        :return:
        """
        key = RedisHelper.get_key(f"user:id:{user_id}")
        return RedisHelper.redis_client.get(key)
    @staticmethod
    @awaitable
    def cache_record(user_id: str, request):
        """
        缓存录制数据
        :param user_id: 开启录制的用户id
        :param request: 客户端请求流量
        :return:
        """
        key = RedisHelper.get_key(f"id:{user_id}:requests")
        RedisHelper.redis_client.rpush(key, request)
        ttl = RedisHelper.redis_client.ttl(key)
        if ttl < 0:
            RedisHelper.redis_client.expire(key, 3600)
    @staticmethod
    def set_address_record(
        user_id: int,
        address: str,
        regex: str,
        is_local: bool,
    ):
        """
        设置录制状态
        :param user_id:
        :param address:
        :param regex: 录制的url正则
        :param is_local: False: 其他端录制,True: 本机录制
        :return:
        """
        # 默认录制数据保存1小时
        value = json.dumps(
            {
                "user_id": user_id,
                "regex": regex,
                "ip": address,
                "local": is_local,
            },
            ensure_ascii=False,
        )
        RedisHelper.redis_client.set(
            RedisHelper.get_key(f"record:ip:{address}"), value, ex=3600
        )
        RedisHelper.redis_client.set(
            RedisHelper.get_key(f"user:id:{user_id}"), value, ex=3600
        )
        # 清除上次录制数据
        RedisHelper.redis_client.delete(RedisHelper.get_key(f"id:{user_id}:requests"))
    @staticmethod
    def remove_address_record(address: str):
        """
        停止录制任务
        :param address:
        :return:
        """
        return RedisHelper.redis_client.delete(
            RedisHelper.get_key(f"record:ip:{address}")
        )
    @staticmethod
    def remove_user_record(user_id: str):
        """
        停止录制任务
        :param user_id:
        :return:
        """
        return RedisHelper.redis_client.delete(
            RedisHelper.get_key(f"user:id:{user_id}")
        )
    @staticmethod
    def list_record_data(user_id: str):
        """
        获取录制数据
        :param user_id: 开启录制的用户id
        :return:
        """
        key = RedisHelper.get_key(f"id:{user_id}:requests")
        data = RedisHelper.redis_client.lrange(key, 0, -1)
        return [json.loads(x) for x in data]
    @staticmethod
    def remove_record_data(user_id: str, index: int):
        """
        删除录制数据
        :param user_id: 开启录制的用户id
        :param index:
        :return:
        """
        key = RedisHelper.get_key(f"id:{user_id}:requests")
        RedisHelper.redis_client.lset(key, index, "DELETED")
        RedisHelper.redis_client.lrem(key, 1, "DELETED")
    @staticmethod
    def async_delete_prefix(key: str):
        """
        根据前缀删除数据
        :param key:
        :return:
        """
        for k in RedisHelper.redis_client.scan_iter(f"{key}*"):
            RedisHelper.redis_client.delete(k)
            logger.bind(name=None).debug(f"delete redis key: {k}")
    @staticmethod
    def delete_prefix(key: str):
        """
        根据前缀删除数据
        :param key:
        :return:
        """
        for k in RedisHelper.redis_client.scan_iter(f"{key}*"):
            RedisHelper.redis_client.delete(k)
            logger.bind(name=None).debug(f"delete redis key: {k}")
    @staticmethod
    def get_key(_redis_key: str, args_key: bool = True, *args, **kwargs):
        if not args_key:
            return f"{RedisHelper.prefix}:{_redis_key}"
        filter_args = [
            a
            for a in args
            if not str(a).startswith(("<class", "<sqlalchemy", "(<sqlalchemy"))
        ]
        for v in kwargs.values():
            if v and not str(v).startswith(("<class", "<sqlalchemy", "(<sqlalchemy")):
                filter_args.append(str(v))
        return (
            f"{RedisHelper.prefix}:{_redis_key}"
            f"{':' + ':'.join(str(a) for a in filter_args) if len(filter_args) > 0 else ''}"
        )
    @staticmethod
    def get_key_with_suffix(cls_name: str, key: str, args: tuple, key_suffix):
        filter_args = [a for a in args if not str(args[0]).startswith("<class")]
        suffix = key_suffix(filter_args)
        return f"{RedisHelper.prefix}:{cls_name}:{key}:{suffix}"
    @staticmethod
    def cache(key: str, expired_time=30 * 60, args_key=True):
        """
        自动缓存装饰器
        :param args_key:
        :param key: 被缓存的key
        :param expired_time: 默认key过期时间
        :return:
        """
        def decorator(func):
            # 缓存已存在
            if asyncio.iscoroutinefunction(func):
                @functools.wraps(func)
                def wrapper(*args, **kwargs):
                    # TODO: redis重复代码优化
                    if not settings.REDIS_ON:
                        return func(*args, **kwargs)
                    cls_name = (
                        inspect.getframeinfo(inspect.currentframe().f_back)[3][0]
                        .split(".")[0]
                        .split(" ")[-1]
                    )
                    redis_key = RedisHelper.get_key(
                        f"{cls_name}:{key}", args_key, *args, **kwargs
                    )
                    data = RedisHelper.redis_client.get(redis_key)
                    # 缓存已存在
                    if data is not None:
                        return pickle.loads(bytes.fromhex(data))
                    # 获取最新数据
                    new_data = func(*args, **kwargs)
                    info = pickle.dumps(new_data)
                    # logger.bind(name=None).debug(f"set redis key: {redis_key}")
                    RedisHelper.redis_client.set(redis_key, info.hex(), ex=expired_time)
                    return new_data
                return wrapper
            else:
                @functools.wraps(func)
                def wrapper(*args, **kwargs):
                    if not settings.REDIS_ON:
                        return func(*args, **kwargs)
                    cls_name = (
                        inspect.getframeinfo(inspect.currentframe().f_back)[3][0]
                        .split(".")[0]
                        .split(" ")[-1]
                    )
                    redis_key = RedisHelper.get_key(
                        f"{cls_name}:{key}", args_key, *args, **kwargs
                    )
                    data = RedisHelper.redis_client.get(redis_key)
                    # 缓存已存在
                    if data is not None:
                        return pickle.loads(bytes.fromhex(data))
                    # 获取最新数据
                    new_data = func(*args, **kwargs)
                    info = pickle.dumps(new_data)
                    # logger.bind(name=None).debug(f"set redis key: {redis_key}")
                    # 添加随机数防止缓存雪崩
                    RedisHelper.redis_client.set(
                        redis_key,
                        info.hex(),
                        ex=expired_time + Random().randint(10, 59),
                    )
                    return new_data
                return wrapper
        return decorator
    @staticmethod
    def up_cache(*key: str, key_and_suffix: Tuple = None):
        """
        redis缓存key,套了此方法,会自动执行更新数据操作后删除缓存
        :param key:
        :param key_and_suffix: 要删除的key和key组成规则
        :return:
        """
        def decorator(func):
            if asyncio.iscoroutinefunction(func):
                @functools.wraps(func)
                def wrapper(*args, **kwargs):
                    new_data = func(*args, **kwargs)
                    if not settings.REDIS_ON:
                        return new_data
                    cls_name = (
                        inspect.getframeinfo(inspect.currentframe().f_back)[3][0]
                        .split(".")[0]
                        .split(" ")[-1]
                    )
                    for k in key:
                        redis_key = f"{RedisHelper.prefix}:{cls_name}:{k}"
                        RedisHelper.async_delete_prefix(redis_key)
                    if key_and_suffix is not None:
                        current_key = RedisHelper.get_key_with_suffix(
                            cls_name, key_and_suffix[0], args, key_and_suffix[1]
                        )
                        RedisHelper.redis_client.delete(current_key)
                    # 更新数据,删除缓存
                    return new_data
                return wrapper
            else:
                @functools.wraps(func)
                def wrapper(*args, **kwargs):
                    new_data = func(*args, **kwargs)
                    if not settings.REDIS_ON:
                        return new_data
                    cls_name = (
                        inspect.getframeinfo(inspect.currentframe().f_back)[3][0]
                        .split(".")[0]
                        .split(" ")[-1]
                    )
                    for k in key:
                        redis_key = f"{RedisHelper.prefix}:{cls_name}:{k}"
                        RedisHelper.delete_prefix(redis_key)
                    if key_and_suffix is not None:
                        current_key = RedisHelper.get_key_with_suffix(
                            cls_name, key_and_suffix[0], args, key_and_suffix[1]
                        )
                        RedisHelper.redis_client.delete(current_key)
                    return new_data
                return wrapper
        return decorator
测试组/Test_platform/Interface_automation/backend/backend/utils/request_util.py
New file
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""
@File    : request_util.py
@Time    : 2024/1/15 10:15
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : Request工具类
"""
import logging
from typing import Dict
import requests
from django.conf import settings
from user_agents import parse
from lunarlink.models import LoginLog
logger = logging.getLogger(__name__)
def get_request_ip(request):
    """
    获取请求IP
    :param request:
    :return:
    """
    x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
    if x_forwarded_for:
        # X-Forwarded-For 可能有一个代理链,因此这里取第一个即客户端的真实 IP
        ip = x_forwarded_for.split(",")[0].strip()
        return ip
    ip = request.META.get("REMOTE_ADDR", "") or getattr(request, "request_ip", None)
    return ip or "unknown"
def get_ip_analysis(ip) -> Dict:
    """
    获取IP解析数据
    :param ip: ip地址
    :return:
    """
    data = {
        "continent": "",
        "country": "",
        "province": "",
        "city": "",
        "district": "",
        "isp": "",
        "area_code": "",
        "country_english": "",
        "country_code": "",
        "longitude": "",
        "latitude": "",
    }
    if ip != "unknown" and ip:
        if getattr(settings, "ENABLE_LOGIN_ANALYSIS_LOG", True):
            # url = f"https://ip-api.com/json/{ip}?lang=zh-CN"
            try:
                res = requests.get(
                    url="https://ip.django-vue-admin.com/ip/analysis",
                    params={"ip": ip},
                    timeout=5,
                )
                if res.status_code == 200:
                    res_data = res.json()
                    if res_data.get("code") == 0:
                        data = res_data.get("data")
            except Exception as e:
                logger.error(e)
    return data
def get_browser(request):
    """
    获取浏览器
    :param request:
    :return:
    """
    ua_string = request.META["HTTP_USER_AGENT"]
    user_agent = parse(ua_string)
    return user_agent.get_browser()
def get_os(request):
    """
    获取操作系统
    :param request:
    :return:
    """
    ua_string = request.META["HTTP_USER_AGENT"]
    user_agent = parse(ua_string)
    return user_agent.get_os()
def save_login_log(request):
    """
    保存登录日志
    :param request: 请求对象
    :return:
    """
    ip = get_request_ip(request=request)
    # 过滤掉平台自身的服务器 IP 地址
    platform_ips = ["127.0.0.1", "47.119.28.171"]
    # 如果请求来自平台自身的 IP,跳过记录日志
    if ip in platform_ips:
        return
    analysis_data = get_ip_analysis(ip=ip)
    analysis_data.update(
        {
            "ip": ip,
            "username": request.user.username,
            "name": request.user.name,
            "agent": str(parse(request.META.get("HTTP_USER_AGENT", ""))),
            "browser": get_browser(request=request),
            "os": get_os(request=request),
            "creator_id": request.user.id,
        }
    )
    LoginLog.objects.create(**analysis_data)
测试组/Test_platform/Interface_automation/backend/backend/utils/swagger.py
New file
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""
@File    : swagger.py
@Time    : 2023/1/13 18:22
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from drf_yasg.inspectors import SwaggerAutoSchema
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
    def get_tags(self, operation_keys=None):
        tags = super().get_tags(operation_keys)
        if "api" in tags and len(operation_keys) >= 3:
            # `operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create']
            if operation_keys[2].startswith('run'):
                tags[0] = 'run'
            else:
                tags[0] = operation_keys[2]
        return tags
测试组/Test_platform/Interface_automation/backend/backend/utils/ws_connection_manager.py
New file
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""
@File    : ws_connection_manager.py
@Time    : 2023/9/12 17:09
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import asyncio
import logging
import json
from typing import TypeVar
from channels.generic.websocket import AsyncWebsocketConsumer
logger = logging.getLogger(__name__)
MsgType = TypeVar("MsgType", str, dict, bytes)
class ConnectionManager(AsyncWebsocketConsumer):
    active_connections = {}
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user_id = None  # 初始化 user_id 属性
    async def connect(self):
        self.user_id = int(self.scope["url_route"]["kwargs"]["user_id"])
        exist = self.active_connections.get(self.user_id)
        if exist:
            await self.close()
        else:
            self.active_connections[self.user_id] = self
            logger.debug(f"websocket: 用户[{self.user_id}]建立连接成功!")
        await self.accept()
        # asyncio.create_task(self.send_heartbeat())
    async def disconnect(self, close_code):
        if self.user_id is not None:
            del self.active_connections[self.user_id]
            logger.debug(f"websocket: 用户[{self.user_id}] 已安全断开!")
    async def receive(self, text_data=None, bytes_data=None):
        if text_data:
            data = json.loads(text_data)
            message_type = data.get("type")
            if message_type in self.questions_and_answers_map:
                response = self.questions_and_answers_map[message_type].format(
                    user_id=self.user_id
                )  # 格式化字符串以包含 user_id
                await self.send_personal_message(
                    self.user_id, response
                )  # 使用 self.user_id
            else:
                # 添加其他消息类型的处理逻辑
                pass
        elif bytes_data:
            # 在这里添加你的二进制数据处理逻辑
            pass
    @staticmethod
    async def pusher(connection: AsyncWebsocketConsumer, message: MsgType) -> None:
        if isinstance(message, str):
            await connection.send(text_data=message)
        elif isinstance(message, dict):
            await connection.send(text_data=json.dumps(message))
        elif isinstance(message, bytes):
            await connection.send(bytes_data=message)
        else:
            raise TypeError(f"websocket不能发送{type(message)}的内容!")
    @classmethod
    async def send_personal_message(cls, user_id: int, message: MsgType) -> None:
        """
        发送个人信息
        """
        connection = cls.active_connections.get(user_id)
        if connection:
            await cls.pusher(connection, message)
    @classmethod
    async def send_data(cls, user_id, msg_type, record_msg):
        msg = dict(type=msg_type, record_msg=record_msg)
        await cls.send_personal_message(user_id, msg)
    questions_and_answers_map = {
        "HELLO SERVER": "Hello {user_id}",
        "HEARTBEAT": "{user_id}",
    }
    async def send_heartbeat(self):
        while True:
            await self.send_personal_message(self.user_id, {"type": 3})
            await asyncio.sleep(60)
# ws_manage = ConnectionManager()
测试组/Test_platform/Interface_automation/backend/backend/wsgi.py
New file
@@ -0,0 +1,16 @@
"""
WSGI config for backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_wsgi_application()
测试组/Test_platform/Interface_automation/backend/conf/docker.py
New file
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""
@File    : docker.py
@Time    : 2023/6/29 15:53
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : docker部署配置
"""
import os
# ================================================= #
# ************** mysql数据库 配置  ************** #
# ================================================= #
# 数据库地址
DATABASE_HOST = os.getenv("DATABASE_HOST")
# 数据库端口
DATABASE_PORT = os.getenv("DATABASE_PORT", 3306)
# 数据库用户名
DATABASE_USER = os.getenv("DATABASE_USER")
# 数据库密码
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD")
# 数据库名
DATABASE_NAME = os.getenv("DATABASE_NAME")
# ================================================= #
# ************** RabbitMQ配置 ************** #
# ================================================= #
MQ_USER = os.getenv("MQ_USER")
MQ_PASSWORD = os.getenv("MQ_PASSWORD")
MQ_HOST = os.getenv("MQ_HOST")
MQ_PORT = os.getenv("MQ_PORT")
MQ_URL = f"amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:{MQ_PORT}//"
# ================================================= #
# ************** Redis配置 ************** #
# ================================================= #
REDIS_ON = os.getenv("REDIS_ON", "True") == "True"
REDIS_HOST = os.getenv("REDIS_HOST")
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
REDIS_PORT = os.getenv("REDIS_PORT")
REDIS_DB = os.getenv("REDIS_DB")
# ================================================= #
# ************** 其他 配置  ************** #
# ================================================= #
DEBUG = False  # 线上环境请设置为False
# 启动登录日志记录(通过调用api获取ip详细地址。如果是内网,关闭即可)
ENABLE_LOGIN_ANALYSIS_LOG = True
ALLOWED_HOSTS = ["*"]
BASE_REPORT_URL = os.getenv("BASE_REPORT_URL")  # 替换成部署的服务器地址
IM_REPORT_SETTING = {
    "base_url": os.getenv("IM_REPORT_BASE_URL"),
    "port": os.getenv("IM_REPORT_PORT", 8081),
    "report_title": os.getenv("IM_REPORT_TITLE"),
}
# ================================================= #
# ************** 监控告警企微机器人配置  ************** #
# ================================================= #
QY_WEB_HOOK = os.getenv("QY_WEB_HOOK")  # 企微机器人webhook地址
# ================================================= #
# ************** 发送邮件配置  ************** #
# ================================================= #
# 使用 SMTP 服务器发送邮件
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND")
# SMTP 服务器地址
EMAIL_HOST = os.getenv("EMAIL_HOST")
# SMTP 服务器端口
EMAIL_PORT = os.getenv("EMAIL_PORT")
# 发件人邮箱账号
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
# 发件人邮箱密码
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
# 是否使用 TLS
EMAIL_USE_TLS = False
# 是否使用 SSL
EMAIL_USE_SSL = True
# ================================================= #
# ************** 录制流量代理配置  ************** #
# ================================================= #
# PROXY Server
PROXY_ON = os.getenv("PROXY_ON", "True") == "True"  # 是否开启代理
PROXY_PORT = int(os.getenv("PROXY_PORT"))
测试组/Test_platform/Interface_automation/backend/conf/env.py
New file
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
"""
@File    : env.py
@Time    : 2023/6/29 15:53
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 本地开发配置
"""
import os
from dotenv import load_dotenv
load_dotenv()
# ================================================= #
# ************** mysql数据库 配置  ************** #
# ================================================= #
# 数据库地址
DATABASE_HOST = os.getenv("DATABASE_HOST")
# 数据库端口
DATABASE_PORT = os.getenv("DATABASE_PORT", 3306)
# 数据库用户名
DATABASE_USER = os.getenv("DATABASE_USER")
# 数据库密码
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD")
# 数据库名
DATABASE_NAME = os.getenv("DATABASE_NAME")
# ================================================= #
# ************** RabbitMQ配置 ************** #
# ================================================= #
MQ_USER = os.getenv("MQ_USER")
MQ_PASSWORD = os.getenv("MQ_PASSWORD")
MQ_HOST = os.getenv("MQ_HOST")
MQ_PORT = os.getenv("MQ_PORT")
MQ_URL = f"amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:{MQ_PORT}//"
# ================================================= #
# ************** Redis配置 ************** #
# ================================================= #
REDIS_ON = os.getenv("REDIS_ON", "True") == "True"
REDIS_HOST = os.getenv("REDIS_HOST")
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
REDIS_PORT = os.getenv("REDIS_PORT")
REDIS_DB = os.getenv("REDIS_DB")
# ================================================= #
# ************** 其他 配置  ************** #
# ================================================= #
DEBUG = True  # 线上环境请设置为False
# 启动登录日志记录(通过调用api获取ip详细地址。如果是内网,关闭即可)
ENABLE_LOGIN_ANALYSIS_LOG = True
ALLOWED_HOSTS = ["*"]
BASE_REPORT_URL = os.getenv("BASE_REPORT_URL")
IM_REPORT_SETTING = {
    "base_url": os.getenv("IM_REPORT_BASE_URL"),
    "port": os.getenv("IM_REPORT_PORT", 8000),
    "report_title": os.getenv("IM_REPORT_TITLE")
}
# ================================================= #
# ************** 监控告警企微机器人配置  ************** #
# ================================================= #
QY_WEB_HOOK = os.getenv("QY_WEB_HOOK")  # 测试环境不需要机器人通知
# ================================================= #
# ************** 发送邮件配置  ************** #
# ================================================= #
# 使用 SMTP 服务器发送邮件
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND")
# SMTP 服务器地址
EMAIL_HOST = os.getenv("EMAIL_HOST")
# SMTP 服务器端口
EMAIL_PORT = os.getenv("EMAIL_PORT")
# 发件人邮箱账号
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
# 发件人邮箱密码
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
# 是否使用 TLS
EMAIL_USE_TL = False
# 是否使用 SSL
EMAIL_USE_SSL = True
# ================================================= #
# ************** 录制流量代理配置  ************** #
# ================================================= #
# PROXY Server
PROXY_ON = os.getenv("PROXY_ON", "True") == "True"  # 是否开启代理
PROXY_PORT = int(os.getenv("PROXY_PORT"))
测试组/Test_platform/Interface_automation/backend/crud/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py
@Time    : 2023/1/14 16:21
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/crud/base_crud.py
New file
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""
@File    : base_curd.py
@Time    : 2023/1/14 16:22
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List
from rest_framework.generics import get_object_or_404
from crud import crud_helper
class BaseCURD(ABC):
    def __init__(self, model):
        self.model = model
    @abstractmethod
    def create_obj(self, creator: str, payload: Any) -> None:
        ...
    @abstractmethod
    def get_obj_by_pk(self, pk: int):
        ...
    @abstractmethod
    def get_obj_by_unique_key(self, unique_key: Dict):
        ...
    @abstractmethod
    def get_or_create(self, filter_kwargs: Dict, defaults: Dict):
        ...
    @abstractmethod
    def list_obj(self, page_filter: Dict) -> List[Dict]:
        ...
    @abstractmethod
    def update_obj_by_pk(self, pk: int, updater: str, payload: Dict):
        ...
    @abstractmethod
    def delete_obj_by_pk(self, pk: int) -> bool:
        ...
class GenericCURD(BaseCURD):
    def delete_obj_by_pk(self, pk: int) -> bool:
        self.get_obj_by_pk(pk=pk).delete()
        return True
    def update_obj_by_pk(self, pk: int, updater: str, payload: Dict):
        return crud_helper.update(
            obj=self.get_obj_by_pk(pk=pk), updater=updater, payload=payload
        )
    def list_obj(self, page_filter: Dict) -> List[Dict]:
        return self.model.objects.filter(**page_filter)
    def get_or_create(self, filter_kwargs: Dict, defaults: Dict):
        return crud_helper.get_or_create(
            model=self.model, filter_kwargs=filter_kwargs, defaults=defaults
        )
    def get_obj_by_unique_key(self, unique_key: Dict):
        return get_object_or_404(self.model, **unique_key)
    def get_obj_by_pk(self, pk: int):
        return get_object_or_404(self.model, id=pk)
    def create_obj(self, creator: str, payload: Any) -> None:
        return crud_helper.create(creator=creator, model=self.model, payload=payload)
测试组/Test_platform/Interface_automation/backend/crud/crud_helper.py
New file
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""
@File    : crud_helper.py
@Time    : 2023/1/14 16:23
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import traceback
from typing import Dict, Tuple, TypeVar
from loguru import logger
from lunarlink.models import BaseTable
BModel = TypeVar("BModel", bound=BaseTable)
def create(creator: str, model: BModel, payload: Dict) -> None:
    try:
        logger.info(f"input: create={model.__name__}, payload={payload}")
        obj = model.objects.create(creator=creator, **payload)
    except Exception as e:
        logger.warning(traceback.format_exc())
        raise e
    logger.info(f"create {model.__name__} success, id: {obj.id}")
def get_or_create(
    model: BModel,
    filter_kwargs: Dict,
    defaults: Dict,
) -> Tuple[BaseTable, bool]:
    """
    :param model:
    :param filter_kwargs:
    :param defaults:
    :return:
    """
    logger.info(
        f"input get_or_create={model.__name__}, "
        f"filter_kwargs={filter_kwargs}, "
        f"defaults={defaults}"
    )
    obj, created = model.objects.get_or_create(
        defaults=defaults,
        **filter_kwargs,
    )
    return obj, created
def update(obj: BModel, updater: str, payload: Dict) -> BaseTable:
    logger.info(
        f"input: update model={obj.__class__.__name__}, id={obj.id}, payload={payload}"
    )
    if updater:
        obj.updater = updater
    for attr, value in payload.items():
        if hasattr(obj, attr) is False:
            logger.warning(f"{attr=} not in obj fields, it will not update")
        setattr(obj, attr, value)
    try:
        obj.save()
    except Exception as e:
        logger.error(traceback.format_exc())
        raise e
    logger.info(f"update {obj.__class__.__name__} success, id: {obj.id}")
    return obj
测试组/Test_platform/Interface_automation/backend/extra_apps/__init__.py
New file
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/6/7 16:45
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/__about__.py
New file
@@ -0,0 +1,9 @@
__title__ = 'HttpRunner'
__description__ = 'One-stop solution for HTTP(S) testing.'
__url__ = 'https://github.com/HttpRunner/HttpRunner'
__version__ = '1.5.15'
__author__ = 'debugtalk'
__author_email__ = 'mail@debugtalk.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2017 debugtalk'
__cake__ = u'\u2728 \U0001f370 \u2728'
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/__init__.py
New file
@@ -0,0 +1,10 @@
# encoding: utf-8
try:
    pass
    # monkey patch at beginning to avoid RecursionError when running locust.
    # from gevent import monkey; monkey.patch_all()
except ImportError:
    pass
from httprunner.api import HttpRunner
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/api.py
New file
@@ -0,0 +1,262 @@
# encoding: utf-8
import logging
import os
import unittest
from httprunner import exceptions, loader, parser, report, runner, utils, validator
logger = logging.getLogger(__name__)
class HttpRunner(object):
    def __init__(self, **kwargs):
        """initialize HttpRunner.
        Args:
            kwargs (dict): key-value arguments used to initialize TextTestRunner.
            Commonly used arguments:
            resultclass (class): HtmlTestResult or TextTestResult
            failfast (bool): False/True, stop the test run on the first error or failure.
            http_client_session (instance): requests.Session(), or locust.client.Session() instance.
        Attributes:
            project_mapping (dict): save project loaded api/testcases, environments and debugtalk.py module.
                {
                    "debugtalk": {
                        "variables": {},
                        "functions": {}
                    },
                    "env": {},
                    "def-api": {},
                    "def-testcase": {}
                }
        """
        self.exception_stage = "initialize HttpRunner()"
        self.http_client_session = kwargs.pop("http_client_session", None)
        kwargs.setdefault("resultclass", report.HtmlTestResult)
        self.unittest_runner = unittest.TextTestRunner(**kwargs)
        self.test_loader = unittest.TestLoader()
        self.summary = None
    def _add_tests(self, testcases):
        """initialize testcase with Runner() and add to test suite.
        Args:
            testcases (list): parsed testcases list
        Returns:
            tuple: unittest.TestSuite()
        """
        def _add_teststep(test_runner, config, teststep_dict):
            """add teststep to testcase."""
            def test(self):
                try:
                    test_runner.run_test(teststep_dict)
                except exceptions.MyBaseFailure as ex:
                    self.fail(str(ex))
                finally:
                    if hasattr(test_runner.http_client_session, "meta_data"):
                        self.meta_data = test_runner.http_client_session.meta_data
                        self.meta_data["validators"] = test_runner.evaluated_validators
                        test_runner.http_client_session.init_meta_data()
            try:
                teststep_dict["name"] = parser.parse_data(
                    teststep_dict["name"],
                    config.get("variables", {}),
                    config.get("functions", {}),
                )
            except exceptions.VariableNotFound:
                pass
            test.__doc__ = teststep_dict["name"]
            return test
        test_suite = unittest.TestSuite()
        for testcase in testcases:
            config = testcase.get("config", {})
            test_runner = runner.Runner(config, self.http_client_session)
            TestSequense = type("TestSequense", (unittest.TestCase,), {})
            teststeps = testcase.get("teststeps", [])
            for index, teststep_dict in enumerate(teststeps):
                for times_index in range(int(teststep_dict.get("times", 1))):
                    # suppose one testcase should not have more than 9999 steps,
                    # and one step should not run more than 999 times.
                    test_method_name = "test_{:04}_{:03}".format(index, times_index)
                    test_method = _add_teststep(test_runner, config, teststep_dict)
                    setattr(TestSequense, test_method_name, test_method)
            loaded_testcase = self.test_loader.loadTestsFromTestCase(TestSequense)
            setattr(loaded_testcase, "config", config)
            setattr(loaded_testcase, "teststeps", testcase.get("teststeps", []))
            setattr(loaded_testcase, "runner", test_runner)
            test_suite.addTest(loaded_testcase)
        return test_suite
    def _run_suite(self, test_suite):
        """run tests in test_suite
        Args:
            test_suite: unittest.TestSuite()
        Returns:
            list: tests_results
        """
        tests_results = []
        for testcase in test_suite:
            testcase_name = testcase.config.get("name")
            logger.info("Start to run testcase: {}".format(testcase_name))
            result = self.unittest_runner.run(testcase)
            tests_results.append((testcase, result))
        return tests_results
    def _aggregate(self, tests_results):
        """aggregate results
        Args:
            tests_results (list): list of (testcase, result)
        """
        self.summary = {
            "success": True,
            "stat": {},
            "time": {},
            "platform": report.get_platform(),
            "details": [],
        }
        for tests_result in tests_results:
            testcase, result = tests_result
            testcase_summary = report.get_summary(result)
            self.summary["success"] &= testcase_summary["success"]
            testcase_summary["name"] = testcase.config.get("name")
            testcase_summary["base_url"] = testcase.config.get("request", {}).get(
                "base_url", ""
            )
            in_out = utils.get_testcase_io(testcase)
            utils.print_io(in_out)
            testcase_summary["in_out"] = in_out
            report.aggregate_stat(self.summary["stat"], testcase_summary["stat"])
            report.aggregate_stat(self.summary["time"], testcase_summary["time"])
            self.summary["details"].append(testcase_summary)
    def _run_tests(self, testcases, mapping=None):
        """start to run test with variables mapping.
        Args:
            testcases (list): list of testcase_dict, each testcase is corresponding to a YAML/JSON file
                [
                    {   # testcase data structure
                        "config": {
                            "name": "desc1",
                            "path": "testcase1_path",
                            "variables": [],        # optional
                            "request": {}           # optional
                            "refs": {
                                "debugtalk": {
                                    "variables": {},
                                    "functions": {}
                                },
                                "env": {},
                                "def-api": {},
                                "def-testcase": {}
                            }
                        },
                        "teststeps": [
                            # teststep data structure
                            {
                                'name': 'test step desc2',
                                'variables': [],    # optional
                                'extract': [],      # optional
                                'validate': [],
                                'request': {},
                                'function_meta': {}
                            },
                            teststep2   # another teststep dict
                        ]
                    },
                    testcase_dict_2     # another testcase dict
                ]
            mapping (dict): if mapping is specified, it will override variables in config block.
        Returns:
            instance: HttpRunner() instance
        """
        self.exception_stage = "parse tests"
        parsed_testcases_list = parser.parse_tests(testcases, mapping)
        self.exception_stage = "add tests to test suite"
        test_suite = self._add_tests(parsed_testcases_list)
        self.exception_stage = "run test suite"
        results = self._run_suite(test_suite)
        self.exception_stage = "aggregate results"
        self._aggregate(results)
        return self
    def run(self, path_or_testcases, dot_env_path=None, mapping=None):
        """main interface, run testcases with variables mapping.
        Args:
            path_or_testcases (str/list/dict): testcase file/foler path, or valid testcases.
            dot_env_path (str): specified .env file path.
            mapping (dict): if mapping is specified, it will override variables in config block.
        Returns:
            instance: HttpRunner() instance
        """
        self.exception_stage = "load tests"
        if validator.is_testcases(path_or_testcases):
            if isinstance(path_or_testcases, dict):
                testcases = [path_or_testcases]
            else:
                testcases = path_or_testcases
        elif validator.is_testcase_path(path_or_testcases):
            testcases = loader.load_tests(path_or_testcases, dot_env_path)
        else:
            raise exceptions.ParamsError("invalid testcase path or testcases.")
        return self._run_tests(testcases, mapping)
    def gen_html_report(self, html_report_name=None, html_report_template=None):
        """generate html report and return report path.
        Args:
            html_report_name (str): output html report file name
            html_report_template (str): report template file path, template should be in Jinja2 format
        Returns:
            str: generated html report path
        """
        if not self.summary:
            raise exceptions.MyBaseError(
                "run method should be called before gen_html_report."
            )
        self.exception_stage = "generate report"
        return report.render_html_report(
            self.summary, html_report_name, html_report_template
        )
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/__init__.py
New file
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py
@Time    : 2023/1/14 17:08
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from httprunner.builtin.comparators import *
from httprunner.builtin.common_util import *
from httprunner.builtin.time_helper import *
from httprunner.builtin.request_helper import *
from httprunner.builtin.faker_helper import *
from httprunner.builtin.rand_helper import *
from httprunner.builtin.login_helper import *
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/auxiliary_func.py
New file
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
@File    : auxiliary_func.py
@Time    : 2022/12/6 15:56
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
import base64
import os
from backend.settings import BASE_DIR
def img_value():
    """
    获取图片换算值
    :return:
    """
    file_path = os.path.join(BASE_DIR, "test_data/images/attachment.jpg")
    file_value = get_file_value(file_path)
    return file_value
def get_file_value(file_path):
    """
    解析编码文件,生成file参数的值
    :param file_path: 文件路径
    :return:
    """
    # 以二进制格式读取文件内容
    with open(file_path, "rb") as f:
        content = f.read()
    content = base64.b64encode(content)
    suffix_name = file_path.split(".")[-1]
    # file参数格式:文件名后缀 + @ + 文件内容进行base64编码后的字符串
    file_value = suffix_name + "@" + content.decode()
    return file_value
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/common_util.py
New file
@@ -0,0 +1,55 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
import datetime
import os
import random
import string
import time
from requests_toolbelt import MultipartEncoder
from httprunner.compat import builtin_str, integer_types
from httprunner.exceptions import ParamsError
def gen_random_string(str_len):
    """generate random string with specified length"""
    return "".join(
        random.choice(string.ascii_letters + string.digits) for _ in range(str_len)
    )
def get_timestamp(str_len=13):
    """get timestamp string, length can only between 0 and 16"""
    if isinstance(str_len, integer_types) and 0 < str_len < 17:
        return builtin_str(time.time()).replace(".", "")[:str_len]
    raise ParamsError("timestamp length can only between 0 and 16.")
def get_current_date(fmt="%Y-%m-%d"):
    """get current date, default format is %Y-%m-%d"""
    return datetime.datetime.now().strftime(fmt)
def multipart_encoder(field_name, file_path, file_type=None, file_headers=None):
    if not os.path.isabs(file_path):
        file_path = os.path.join(os.getcwd(), file_path)
    filename = os.path.basename(file_path)
    with open(file_path, "rb") as f:
        fields = {field_name: (filename, f.read(), file_type)}
    return MultipartEncoder(fields)
def multipart_content_type(multipart_encoder):
    return multipart_encoder.content_type
""" built-in hooks
"""
def setup_hook_prepare_kwargs(request):
    pass
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/comparators.py
New file
@@ -0,0 +1,169 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Built-in dependent functions used in YAML/JSON testcases.
"""
import re
import pydash
from httprunner.compat import basestring, builtin_str, integer_types
"""
built-in comparators
"""
def equals(check_value, expect_value):
    assert check_value == expect_value
def less_than(check_value, expect_value):
    assert check_value < expect_value
def less_than_or_equals(check_value, expect_value):
    assert check_value <= expect_value
def greater_than(check_value, expect_value):
    assert check_value > expect_value
def greater_than_or_equals(check_value, expect_value):
    assert check_value >= expect_value
def not_equals(check_value, expect_value):
    assert check_value != expect_value
def string_equals(check_value, expect_value):
    assert builtin_str(check_value) == builtin_str(expect_value)
def length_equals(check_value, expect_value):
    assert isinstance(expect_value, integer_types)
    assert len(check_value) == expect_value
def length_greater_than(check_value, expect_value):
    assert isinstance(expect_value, integer_types)
    assert len(check_value) > expect_value
def length_greater_than_or_equals(check_value, expect_value):
    assert isinstance(expect_value, integer_types)
    assert len(check_value) >= expect_value
def length_less_than(check_value, expect_value):
    assert isinstance(expect_value, integer_types)
    assert len(check_value) < expect_value
def length_less_than_or_equals(check_value, expect_value):
    assert isinstance(expect_value, integer_types)
    assert len(check_value) <= expect_value
def contains(check_value, expect_value):
    assert isinstance(check_value, (list, tuple, dict, basestring))
    assert expect_value in check_value
def not_contains(check_value, expect_value):
    assert isinstance(check_value, (list, tuple, dict, basestring))
    assert expect_value not in check_value
def contained_by(check_value, expect_value):
    assert isinstance(expect_value, (list, tuple, dict, basestring))
    assert check_value in expect_value
def _get_expression(item, expression, expect_value, jsonpath):
    parsed_expression = None
    if isinstance(item, dict):
        item_value = pydash.get(item, jsonpath)
        if isinstance(item_value, (int, float, list, dict, bool, type(None))):
            parsed_expression = f"{item_value} {expression} {expect_value}"
        else:
            parsed_expression = (
                f"'{pydash.get(item, jsonpath)}' {expression} '{expect_value}'"
            )
    if isinstance(item, str):
        parsed_expression = f"{item} {expression} {expect_value}"
    if parsed_expression is None:
        raise AssertionError(f"list的元素只能是dict或者string")
    return parsed_expression
def list_any_item_contains(check_value: list, jsonpath_expression_value):
    assert isinstance(check_value, list)
    jsonpath, expression, expect_value = jsonpath_expression_value.split(" ")
    for item in check_value:
        parsed_expression = _get_expression(
            item=item,
            expression=expression,
            expect_value=expect_value,
            jsonpath=jsonpath,
        )
        try:
            if eval(parsed_expression) is True:
                break
        except Exception as e:
            raise e
    else:
        raise AssertionError(f"{check_value} {expression} {expect_value}")
def list_all_item_contains(check_value: list, jsonpath_expression_value):
    assert isinstance(check_value, list)
    jsonpath, expression, expect_value = jsonpath_expression_value.split(" ")
    for item in check_value:
        parsed_expression = _get_expression(
            item=item,
            expression=expression,
            expect_value=expect_value,
            jsonpath=jsonpath,
        )
        try:
            if eval(parsed_expression) is False:
                raise AssertionError(f"{check_value} {expression} {expect_value}")
        except Exception as e:
            raise e
def type_match(check_value, expect_value):
    def get_type(name):
        if isinstance(name, type):
            return name
        elif isinstance(name, basestring):
            try:
                return __builtins__[name]
            except KeyError:
                raise ValueError(name)
        else:
            raise ValueError(name)
    assert isinstance(check_value, get_type(expect_value))
def regex_match(check_value, expect_value):
    assert isinstance(expect_value, basestring)
    assert isinstance(check_value, basestring)
    assert re.match(expect_value, check_value)
def startswith(check_value, expect_value):
    assert builtin_str(check_value).startswith(builtin_str(expect_value))
def endswith(check_value, expect_value):
    assert builtin_str(check_value).endswith(builtin_str(expect_value))
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/faker_helper.py
New file
@@ -0,0 +1,16 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
from faker import Faker
F = Faker(locale="zh_CN")
# 假名f_name()
# 假地址f_addr()
# 假电话f_phone()
f_name = lambda: F.name()
f_addr = lambda: F.address()
f_phone = lambda: F.phone_number()
f_time = lambda: F.time()
f_date = lambda: F.date()
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/login_helper.py
New file
@@ -0,0 +1,46 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import json
import pydash
import requests
uac_token_url = "http://192.168.22.19:8002/api/uac/token/"
logger = logging.getLogger(__name__)
def _get_token(biz, account, password, env="qa"):
    data = {"biz": biz, "account": account, "password": password, "env": env}
    headers = {"Content-Type": "application/json; charset=utf-8"}
    res = requests.post(
        url=uac_token_url, headers=headers, data=json.dumps(data)
    ).json()
    return res, data
def get_userid(biz, account, password, env="qa"):
    res, data = _get_token(biz, account, password, env)
    user_id = pydash.get(res, "user_id")
    if user_id:
        logger.info(f"获取user_id成功: {user_id}")
        return user_id
    else:
        logger.warning(f"获取user_id失败,入参是: {data}, 响应是: {res}")
        raise Exception("获取user_id失败")
def get_uac_token(biz, account, password, env="qa"):
    res, data = _get_token(biz, account, password, env)
    token = pydash.get(res, "token")
    if token:
        logger.info(f"获取token成功: {token}")
        return token
    else:
        logger.warning(f"获取token失败,入参是: {data}, 响应是: {res}")
        raise Exception("获取token失败")
if __name__ == "__main__":
    get_userid("cm", "13533975028", "397726")
    get_uac_token("cm", "13533975028", "397726")
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/rand_helper.py
New file
@@ -0,0 +1,35 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
import random
import string
import uuid
def rand_int(begin: int = 0, end: int = 10000):
    """生成0-10000的随机数"""
    return random.randint(begin, end)
def rand_int4():
    """4位随机数"""
    return rand_int(1000, 9999)
def rand_int5():
    """5位随机数"""
    return rand_int(10000, 99999)
def rand_int6():
    """6位随机数"""
    return rand_int(100000, 999999)
def rand_str(n: int = 5):
    """获取大小写字母+数字的随机字符串,默认5位"""
    seq = string.ascii_letters + string.digits
    return "".join(random.choices(seq, k=n))
def uid():
    return str(uuid.uuid1())
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/request_helper.py
New file
@@ -0,0 +1,53 @@
# !/usr/bin/python3
# -*- coding: utf-8 -*-
import json
import logging
import pydash
# from loguru import logger
logger = logging.getLogger(__name__)
def _load_json(in_data):
    if isinstance(in_data, (dict, list)) is False:
        try:
            in_data = json.loads(in_data)
        except Exception as e:
            logger.error(str(e))
            raise e
    return in_data
def set_json(request_obj, in_data={}, include="", json_path="."):
    """
    修改请求体的json, 包含模式
    """
    in_data = _load_json(in_data)
    request_data = pydash.get(request_obj["json"], json_path)
    include_keys = include.split("-")
    for k in include_keys:
        v = pydash.get(in_data, k)
        path = k.split(".")[-1]
        pydash.set_(request_data, path, v)
def set_json_e(request_obj, in_data={}, exclude="", in_path="."):
    """
    修改请求体的json, 排除模式
    """
    in_data = _load_json(in_data)
    request_data = pydash.get(request_obj["json"], in_path)
    exclude_keys = exclude.split("-")
    if isinstance(in_data, dict):
        for k, v in in_data.items():
            if k in exclude_keys:
                continue
            pydash.set_(request_data, k, v)
    else:
        for index, value in enumerate(in_data):
            if index in exclude_keys:
                continue
            pydash.set_(request_data, index, value)
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/builtin/time_helper.py
New file
@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
"""
@File    : time_helper.py
@Time    : 2023/1/14 17:09
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
# 获取时间戳
import datetime
import time
def get_day(days: int = 0, **kwargs) -> str:
    """
    获取日期,默认无连接符格式
    :param days: 负数表示过去,整数表示未来
    :param kwargs: 可选h, m, s
        h: 第n小时
        m: 第n分钟
        s: 第n秒
    :return:
    >>> get_day()
    # 今天的日期
    >>> get_day(1)
    # 明天的日期
    >>> get_day(-1)
    # 昨天的日期
    >>> get_day(h=9,m=15,s=30)
    # 今天的9时15分30秒 20230116 9:15:30
    >>> get_day(sep='-', h=9,m=15,s=30)
    # 2021-08-07 9:15:30 # 今天的9时15分30秒, 日期分隔符是-
    """
    d = datetime.timedelta(days)
    n = datetime.datetime.now()
    sep: str = kwargs.get("sep", "")
    fmt = sep.join(["%Y", "%m", "%d"])
    if kwargs:
        h = kwargs.get("h", "00")
        m = kwargs.get("m", "00")
        s = kwargs.get("s", "00")
        fmt = f"{fmt} {h}:{m}:{s}"
    return (n + d).strftime(fmt)
def get_ts(interval: int = 0, t=None) -> str:
    """获取时间戳
    :param interval: 时间间隔,t为None时,时间间隔为秒,其它则为天
    :param t: 要转换成时间戳的时分秒
    :return:
    >>> get_ts(0)
    # 当前时间戳
    >>> get_ts(0, 9)
    # 今天9点的时间戳
    >>> get_ts(1, 9)
    # 明天9点的时间戳
    >>> get_ts(0, '9:15')
    # 今天9点15分的时间戳
    >>> get_ts(0, '9:15:10')
    # 今天9点15分10秒的时间戳
    """
    if isinstance(interval, int) and t is None:
        return str(int(time.time()) + interval)
    else:
        time_format = "%Y%m%d"
        t = str(t)
        if t.count(":") == 0 and len(t.split()) == 1:
            time_format = time_format + "%H"
        elif t.count(":") == 1 and len(t.split()) == 1:
            time_format = time_format + "%H:%M"
        elif t.count(":") == 2 and len(t.split()) == 1:
            time_format = time_format + "%H:%M:%S"
        time_arr = time.strptime(get_day(interval) + t, time_format)
        return str(int(time.mktime(time_arr)))
def get_ts_int(interval: int = 0, t=None) -> int:
    """获取时间戳
    :param interval: 时间间隔,t为None时,时间间隔为秒,其它则为天
    :param t: 要转换成时间戳的时分秒
    :return:
    """
    return int(get_ts(interval, t))
def get_day_fmt(fmt_type="sec", **kwargs):
    """获取日期
    fmt_type : str
        日期格式,默认'sec', 包含时分秒, 'day'只包含年月日
    kwargs : str, key可以是days, hours, seconds, 等
        时间差值,负数表示过去,正数表示未来
        昨天:days=-1,明天days=1
    >>> get_day_fmt()
    # 当前日期,包含时分秒
    2021-08-07 12:08:54
    >>> get_day_fmt('day')
    # 当前日期,只包含年月日
    2021-08-07
    # 昨天的日期
    >>> get_day_fmt('day', days=-1)
    2021-08-06
    """
    fmt = "%Y-%m-%d %H:%M:%S"
    if fmt_type == "day":
        fmt = "%Y-%m-%d"
    day = datetime.timedelta(**kwargs)
    now = datetime.datetime.now()
    return (now + day).strftime(fmt)
def get_day_h(days: int = 0, **kwargs):
    """
    >>> get_day_h(0,h=7)
    2021042207
    """
    d = datetime.timedelta(days)
    n = datetime.datetime.now()
    time_str = f"%Y%m%d"
    if kwargs:
        h = str(kwargs.get("h"))
        h = h.rjust(2, "0")
        time_str = f"{time_str}{h}"
    return (n + d).strftime(time_str)
def get_hour_ts(interval: int = 0) -> str:
    # 当前时间所在的小时,转换为时间戳
    # 2021-3-9 17:0:12, 取2021-3-9 17:0:0,然后转换为时间戳
    now_hour = int(
        datetime.datetime.now().replace(minute=0, second=0, microsecond=0).timestamp()
    )
    return str(now_hour + interval * 3600)
def get_hour() -> str:
    # 获取当前的小时,不足两位补0
    return str(datetime.datetime.now().hour).rjust(2, "0")
def get_hour_ts_int(interval: int = 0) -> int:
    return int(get_hour(interval))
def wait(i: int = 0):
    time.sleep(i)
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/cli.py
New file
@@ -0,0 +1,186 @@
# encoding: utf-8
import argparse
import multiprocessing
import logging
import sys
import unittest
# from httprunner import logger
from httprunner.__about__ import __description__, __version__
from httprunner.api import HttpRunner
from httprunner.compat import is_py2
from httprunner.utils import (create_scaffold, get_python2_retire_msg,
                              prettify_json_file, validate_json_file)
logger = logging.getLogger(__name__)
def main_hrun():
    """ API test: parse command line options and run commands.
    """
    parser = argparse.ArgumentParser(description=__description__)
    parser.add_argument(
        '-V', '--version', dest='version', action='store_true',
        help="show version")
    parser.add_argument(
        'testcase_paths', nargs='*',
        help="testcase file path")
    parser.add_argument(
        '--no-html-report', action='store_true', default=False,
        help="do not generate html report.")
    parser.add_argument(
        '--html-report-name',
        help="specify html report name, only effective when generating html report.")
    parser.add_argument(
        '--html-report-template',
        help="specify html report template path.")
    parser.add_argument(
        '--log-level', default='INFO',
        help="Specify logging level, default is INFO.")
    parser.add_argument(
        '--log-file',
        help="Write logs to specified file path.")
    parser.add_argument(
        '--dot-env-path',
        help="Specify .env file path, which is useful for keeping sensitive data.")
    parser.add_argument(
        '--failfast', action='store_true', default=False,
        help="Stop the test run on the first error or failure.")
    parser.add_argument(
        '--startproject',
        help="Specify new project name.")
    parser.add_argument(
        '--validate', nargs='*',
        help="Validate JSON testcase format.")
    parser.add_argument(
        '--prettify', nargs='*',
        help="Prettify JSON testcase format.")
    args = parser.parse_args()
    # logger.setup_logger(args.log_level, args.log_file)
    if is_py2:
        logger.warning(get_python2_retire_msg())
    if args.version:
        logger.info("{}".format(__version__), "GREEN")
        exit(0)
    if args.validate:
        validate_json_file(args.validate)
        exit(0)
    if args.prettify:
        prettify_json_file(args.prettify)
        exit(0)
    project_name = args.startproject
    if project_name:
        create_scaffold(project_name)
        exit(0)
    try:
        runner = HttpRunner(
            failfast=args.failfast
        )
        runner.run(
            args.testcase_paths,
            dot_env_path=args.dot_env_path
        )
    except Exception:
        logger.error("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage))
        raise
    if not args.no_html_report:
        runner.gen_html_report(
            html_report_name=args.html_report_name,
            html_report_template=args.html_report_template
        )
    summary = runner.summary
    return 0 if summary["success"] else 1
def main_locust():
    """ Performance test with locust: parse command line options and run commands.
    """
    try:
        from httprunner import locusts
    except ImportError:
        msg = "Locust is not installed, install first and try again.\n"
        msg += "install command: pip install locustio"
        print(msg)
        exit(1)
    sys.argv[0] = 'locust'
    if len(sys.argv) == 1:
        sys.argv.extend(["-h"])
    if sys.argv[1] in ["-h", "--help", "-V", "--version"]:
        locusts.main()
        sys.exit(0)
    # set logging level
    if "-L" in sys.argv:
        loglevel_index = sys.argv.index('-L') + 1
    elif "--loglevel" in sys.argv:
        loglevel_index = sys.argv.index('--loglevel') + 1
    else:
        loglevel_index = None
    if loglevel_index and loglevel_index < len(sys.argv):
        loglevel = sys.argv[loglevel_index]
    else:
        # default
        loglevel = "INFO"
    # logger.setup_logger(loglevel)
    # get testcase file path
    try:
        if "-f" in sys.argv:
            testcase_index = sys.argv.index('-f') + 1
        elif "--locustfile" in sys.argv:
            testcase_index = sys.argv.index('--locustfile') + 1
        else:
            testcase_index = None
        assert testcase_index and testcase_index < len(sys.argv)
    except AssertionError:
        print("Testcase file is not specified, exit.")
        sys.exit(1)
    testcase_file_path = sys.argv[testcase_index]
    sys.argv[testcase_index] = locusts.parse_locustfile(testcase_file_path)
    if "--processes" in sys.argv:
        """ locusts -f locustfile.py --processes 4
        """
        if "--no-web" in sys.argv:
            logger.error("conflict parameter args: --processes & --no-web. \nexit.")
            sys.exit(1)
        processes_index = sys.argv.index('--processes')
        processes_count_index = processes_index + 1
        if processes_count_index >= len(sys.argv):
            """ do not specify processes count explicitly
                locusts -f locustfile.py --processes
            """
            processes_count = multiprocessing.cpu_count()
            logger.warning("processes count not specified, use {} by default.".format(processes_count))
        else:
            try:
                """ locusts -f locustfile.py --processes 4 """
                processes_count = int(sys.argv[processes_count_index])
                sys.argv.pop(processes_count_index)
            except ValueError:
                """ locusts -f locustfile.py --processes -P 8888 """
                processes_count = multiprocessing.cpu_count()
                logger.warning("processes count not specified, use {} by default.".format(processes_count))
        sys.argv.pop(processes_index)
        locusts.run_locusts_with_processes(sys.argv, processes_count)
    else:
        locusts.main()
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/client.py
New file
@@ -0,0 +1,212 @@
# encoding: utf-8
import re
import curlify
import time
import logging
import requests
import urllib3
# from httprunner import logger
from httprunner.exceptions import ParamsError
from requests import Request, Response
from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
                                 RequestException)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
logger = logging.getLogger(__name__)
class ApiResponse(Response):
    def raise_for_status(self):
        if hasattr(self, 'error') and self.error:
            raise self.error
        Response.raise_for_status(self)
class HttpSession(requests.Session):
    """
    Class for performing HTTP requests and holding (session-) cookies between requests (in order
    to be able to log in and out of websites). Each request is logged so that HttpRunner can
    display statistics.
    This is a slightly extended version of `python-request <http://python-requests.org>`_'s
    :py:class:`requests.Session` class and mostly this class works exactly the same. However
    the methods for making requests (get, post, delete, put, head, options, patch, request)
    can now take a *url* argument that's only the path part of the URL, in which case the host
    part of the URL will be prepended with the HttpSession.base_url which is normally inherited
    from a HttpRunner class' host property.
    """
    def __init__(self, base_url=None, *args, **kwargs):
        super(HttpSession, self).__init__(*args, **kwargs)
        self.base_url = base_url if base_url else ""
        self.init_meta_data()
    def _build_url(self, path):
        """ prepend url with hostname unless it's already an absolute URL """
        if absolute_http_url_regexp.match(path):
            return path
        elif self.base_url:
            return "{}/{}".format(self.base_url.rstrip("/"), path.lstrip("/"))
        else:
            raise ParamsError("base url missed!")
    def init_meta_data(self):
        """ initialize meta_data, it will store detail data of request and response
        """
        self.meta_data = {
            "request": {
                "url": "N/A",
                "method": "N/A",
                "headers": {},
                "start_timestamp": None
            },
            "response": {
                "status_code": "N/A",
                "headers": {},
                "content_size": "N/A",
                "response_time_ms": "N/A",
                "elapsed_ms": "N/A",
                "encoding": None,
                "content": None,
                "content_type": ""
            }
        }
    def request(self, method, url, name=None, **kwargs):
        """
        Constructs and sends a :py:class:`requests.Request`.
        Returns :py:class:`requests.Response` object.
        :param method:
            method for the new :class:`Request` object.
        :param url:
            URL for the new :class:`Request` object.
        :param name: (optional)
            Placeholder, make compatible with Locust's HttpSession
        :param params: (optional)
            Dictionary or bytes to be sent in the query string for the :class:`Request`.
        :param data: (optional)
            Dictionary or bytes to send in the body of the :class:`Request`.
        :param headers: (optional)
            Dictionary of HTTP Headers to send with the :class:`Request`.
        :param cookies: (optional)
            Dict or CookieJar object to send with the :class:`Request`.
        :param files: (optional)
            Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
        :param auth: (optional)
            Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth.
        :param timeout: (optional)
            How long to wait for the server to send data before giving up, as a float, or \
            a (`connect timeout, read timeout <user/advanced.html#timeouts>`_) tuple.
            :type timeout: float or tuple
        :param allow_redirects: (optional)
            Set to True by default.
        :type allow_redirects: bool
        :param proxies: (optional)
            Dictionary mapping protocol to the URL of the proxy.
        :param stream: (optional)
            whether to immediately download the response content. Defaults to ``False``.
        :param verify: (optional)
            if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
        :param cert: (optional)
            if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        """
        def log_print(request_response):
            msg = "\n================== {} details ==================\n".format(request_response)
            for key, value in self.meta_data[request_response].items():
                msg += "{:<16} : {}\n".format(key, repr(value))
            logger.debug(msg)
        # record original request info
        self.meta_data["request"]["method"] = method
        self.meta_data["request"]["url"] = url
        self.meta_data["request"].update(kwargs)
        self.meta_data["request"]["start_timestamp"] = time.time()
        # prepend url with hostname unless it's already an absolute URL
        url = self._build_url(url)
        kwargs.setdefault("timeout", 120)
        response = self._send_request_safe_mode(method, url, **kwargs)
        # record the consumed time
        self.meta_data["response"]["response_time_ms"] = \
            round((time.time() - self.meta_data["request"]["start_timestamp"]) * 1000, 2)
        self.meta_data["response"]["elapsed_ms"] = response.elapsed.microseconds / 1000.0
        # record actual request info
        self.meta_data["request"]["url"] = (response.history and response.history[0] or response).request.url
        self.meta_data["request"]["headers"] = dict(response.request.headers)
        self.meta_data["request"]["body"] = response.request.body
        # log request details in debug mode
        log_print("request")
        # record response info
        self.meta_data["response"]["ok"] = response.ok
        self.meta_data["response"]["url"] = response.url
        self.meta_data["response"]["status_code"] = response.status_code
        self.meta_data["response"]["reason"] = response.reason
        self.meta_data["response"]["headers"] = dict(response.headers)
        self.meta_data["response"]["cookies"] = response.cookies or {}
        self.meta_data["response"]["encoding"] = response.encoding
        self.meta_data["response"]["content"] = response.content
        self.meta_data["response"]["text"] = response.text
        self.meta_data["response"]["content_type"] = response.headers.get("Content-Type", "")
        try:
            self.meta_data["response"]["json"] = response.json()
        except ValueError:
            self.meta_data["response"]["json"] = None
        # get the length of the content, but if the argument stream is set to True, we take
        # the size from the content-length header, in order to not trigger fetching of the body
        if kwargs.get("stream", False):
            self.meta_data["response"]["content_size"] = int(self.meta_data["response"]["headers"].get("content-length") or 0)
        else:
            self.meta_data["response"]["content_size"] = len(response.content or "")
        # log response details in debug mode
        log_print("response")
        try:
            response.raise_for_status()
        except RequestException as e:
            logger.error(u"{exception}".format(exception=str(e)))
        else:
            logger.info(
                """status_code: {}, response_time(ms): {} ms, response_length: {} bytes""".format(
                    self.meta_data["response"]["status_code"],
                    self.meta_data["response"]["response_time_ms"],
                    self.meta_data["response"]["content_size"]
                )
            )
        return response
    def _send_request_safe_mode(self, method, url, **kwargs):
        """
        Send a HTTP request, and catch any exception that might occur due to connection problems.
        Safe mode has been removed from requests 1.x.
        """
        try:
            msg = "processed request:\n"
            msg += "> {method} {url}\n".format(method=method, url=url)
            msg += "> kwargs: {kwargs}".format(kwargs=kwargs)
            logger.debug(msg)
            return requests.Session.request(self, method, url, **kwargs)
        except (MissingSchema, InvalidSchema, InvalidURL):
            raise
        except RequestException as ex:
            resp = ApiResponse()
            resp.error = ex
            resp.status_code = 0  # with this status_code, content returns None
            resp.request = Request(method, url).prepare()
            return resp
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/compat.py
New file
@@ -0,0 +1,60 @@
# encoding: utf-8
"""
httprunner.compat
~~~~~~~~~~~~~~~~~
This module handles import compatibility issues between Python 2 and
Python 3.
"""
from collections import OrderedDict
try:
    import simplejson as json
except ImportError:
    import json
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
# ---------
# Specifics
# ---------
try:
    JSONDecodeError = json.JSONDecodeError
except AttributeError:
    JSONDecodeError = ValueError
if is_py2:
    builtin_str = str
    bytes = str
    str = unicode
    basestring = basestring
    numeric_types = (int, long, float)
    integer_types = (int, long)
    FileNotFoundError = IOError
elif is_py3:
    builtin_str = str
    str = str
    bytes = bytes
    basestring = (str, bytes)
    numeric_types = (int, float)
    integer_types = (int,)
    FileNotFoundError = FileNotFoundError
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/context.py
New file
@@ -0,0 +1,265 @@
# encoding: utf-8
import copy
import logging
from httprunner import exceptions, parser, utils
from httprunner.compat import OrderedDict
logger = logging.getLogger(__name__)
class Context(object):
    """ Manages context functions and variables.
        context has two levels, testcase and teststep.
    """
    def __init__(self, variables=None, functions=None):
        """ init Context with testcase variables and functions.
        """
        # testcase level context
        # TESTCASE_SHARED_FUNCTIONS_MAPPING are unchangeable.
        # TESTCASE_SHARED_VARIABLES_MAPPING may change by Hrun.set_config
        if isinstance(variables, list):
            self.TESTCASE_SHARED_VARIABLES_MAPPING = utils.convert_mappinglist_to_orderdict(variables)
        else:
            # dict
            self.TESTCASE_SHARED_VARIABLES_MAPPING = variables or OrderedDict()
        self.TESTCASE_SHARED_FUNCTIONS_MAPPING = functions or OrderedDict()
        # testcase level request, will not change
        self.TESTCASE_SHARED_REQUEST_MAPPING = {}
        self.evaluated_validators = []
        self.init_context_variables(level="testcase")
    def init_context_variables(self, level="testcase"):
        """ initialize testcase/teststep context
        Args:
            level (enum): "testcase" or "teststep"
        """
        if level == "testcase":
            # testcase level runtime context, will be updated with extracted variables in each teststep.
            self.testcase_runtime_variables_mapping = copy.deepcopy(self.TESTCASE_SHARED_VARIABLES_MAPPING)
        # teststep level context, will be altered in each teststep.
        # teststep config shall inherit from testcase configs,
        # but can not change testcase configs, that's why we use copy.deepcopy here.
        self.teststep_variables_mapping = copy.deepcopy(self.testcase_runtime_variables_mapping)
    def update_context_variables(self, variables, level):
        """ update context variables, with level specified.
        Args:
            variables (list/OrderedDict): testcase config block or teststep block
                [
                    {"TOKEN": "debugtalk"},
                    {"random": "${gen_random_string(5)}"},
                    {"json": {'name': 'user', 'password': '123456'}},
                    {"md5": "${gen_md5($TOKEN, $json, $random)}"}
                ]
                OrderDict({
                    "TOKEN": "debugtalk",
                    "random": "${gen_random_string(5)}",
                    "json": {'name': 'user', 'password': '123456'},
                    "md5": "${gen_md5($TOKEN, $json, $random)}"
                })
            level (enum): "testcase" or "teststep"
        """
        if isinstance(variables, list):
            variables = utils.convert_mappinglist_to_orderdict(variables)
        for variable_name, variable_value in variables.items():
            variable_eval_value = self.eval_content(variable_value)
            if level == "testcase":
                self.testcase_runtime_variables_mapping[variable_name] = variable_eval_value
            self.update_teststep_variables_mapping(variable_name, variable_eval_value)
    def eval_content(self, content):
        """ evaluate content recursively, take effect on each variable and function in content.
            content may be in any data structure, include dict, list, tuple, number, string, etc.
        """
        return parser.parse_data(
            content,
            self.teststep_variables_mapping,
            self.TESTCASE_SHARED_FUNCTIONS_MAPPING
        )
    def update_testcase_runtime_variables_mapping(self, variables):
        """ update testcase_runtime_variables_mapping with extracted vairables in teststep.
        Args:
            variables (OrderDict): extracted variables in teststep
        """
        for variable_name, variable_value in variables.items():
            self.testcase_runtime_variables_mapping[variable_name] = variable_value
            self.update_teststep_variables_mapping(variable_name, variable_value)
    def update_teststep_variables_mapping(self, variable_name, variable_value):
        """ bind and update testcase variables mapping
        """
        self.teststep_variables_mapping[variable_name] = variable_value
    def get_parsed_request(self, request_dict, level="teststep"):
        """ get parsed request with variables and functions.
        Args:
            request_dict (dict): request config mapping
            level (enum): "testcase" or "teststep"
        Returns:
            dict: parsed request dict
        """
        if level == "testcase":
            # testcase config request dict has been parsed in parse_tests
            self.TESTCASE_SHARED_REQUEST_MAPPING = copy.deepcopy(request_dict)
            return self.TESTCASE_SHARED_REQUEST_MAPPING
        else:
            # teststep
            return self.eval_content(
                utils.deep_update_dict(
                    copy.deepcopy(self.TESTCASE_SHARED_REQUEST_MAPPING),
                    request_dict
                )
            )
    def __eval_check_item(self, validator, resp_obj):
        """ evaluate check item in validator.
        Args:
            validator (dict): validator
                {"check": "status_code", "comparator": "eq", "expect": 201}
                {"check": "$resp_body_success", "comparator": "eq", "expect": True}
            resp_obj (object): requests.Response() object
        Returns:
            dict: validator info
                {
                    "check": "status_code",
                    "check_value": 200,
                    "expect": 201,
                    "comparator": "eq"
                }
        """
        check_item = validator["check"]
        # check_item should only be the following 5 formats:
        # 1, variable reference, e.g. $token
        # 2, function reference, e.g. ${is_status_code_200($status_code)}
        # 3, dict or list, maybe containing variable/function reference, e.g. {"var": "$abc"}
        # 4, string joined by delimiter. e.g. "status_code", "headers.content-type"
        # 5, regex string, e.g. "LB[\d]*(.*)RB[\d]*"
        if isinstance(check_item, (dict, list)) \
            or parser.extract_variables(check_item) \
            or parser.extract_functions(check_item):
            # format 1/2/3
            check_value = self.eval_content(check_item)
            # convert content.json.0.$k > content.json.0.k
            # extract with content.json.0.k
            if isinstance(check_value, str) and check_value.startswith("content."):
                check_value = resp_obj.extract_field(check_value)
        else:
            # format 4/5
            check_value = resp_obj.extract_field(check_item)
        validator["check_value"] = check_value
        # expect_value should only be in 2 types:
        # 1, variable reference, e.g. $expect_status_code
        # 2, actual value, e.g. 200
        expect_value = self.eval_content(validator["expect"])
        validator["expect"] = expect_value
        validator["check_result"] = "unchecked"
        return validator
    def _do_validation(self, validator_dict):
        """ validate with functions
        Args:
            validator_dict (dict): validator dict
                {
                    "check": "status_code",
                    "check_value": 200,
                    "expect": 201,
                    "comparator": "eq"
                }
        """
        # TODO: move comparator uniform to init_test_suites
        comparator = utils.get_uniform_comparator(validator_dict["comparator"])
        validate_func = parser.get_mapping_function(comparator, self.TESTCASE_SHARED_FUNCTIONS_MAPPING)
        check_item = validator_dict["check"]
        check_value = validator_dict["check_value"]
        expect_value = validator_dict["expect"]
        if (check_value is None or expect_value is None) \
            and comparator not in ["is", "eq", "equals", "not_equals", "=="]:
            raise exceptions.ParamsError("Null value can only be compared with comparator: eq/equals/==")
        validate_msg = "validate: {} {} {}({})".format(
            check_item,
            comparator,
            expect_value,
            type(expect_value).__name__
        )
        try:
            validator_dict["check_result"] = "pass"
            validate_func(check_value, expect_value)
            validate_msg += "\t==> pass"
            logger.debug(validate_msg)
        except (AssertionError, TypeError):
            validate_msg += "\t==> fail"
            validate_msg += "\n{}({}) {} {}({})".format(
                check_value,
                type(check_value).__name__,
                comparator,
                expect_value,
                type(expect_value).__name__
            )
            logger.error(validate_msg)
            validator_dict["check_result"] = "fail"
            raise exceptions.ValidationFailure(validate_msg)
    def validate(self, validators, resp_obj):
        """ make validations
        """
        evaluated_validators = []
        if not validators:
            return evaluated_validators
        logger.info("start to validate.")
        validate_pass = True
        failures = []
        for validator in validators:
            # evaluate validators with context variable mapping.
            evaluated_validator = self.__eval_check_item(
                parser.parse_validator(validator),
                resp_obj
            )
            try:
                self._do_validation(evaluated_validator)
            except exceptions.ValidationFailure as ex:
                validate_pass = False
                failures.append(str(ex))
            evaluated_validators.append(evaluated_validator)
        if not validate_pass:
            failures_string = "\n".join([failure for failure in failures])
            raise exceptions.ValidationFailure(failures_string)
        return evaluated_validators
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/exceptions.py
New file
@@ -0,0 +1,76 @@
# encoding: utf-8
from httprunner.compat import JSONDecodeError, FileNotFoundError
""" failure type exceptions
    these exceptions will mark test as failure
"""
class MyBaseFailure(Exception):
    pass
class ValidationFailure(MyBaseFailure):
    pass
class ExtractFailure(MyBaseFailure):
    pass
class SetupHooksFailure(MyBaseFailure):
    pass
class TeardownHooksFailure(MyBaseFailure):
    pass
""" error type exceptions
    these exceptions will mark test as error
"""
class MyBaseError(Exception):
    pass
class FileFormatError(MyBaseError):
    pass
class ParamsError(MyBaseError):
    pass
class NotFoundError(MyBaseError):
    pass
class FileNotFound(FileNotFoundError, NotFoundError):
    pass
class FunctionNotFound(NotFoundError):
    pass
class VariableNotFound(NotFoundError):
    pass
class ApiNotFound(NotFoundError):
    pass
class TestcaseNotFound(NotFoundError):
    pass
"""Validate expression exception
"""
class ExpectValueParseFailure(MyBaseFailure):
    pass
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/loader.py
New file
@@ -0,0 +1,1032 @@
import collections
import csv
import importlib
import io
import json
import os
import sys
import logging
import yaml
from httprunner import builtin, exceptions, parser, utils, validator
from httprunner.compat import OrderedDict
logger = logging.getLogger(__name__)
###############################################################################
# file loader
###############################################################################
def _check_format(file_path, content):
    """check testcase format if valid"""
    # TODO: replace with JSON schema validation
    if not content:
        # testcase file content is empty
        err_msg = "Testcase file content is empty: {}".format(file_path)
        logger.error(err_msg)
        raise exceptions.FileFormatError(err_msg)
    elif not isinstance(content, (list, dict)):
        # testcase file content does not match testcase format
        err_msg = "Testcase file content format invalid: {}".format(file_path)
        logger.error(err_msg)
        raise exceptions.FileFormatError(err_msg)
def load_yaml_file(yaml_file):
    """load yaml file and check file content format"""
    with io.open(yaml_file, "r", encoding="utf-8") as stream:
        yaml_content = yaml.load(stream)
        _check_format(yaml_file, yaml_content)
        return yaml_content
def load_json_file(json_file):
    """load json file and check file content format"""
    with io.open(json_file, encoding="utf-8") as data_file:
        try:
            json_content = json.load(data_file)
        except exceptions.JSONDecodeError:
            err_msg = "JSONDecodeError: JSON file format error: {}".format(json_file)
            logger.error(err_msg)
            raise exceptions.FileFormatError(err_msg)
        _check_format(json_file, json_content)
        return json_content
def load_csv_file(csv_file):
    """load csv file and check file content format
    @param
        csv_file: csv file path
        e.g. csv file content:
            username,password
            test1,111111
            test2,222222
            test3,333333
    @return
        list of parameter, each parameter is in dict format
        e.g.
        [
            {'username': 'test1', 'password': '111111'},
            {'username': 'test2', 'password': '222222'},
            {'username': 'test3', 'password': '333333'}
        ]
    """
    csv_content_list = []
    with io.open(csv_file, encoding="utf-8") as csvfile:
        reader = csv.DictReader(csvfile)
        for row in reader:
            csv_content_list.append(row)
    return csv_content_list
def load_file(file_path):
    if not os.path.isfile(file_path):
        raise exceptions.FileNotFound("{} does not exist.".format(file_path))
    file_suffix = os.path.splitext(file_path)[1].lower()
    if file_suffix == ".json":
        return load_json_file(file_path)
    elif file_suffix in [".yaml", ".yml"]:
        return load_yaml_file(file_path)
    elif file_suffix == ".csv":
        return load_csv_file(file_path)
    else:
        # '' or other suffix
        err_msg = "Unsupported file format: {}".format(file_path)
        logger.warning(err_msg)
        return []
def load_folder_files(folder_path, recursive=True):
    """load folder path, return all files endswith yml/yaml/json in list.
    Args:
        folder_path (str): specified folder path to load
        recursive (bool): load files recursively if True
    Returns:
        list: files endswith yml/yaml/json
    """
    if isinstance(folder_path, (list, set)):
        files = []
        for path in set(folder_path):
            files.extend(load_folder_files(path, recursive))
        return files
    if not os.path.exists(folder_path):
        return []
    file_list = []
    for dirpath, dirnames, filenames in os.walk(folder_path):
        filenames_list = []
        for filename in filenames:
            if not filename.endswith((".yml", ".yaml", ".json")):
                continue
            filenames_list.append(filename)
        for filename in filenames_list:
            file_path = os.path.join(dirpath, filename)
            file_list.append(file_path)
        if not recursive:
            break
    return file_list
def load_dot_env_file(dot_env_path):
    """load .env file.
    Args:
        dot_env_path (str): .env file path
    Returns:
        dict: environment variables mapping
            {
                "UserName": "debugtalk",
                "Password": "123456",
                "PROJECT_KEY": "ABCDEFGH"
            }
    Raises:
        exceptions.FileFormatError: If .env file format is invalid.
    """
    if not os.path.isfile(dot_env_path):
        raise exceptions.FileNotFound(".env file path is not exist.")
    logger.info("Loading environment variables from {}".format(dot_env_path))
    env_variables_mapping = {}
    with io.open(dot_env_path, "r", encoding="utf-8") as fp:
        for line in fp:
            # maxsplit=1
            if "=" in line:
                variable, value = line.split("=", 1)
            elif ":" in line:
                variable, value = line.split(":", 1)
            else:
                raise exceptions.FileFormatError(".env format error")
            env_variables_mapping[variable.strip()] = value.strip()
    utils.set_os_environ(env_variables_mapping)
    return env_variables_mapping
def locate_file(start_path, file_name):
    """locate filename and return file path.
        searching will be recursive upward until current working directory.
    Args:
        start_path (str): start locating path, maybe file path or directory path
    Returns:
        str: located file path. None if file not found.
    Raises:
        exceptions.FileNotFound: If failed to locate file.
    """
    if os.path.isfile(start_path):
        start_dir_path = os.path.dirname(start_path)
    elif os.path.isdir(start_path):
        start_dir_path = start_path
    else:
        raise exceptions.FileNotFound("invalid path: {}".format(start_path))
    file_path = os.path.join(start_dir_path, file_name)
    if os.path.isfile(file_path):
        return file_path
    # current working directory
    if os.path.abspath(start_dir_path) in [os.getcwd(), os.path.abspath(os.sep)]:
        raise exceptions.FileNotFound(
            "{} not found in {}".format(file_name, start_path)
        )
    # locate recursive upward
    return locate_file(os.path.dirname(start_dir_path), file_name)
###############################################################################
##   debugtalk.py module loader
###############################################################################
def load_python_module(module):
    """load python module.
    Args:
        module: python module
    Returns:
        dict: variables and functions mapping for specified python module
            {
                "variables": {},
                "functions": {}
            }
    """
    debugtalk_module = {"variables": {}, "functions": {}}
    for name, item in vars(module).items():
        if validator.is_function((name, item)):
            debugtalk_module["functions"][name] = item
        elif validator.is_variable((name, item)):
            if isinstance(item, tuple):
                continue
            debugtalk_module["variables"][name] = item
        else:
            pass
    return debugtalk_module
def load_builtin_module():
    """load built_in module"""
    built_in_module = load_python_module(builtin)
    return built_in_module
def load_debugtalk_module():
    """load project debugtalk.py module
        debugtalk.py should be located in project working directory.
    Returns:
        dict: debugtalk module mapping
            {
                "variables": {},
                "functions": {}
            }
    """
    # load debugtalk.py module
    imported_module = importlib.import_module("debugtalk")
    debugtalk_module = load_python_module(imported_module)
    return debugtalk_module
def get_module_item(module_mapping, item_type, item_name):
    """get expected function or variable from module mapping.
    Args:
        module_mapping(dict): module mapping with variables and functions.
            {
                "variables": {},
                "functions": {}
            }
        item_type(str): "functions" or "variables"
        item_name(str): function name or variable name
    Returns:
        object: specified variable or function object.
    Raises:
        exceptions.FunctionNotFound: If specified function not found in module mapping
        exceptions.VariableNotFound: If specified variable not found in module mapping
    """
    try:
        return module_mapping[item_type][item_name]
    except KeyError:
        err_msg = "{} not found in debugtalk.py module!\n".format(item_name)
        err_msg += "module mapping: {}".format(module_mapping)
        if item_type == "functions":
            raise exceptions.FunctionNotFound(err_msg)
        else:
            raise exceptions.VariableNotFound(err_msg)
###############################################################################
##   testcase loader
###############################################################################
def _load_teststeps(test_block, project_mapping):
    """load teststeps with api/testcase references
    Args:
        test_block (dict): test block content, maybe in 3 formats.
            # api reference
            {
                "name": "add product to cart",
                "api": "api_add_cart()",
                "validate": []
            }
            # testcase reference
            {
                "name": "add product to cart",
                "suite": "create_and_check()",
                "validate": []
            }
            # define directly
            {
                "name": "checkout cart",
                "request": {},
                "validate": []
            }
    Returns:
        list: loaded teststeps list
    """
    def extend_api_definition(block):
        ref_call = block["api"]
        def_block = _get_block_by_name(ref_call, "def-api", project_mapping)
        _extend_block(block, def_block)
    teststeps = []
    # reference api
    if "api" in test_block:
        extend_api_definition(test_block)
        teststeps.append(test_block)
    # reference testcase
    elif "suite" in test_block:  # TODO: replace suite with testcase
        ref_call = test_block["suite"]
        block = _get_block_by_name(ref_call, "def-testcase", project_mapping)
        # TODO: bugfix lost block config variables
        for teststep in block["teststeps"]:
            if "api" in teststep:
                extend_api_definition(teststep)
            teststeps.append(teststep)
    # define directly
    else:
        teststeps.append(test_block)
    return teststeps
def _load_testcase(raw_testcase, project_mapping):
    """load testcase/testsuite with api/testcase references
    Args:
        raw_testcase (list): raw testcase content loaded from JSON/YAML file:
            [
                # config part
                {
                    "config": {
                        "name": "",
                        "def": "suite_order()",
                        "request": {}
                    }
                },
                # teststeps part
                {
                    "test": {...}
                },
                {
                    "test": {...}
                }
            ]
        project_mapping (dict): project_mapping
    Returns:
        dict: loaded testcase content
            {
                "config": {},
                "teststeps": [teststep11, teststep12]
            }
    """
    loaded_testcase = {"config": {}, "teststeps": []}
    for item in raw_testcase:
        # TODO: add json schema validation
        if not isinstance(item, dict) or len(item) != 1:
            raise exceptions.FileFormatError("Testcase format error: {}".format(item))
        key, test_block = item.popitem()
        if not isinstance(test_block, dict):
            raise exceptions.FileFormatError("Testcase format error: {}".format(item))
        if key == "config":
            loaded_testcase["config"].update(test_block)
        elif key == "test":
            loaded_testcase["teststeps"].extend(
                _load_teststeps(test_block, project_mapping)
            )
        else:
            logger.warning(
                "unexpected block key: {}. block key should only be 'config' or 'test'.".format(
                    key
                )
            )
    return loaded_testcase
def _get_block_by_name(ref_call, ref_type, project_mapping):
    """get test content by reference name.
    Args:
        ref_call (str): call function.
            e.g. api_v1_Account_Login_POST($UserName, $Password)
        ref_type (enum): "def-api" or "def-testcase"
        project_mapping (dict): project_mapping
    Returns:
        dict: api/testcase definition.
    Raises:
        exceptions.ParamsError: call args number is not equal to defined args number.
    """
    function_meta = parser.parse_function(ref_call)
    func_name = function_meta["func_name"]
    call_args = function_meta["args"]
    block = _get_test_definition(func_name, ref_type, project_mapping)
    def_args = block.get("function_meta", {}).get("args", [])
    if len(call_args) != len(def_args):
        err_msg = "{}: call args number is not equal to defined args number!\n".format(
            func_name
        )
        err_msg += "defined args: {}\n".format(def_args)
        err_msg += "reference args: {}".format(call_args)
        logger.error(err_msg)
        raise exceptions.ParamsError(err_msg)
    args_mapping = {}
    for index, item in enumerate(def_args):
        if call_args[index] == item:
            continue
        args_mapping[item] = call_args[index]
    if args_mapping:
        block = parser.substitute_variables(block, args_mapping)
    return block
def _get_test_definition(name, ref_type, project_mapping):
    """get expected api or testcase.
    Args:
        name (str): api or testcase name
        ref_type (enum): "def-api" or "def-testcase"
        project_mapping (dict): project_mapping
    Returns:
        dict: expected api/testcase info if found.
    Raises:
        exceptions.ApiNotFound: api not found
        exceptions.TestcaseNotFound: testcase not found
    """
    block = project_mapping.get(ref_type, {}).get(name)
    if not block:
        err_msg = "{} not found!".format(name)
        if ref_type == "def-api":
            raise exceptions.ApiNotFound(err_msg)
        else:
            # ref_type == "def-testcase":
            raise exceptions.TestcaseNotFound(err_msg)
    return block
def _extend_block(ref_block, def_block):
    """extend ref_block with def_block.
    Args:
        def_block (dict): api definition dict.
        ref_block (dict): reference block
    Returns:
        dict: extended reference block.
    Examples:
        >>> def_block = {
                "name": "get token 1",
                "request": {...},
                "validate": [{'eq': ['status_code', 200]}]
            }
        >>> ref_block = {
                "name": "get token 2",
                "extract": [{"token": "content.token"}],
                "validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
            }
        >>> _extend_block(def_block, ref_block)
            {
                "name": "get token 2",
                "request": {...},
                "extract": [{"token": "content.token"}],
                "validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
            }
    """
    # TODO: override variables
    def_validators = def_block.get("validate") or def_block.get("validators", [])
    ref_validators = ref_block.get("validate") or ref_block.get("validators", [])
    def_extrators = (
        def_block.get("extract")
        or def_block.get("extractors")
        or def_block.get("extract_binds", [])
    )
    ref_extractors = (
        ref_block.get("extract")
        or ref_block.get("extractors")
        or ref_block.get("extract_binds", [])
    )
    ref_block.update(def_block)
    ref_block["validate"] = _merge_validator(def_validators, ref_validators)
    ref_block["extract"] = _merge_extractor(def_extrators, ref_extractors)
def _convert_validators_to_mapping(validators):
    """convert validators list to mapping.
    Args:
        validators (list): validators in list
    Returns:
        dict: validators mapping, use (check, comparator) as key.
    Examples:
        >>> validators = [
                {"check": "v1", "expect": 201, "comparator": "eq"},
                {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
            ]
        >>> _convert_validators_to_mapping(validators)
            {
                ("v1", "eq"): {"check": "v1", "expect": 201, "comparator": "eq"},
                ('{"b": 1}', "eq"): {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
            }
    """
    validators_mapping = {}
    for validator in validators:
        validator = parser.parse_validator(validator)
        if not isinstance(validator["check"], collections.Hashable):
            check = json.dumps(validator["check"])
        else:
            check = validator["check"]
        key = (check, validator["comparator"])
        validators_mapping[key] = validator
    return validators_mapping
def _merge_validator(def_validators, ref_validators):
    """merge def_validators with ref_validators.
    Args:
        def_validators (list):
        ref_validators (list):
    Returns:
        list: merged validators
    Examples:
        >>> def_validators = [{'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"}]
        >>> ref_validators = [{"check": "v1", "expect": 201}, {'len_eq': ['s3', 12]}]
        >>> _merge_validator(def_validators, ref_validators)
            [
                {"check": "v1", "expect": 201, "comparator": "eq"},
                {"check": "s2", "expect": 16, "comparator": "len_eq"},
                {"check": "s3", "expect": 12, "comparator": "len_eq"}
            ]
    """
    if not def_validators:
        return ref_validators
    elif not ref_validators:
        return def_validators
    else:
        def_validators_mapping = _convert_validators_to_mapping(def_validators)
        ref_validators_mapping = _convert_validators_to_mapping(ref_validators)
        def_validators_mapping.update(ref_validators_mapping)
        return list(def_validators_mapping.values())
def _merge_extractor(def_extrators, ref_extractors):
    """merge def_extrators with ref_extractors
    Args:
        def_extrators (list): [{"var1": "val1"}, {"var2": "val2"}]
        ref_extractors (list): [{"var1": "val111"}, {"var3": "val3"}]
    Returns:
        list: merged extractors
    Examples:
        >>> def_extrators = [{"var1": "val1"}, {"var2": "val2"}]
        >>> ref_extractors = [{"var1": "val111"}, {"var3": "val3"}]
        >>> _merge_extractor(def_extrators, ref_extractors)
            [
                {"var1": "val111"},
                {"var2": "val2"},
                {"var3": "val3"}
            ]
    """
    if not def_extrators:
        return ref_extractors
    elif not ref_extractors:
        return def_extrators
    else:
        extractor_dict = OrderedDict()
        for api_extrator in def_extrators:
            if len(api_extrator) != 1:
                logger.warning("incorrect extractor: {}".format(api_extrator))
                continue
            var_name = list(api_extrator.keys())[0]
            extractor_dict[var_name] = api_extrator[var_name]
        for test_extrator in ref_extractors:
            if len(test_extrator) != 1:
                logger.warning("incorrect extractor: {}".format(test_extrator))
                continue
            var_name = list(test_extrator.keys())[0]
            extractor_dict[var_name] = test_extrator[var_name]
        extractor_list = []
        for key, value in extractor_dict.items():
            extractor_list.append({key: value})
        return extractor_list
def load_folder_content(folder_path):
    """load api/testcases/testsuites definitions from folder.
    Args:
        folder_path (str): api/testcases/testsuites files folder.
    Returns:
        dict: api definition mapping.
            {
                "tests/api/basic.yml": [
                    {"api": {"def": "api_login", "request": {}, "validate": []}},
                    {"api": {"def": "api_logout", "request": {}, "validate": []}}
                ]
            }
    """
    items_mapping = {}
    for file_path in load_folder_files(folder_path):
        items_mapping[file_path] = load_file(file_path)
    return items_mapping
def load_api_folder(api_folder_path):
    """load api definitions from api folder.
    Args:
        api_folder_path (str): api files folder.
            api file should be in the following format:
            [
                {
                    "api": {
                        "def": "api_login",
                        "request": {},
                        "validate": []
                    }
                },
                {
                    "api": {
                        "def": "api_logout",
                        "request": {},
                        "validate": []
                    }
                }
            ]
    Returns:
        dict: api definition mapping.
            {
                "api_login": {
                    "function_meta": {"func_name": "api_login", "args": [], "kwargs": {}}
                    "request": {}
                },
                "api_logout": {
                    "function_meta": {"func_name": "api_logout", "args": [], "kwargs": {}}
                    "request": {}
                }
            }
    """
    api_definition_mapping = {}
    api_items_mapping = load_folder_content(api_folder_path)
    for api_file_path, api_items in api_items_mapping.items():
        # TODO: add JSON schema validation
        for api_item in api_items:
            key, api_dict = api_item.popitem()
            api_def = api_dict.pop("def")
            function_meta = parser.parse_function(api_def)
            func_name = function_meta["func_name"]
            if func_name in api_definition_mapping:
                logger.warning("API definition duplicated: {}".format(func_name))
            api_dict["function_meta"] = function_meta
            api_definition_mapping[func_name] = api_dict
    return api_definition_mapping
def load_test_folder(test_folder_path):
    """load testcases definitions from folder.
    Args:
        test_folder_path (str): testcases files folder.
            testcase file should be in the following format:
            [
                {
                    "config": {
                        "def": "create_and_check",
                        "request": {},
                        "validate": []
                    }
                },
                {
                    "test": {
                        "api": "get_user",
                        "validate": []
                    }
                }
            ]
    Returns:
        dict: testcases definition mapping.
            {
                "create_and_check": [
                    {"config": {}},
                    {"test": {}},
                    {"test": {}}
                ],
                "tests/testcases/create_and_get.yml": [
                    {"config": {}},
                    {"test": {}},
                    {"test": {}}
                ]
            }
    """
    test_definition_mapping = {}
    test_items_mapping = load_folder_content(test_folder_path)
    for test_file_path, items in test_items_mapping.items():
        # TODO: add JSON schema validation
        testcase = {"config": {}, "teststeps": []}
        for item in items:
            key, block = item.popitem()
            if key == "config":
                testcase["config"].update(block)
                if "def" not in block:
                    test_definition_mapping[test_file_path] = testcase
                    continue
                testcase_def = block.pop("def")
                function_meta = parser.parse_function(testcase_def)
                func_name = function_meta["func_name"]
                if func_name in test_definition_mapping:
                    logger.warning("API definition duplicated: {}".format(func_name))
                testcase["function_meta"] = function_meta
                test_definition_mapping[func_name] = testcase
            else:
                # key == "test":
                testcase["teststeps"].append(block)
    return test_definition_mapping
def locate_debugtalk_py(start_path):
    """locate debugtalk.py file.
    Args:
        start_path (str): start locating path, maybe testcase file path or directory path
    """
    try:
        debugtalk_path = locate_file(start_path, "debugtalk.py")
        return os.path.abspath(debugtalk_path)
    except exceptions.FileNotFound:
        return None
def load_project_tests(test_path, dot_env_path=None):
    """load api, testcases, .env, builtin module and debugtalk.py.
        api/testcases folder is relative to project_working_directory
    Args:
        test_path (str): test file/folder path, locate pwd from this path.
        dot_env_path (str): specified .env file path
    Returns:
        dict: project loaded api/testcases definitions, environments and debugtalk.py module.
    """
    project_mapping = {}
    debugtalk_path = locate_debugtalk_py(test_path)
    # locate PWD with debugtalk.py path
    if debugtalk_path:
        # The folder contains debugtalk.py will be treated as PWD.
        project_working_directory = os.path.dirname(debugtalk_path)
    else:
        # debugtalk.py is not found, use os.getcwd() as PWD.
        project_working_directory = os.getcwd()
    # add PWD to sys.path
    sys.path.insert(0, project_working_directory)
    # load .env
    dot_env_path = dot_env_path or os.path.join(project_working_directory, ".env")
    if os.path.isfile(dot_env_path):
        project_mapping["env"] = load_dot_env_file(dot_env_path)
    else:
        project_mapping["env"] = {}
    # load debugtalk.py
    if debugtalk_path:
        project_mapping["debugtalk"] = load_debugtalk_module()
    else:
        project_mapping["debugtalk"] = {"variables": {}, "functions": {}}
    project_mapping["def-api"] = load_api_folder(
        os.path.join(project_working_directory, "api")
    )
    # TODO: replace suite with testcases
    project_mapping["def-testcase"] = load_test_folder(
        os.path.join(project_working_directory, "suite")
    )
    return project_mapping
def load_tests(path, dot_env_path=None):
    """load testcases from file path, extend and merge with api/testcase definitions.
    Args:
        path (str/list): testcase file/foler path.
            path could be in several types:
                - absolute/relative file path
                - absolute/relative folder path
                - list/set container with file(s) and/or folder(s)
        dot_env_path (str): specified .env file path
    Returns:
        list: testcases list, each testcase is corresponding to a file
        [
            {   # testcase data structure
                "config": {
                    "name": "desc1",
                    "path": "testcase1_path",
                    "variables": [],                    # optional
                    "request": {}                       # optional
                    "refs": {
                        "debugtalk": {
                            "variables": {},
                            "functions": {}
                        },
                        "env": {},
                        "def-api": {},
                        "def-testcase": {}
                    }
                },
                "teststeps": [
                    # teststep data structure
                    {
                        'name': 'test step desc1',
                        'variables': [],    # optional
                        'extract': [],      # optional
                        'validate': [],
                        'request': {},
                        'function_meta': {}
                    },
                    teststep2   # another teststep dict
                ]
            },
            testcase_dict_2     # another testcase dict
        ]
    """
    if isinstance(path, (list, set)):
        testcases_list = []
        for file_path in set(path):
            testcases = load_tests(file_path, dot_env_path)
            if not testcases:
                continue
            testcases_list.extend(testcases)
        return testcases_list
    if not os.path.exists(path):
        err_msg = "path not exist: {}".format(path)
        logger.error(err_msg)
        raise exceptions.FileNotFound(err_msg)
    if not os.path.isabs(path):
        path = os.path.join(os.getcwd(), path)
    if os.path.isdir(path):
        files_list = load_folder_files(path)
        testcases_list = load_tests(files_list, dot_env_path)
    elif os.path.isfile(path):
        try:
            raw_testcase = load_file(path)
            project_mapping = load_project_tests(path, dot_env_path)
            testcase = _load_testcase(raw_testcase, project_mapping)
            testcase["config"]["path"] = path
            testcase["config"]["refs"] = project_mapping
            testcases_list = [testcase]
        except exceptions.FileFormatError:
            testcases_list = []
    return testcases_list
def load_locust_tests(path, dot_env_path=None):
    """load locust testcases
    Args:
        path (str): testcase/testsuite file path.
        dot_env_path (str): specified .env file path
    Returns:
        dict: locust testcases with weight
        {
            "config": {...},
            "tests": [
                # weight 3
                [teststep11],
                [teststep11],
                [teststep11],
                # weight 2
                [teststep21, teststep22],
                [teststep21, teststep22]
            ]
        }
    """
    raw_testcase = load_file(path)
    project_mapping = load_project_tests(path, dot_env_path)
    config = {"refs": project_mapping}
    tests = []
    for item in raw_testcase:
        key, test_block = item.popitem()
        if key == "config":
            config.update(test_block)
        elif key == "test":
            teststeps = _load_teststeps(test_block, project_mapping)
            weight = test_block.pop("weight", 1)
            for _ in range(weight):
                tests.append(teststeps)
    return {"config": config, "tests": tests}
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/parser.py
New file
@@ -0,0 +1,838 @@
# encoding: utf-8
import ast
import logging
import re
# from loguru import logger
import logging
from httprunner import exceptions, utils
from httprunner.compat import basestring, builtin_str, numeric_types, str
variable_regexp = r"\$([\w_]+)"
function_regexp = r"\$\{([\w_]+\([\$\w\.\-/_ =,]*\))\}"
function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-/_ =,]*)\)$")
# use $$ to escape $ notation
dolloar_regex_compile = re.compile(r"\$\$")
# variable notation, e.g. ${var} or $var
variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
# function notation, e.g. ${func1($var_1, $var_3)}
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
logger = logging.getLogger(__name__)
def parse_string_value(str_value):
    """parse string to number if possible
    e.g. "123" => 123
         "12.2" => 12.3
         "abc" => "abc"
         "$var" => "$var"
    """
    try:
        return ast.literal_eval(str_value)
    except ValueError:
        return str_value
    except SyntaxError:
        # e.g. $var, ${func}
        return str_value
def extract_variables(content):
    """extract all variable names from content, which is in format $variable
    Args:
        content (str): string content
    Returns:
        list: variables list extracted from string content
    Examples:
        >>> extract_variables("$variable")
        ["variable"]
        >>> extract_variables("/blog/$postid")
        ["postid"]
        >>> extract_variables("/$var1/$var2")
        ["var1", "var2"]
        >>> extract_variables("abc")
        []
    """
    # TODO: change variable notation from $var to {{var}}
    try:
        return re.findall(variable_regexp, content)
    except TypeError:
        return []
def extract_functions(content):
    """extract all functions from string content, which are in format ${fun()}
    Args:
        content (str): string content
    Returns:
        list: functions list extracted from string content
    Examples:
        >>> extract_functions("${func(5)}")
        ["func(5)"]
        >>> extract_functions("${func(a=1, b=2)}")
        ["func(a=1, b=2)"]
        >>> extract_functions("/api/1000?_t=${get_timestamp()}")
        ["get_timestamp()"]
        >>> extract_functions("/api/${add(1, 2)}")
        ["add(1, 2)"]
        >>> extract_functions("/api/${add(1, 2)}?_t=${get_timestamp()}")
        ["add(1, 2)", "get_timestamp()"]
    """
    try:
        return re.findall(function_regexp, content)
    except TypeError:
        return []
def parse_function(content):
    """parse function name and args from string content.
    Args:
        content (str): string content
    Returns:
        dict: function meta dict
            {
                "func_name": "xxx",
                "args": [],
                "kwargs": {}
            }
    Examples:
        >>> parse_function("func()")
        {'func_name': 'func', 'args': [], 'kwargs': {}}
        >>> parse_function("func(5)")
        {'func_name': 'func', 'args': [5], 'kwargs': {}}
        >>> parse_function("func(1, 2)")
        {'func_name': 'func', 'args': [1, 2], 'kwargs': {}}
        >>> parse_function("func(a=1, b=2)")
        {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}}
        >>> parse_function("func(1, 2, a=3, b=4)")
        {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a':3, 'b':4}}
    """
    matched = function_regexp_compile.match(content)
    if not matched:
        raise exceptions.FunctionNotFound("{} not found!".format(content))
    function_meta = {"func_name": matched.group(1), "args": [], "kwargs": {}}
    args_str = matched.group(2).strip()
    if args_str == "":
        return function_meta
    args_list = args_str.split(",")
    for arg in args_list:
        arg = arg.strip()
        if "=" in arg:
            key, value = arg.split("=")
            function_meta["kwargs"][key.strip()] = parse_string_value(value.strip())
        else:
            function_meta["args"].append(parse_string_value(arg))
    return function_meta
def parse_validator(validator):
    """parse validator, validator maybe in two format
    @param (dict) validator
        format1: this is kept for compatiblity with the previous versions.
            {"check": "status_code", "comparator": "eq", "expect": 201}
            {"check": "$resp_body_success", "comparator": "eq", "expect": True}
        format2: recommended new version
            {'eq': ['status_code', 201]}
            {'eq': ['$resp_body_success', True]}
    @return (dict) validator info
        {
            "check": "status_code",
            "expect": 201,
            "comparator": "eq"
        }
    """
    if not isinstance(validator, dict):
        raise exceptions.ParamsError("invalid validator: {}".format(validator))
    if "check" in validator and len(validator) > 1:
        # format1
        check_item = validator.get("check")
        if "expect" in validator:
            expect_value = validator.get("expect")
        elif "expected" in validator:
            expect_value = validator.get("expected")
        else:
            raise exceptions.ParamsError("invalid validator: {}".format(validator))
        comparator = validator.get("comparator", "eq")
    elif len(validator) == 1:
        # format2
        comparator = list(validator.keys())[0]
        compare_values: list = validator[comparator]
        if len(compare_values) == 2:
            compare_values.append("")
        if not isinstance(compare_values, list) or len(compare_values) != 3:
            raise exceptions.ParamsError("invalid validator: {}".format(validator))
        if comparator in ("list_any_item_contains", "list_all_item_contains"):
            # list item比较器特殊检查
            if len(compare_values[1].split(" ")) != 3:
                msg = f"{compare_values} 是错误的表达式, 正确的期望值表达式比如:k = v,等号前后要有空格符"
                raise exceptions.ExpectValueParseFailure(msg)
        check_item, expect_value, desc = compare_values
    else:
        raise exceptions.ParamsError("invalid validator: {}".format(validator))
    return {
        "check": check_item,
        "expect": expect_value,
        "comparator": comparator,
        "desc": desc,
    }
def substitute_variables(content, variables_mapping):
    """substitute variables in content with variables_mapping
    Args:
        content (str/dict/list/numeric/bool/type): content to be substituted.
        variables_mapping (dict): variables mapping.
    Returns:
        substituted content.
    Examples:
        >>> content = {
                'request': {
                    'url': '/api/users/$uid',
                    'headers': {'token': '$token'}
                }
            }
        >>> variables_mapping = {"$uid": 1000}
        >>> substitute_variables(content, variables_mapping)
            {
                'request': {
                    'url': '/api/users/1000',
                    'headers': {'token': '$token'}
                }
            }
    """
    if isinstance(content, (list, set, tuple)):
        return [substitute_variables(item, variables_mapping) for item in content]
    if isinstance(content, dict):
        substituted_data = {}
        for key, value in content.items():
            eval_key = substitute_variables(key, variables_mapping)
            eval_value = substitute_variables(value, variables_mapping)
            substituted_data[eval_key] = eval_value
        return substituted_data
    if isinstance(content, basestring):
        # content is in string format here
        for var, value in variables_mapping.items():
            if content == var:
                # content is a variable
                content = value
            else:
                if not isinstance(value, str):
                    value = builtin_str(value)
                content = content.replace(var, value)
    return content
def parse_parameters(parameters, variables_mapping, functions_mapping):
    """parse parameters and generate cartesian product.
    Args:
        parameters (list) parameters: parameter name and value in list
            parameter value may be in three types:
                (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
                (2) call built-in parameterize function, "${parameterize(account.csv)}"
                (3) call custom function in debugtalk.py, "${gen_app_version()}"
        variables_mapping (dict): variables mapping loaded from debugtalk.py
        functions_mapping (dict): functions mapping loaded from debugtalk.py
    Returns:
        list: cartesian product list
    Examples:
        >>> parameters = [
            {"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]},
            {"username-password": "${parameterize(account.csv)}"},
            {"app_version": "${gen_app_version()}"}
        ]
        >>> parse_parameters(parameters)
    """
    parsed_parameters_list = []
    for parameter in parameters:
        parameter_name, parameter_content = list(parameter.items())[0]
        parameter_name_list = parameter_name.split("-")
        if isinstance(parameter_content, list):
            # (1) data list
            # e.g. {"app_version": ["2.8.5", "2.8.6"]}
            #       => [{"app_version": "2.8.5", "app_version": "2.8.6"}]
            # e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]}
            #       => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}]
            parameter_content_list = []
            for parameter_item in parameter_content:
                if not isinstance(parameter_item, (list, tuple)):
                    # "2.8.5" => ["2.8.5"]
                    parameter_item = [parameter_item]
                # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"}
                # ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"}
                parameter_content_dict = dict(zip(parameter_name_list, parameter_item))
                parameter_content_list.append(parameter_content_dict)
        else:
            # (2) & (3)
            parsed_parameter_content = parse_data(
                parameter_content, variables_mapping, functions_mapping
            )
            # e.g. [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}]
            # e.g. [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}]
            if not isinstance(parsed_parameter_content, list):
                raise exceptions.ParamsError("parameters syntax error!")
            parameter_content_list = [
                # get subset by parameter name
                {key: parameter_item[key] for key in parameter_name_list}
                for parameter_item in parsed_parameter_content
            ]
        parsed_parameters_list.append(parameter_content_list)
    return utils.gen_cartesian_product(*parsed_parameters_list)
###############################################################################
##  parse content with variables and functions mapping
###############################################################################
def get_builtin_item(item_type, item_name):
    """
    Args:
        item_type (enum): "variables" or "functions"
        item_name (str): variable name or function name
    Returns:
        variable or function with the name of item_name
    """
    # override built_in module with debugtalk.py module
    from httprunner import loader
    built_in_module = loader.load_builtin_module()
    if item_type == "variables":
        try:
            return built_in_module["variables"][item_name]
        except KeyError:
            raise exceptions.VariableNotFound("{} is not found.".format(item_name))
    else:
        # item_type == "functions":
        try:
            return built_in_module["functions"][item_name]
        except KeyError:
            raise exceptions.FunctionNotFound("{} is not found.".format(item_name))
def get_mapping_variable(variable_name, variables_mapping):
    """get variable from variables_mapping.
    Args:
        variable_name (str): variable name
        variables_mapping (dict): variables mapping
    Returns:
        mapping variable value.
    Raises:
        exceptions.VariableNotFound: variable is not found.
    """
    if variable_name in variables_mapping:
        return variables_mapping[variable_name]
    else:
        return get_builtin_item("variables", variable_name)
def get_mapping_function(function_name, functions_mapping):
    """get function from functions_mapping,
        if not found, then try to check if builtin function.
    Args:
        function_name (str): variable name
        functions_mapping (dict): variables mapping
    Returns:
        mapping function object.
    Raises:
        exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
    """
    if function_name in functions_mapping:
        return functions_mapping[function_name]
    try:
        return get_builtin_item("functions", function_name)
    except exceptions.FunctionNotFound:
        pass
    try:
        # check if builtin functions
        item_func = eval(function_name)
        if callable(item_func):
            # is builtin function
            return item_func
    except (NameError, TypeError):
        # is not builtin function
        raise exceptions.FunctionNotFound("{} is not found.".format(function_name))
def parse_string_functions(content, variables_mapping, functions_mapping):
    """parse string content with functions mapping.
    Args:
        content (str): string content to be parsed.
        variables_mapping (dict): variables mapping.
        functions_mapping (dict): functions mapping.
    Returns:
        str: parsed string content.
    Examples:
        >>> content = "abc${add_one(3)}def"
        >>> functions_mapping = {"add_one": lambda x: x + 1}
        >>> parse_string_functions(content, functions_mapping)
            "abc4def"
    """
    functions_list = extract_functions(content)
    for func_content in functions_list:
        function_meta = parse_function(func_content)
        func_name = function_meta["func_name"]
        args = function_meta.get("args", [])
        kwargs = function_meta.get("kwargs", {})
        args = parse_data(args, variables_mapping, functions_mapping)
        kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
        if func_name in ["parameterize", "P"]:
            from httprunner import loader
            eval_value = loader.load_csv_file(*args, **kwargs)
        else:
            func = get_mapping_function(func_name, functions_mapping)
            eval_value = func(*args, **kwargs)
        func_content = "${" + func_content + "}"
        if func_content == content:
            # content is a function, e.g. "${add_one(3)}"
            content = eval_value
        else:
            # content contains one or many functions, e.g. "abc${add_one(3)}def"
            content = content.replace(func_content, str(eval_value), 1)
    return content
def parse_string_variables(content, variables_mapping):
    """parse string content with variables mapping.
    Args:
        content (str): string content to be parsed.
        variables_mapping (dict): variables mapping.
    Returns:
        str: parsed string content.
    Examples:
        >>> content = "/api/users/$uid"
        >>> variables_mapping = {"$uid": 1000}
        >>> parse_string_variables(content, variables_mapping)
            "/api/users/1000"
    """
    variables_list = extract_variables(content)
    for variable_name in variables_list:
        variable_value = get_mapping_variable(variable_name, variables_mapping)
        # TODO: replace variable label from $var to {{var}}
        if "${}".format(variable_name) == content:
            # content is a variable
            content = variable_value
        else:
            # content contains one or several variables
            if not isinstance(variable_value, str):
                variable_value = builtin_str(variable_value)
            content = content.replace("${}".format(variable_name), variable_value, 1)
    return content
def parse_function_params(params) -> dict:
    """parse function params to args and kwargs.
    Args:
        params (str): function param in string
    Returns:
        dict: function meta dict
            {
                "args": [],
                "kwargs": {}
            }
    Examples:
        >>> parse_function_params("")
        {'args': [], 'kwargs': {}}
        >>> parse_function_params("5")
        {'args': [5], 'kwargs': {}}
        >>> parse_function_params("1, 2")
        {'args': [1, 2], 'kwargs': {}}
        >>> parse_function_params("a=1, b=2")
        {'args': [], 'kwargs': {'a': 1, 'b': 2}}
        >>> parse_function_params("1, 2, a=3, b=4")
        {'args': [1, 2], 'kwargs': {'a':3, 'b':4}}
    """
    function_meta = {"args": [], "kwargs": {}}
    params_str = params.strip()
    if params_str == "":
        return function_meta
    args_list = params_str.split(",")
    for arg in args_list:
        arg = arg.strip()
        if "=" in arg:
            key, value = arg.split("=")
            function_meta["kwargs"][key.strip()] = parse_string_value(value.strip())
        else:
            function_meta["args"].append(parse_string_value(arg))
    return function_meta
def parse_string(
    raw_string,
    variables_mapping,
    functions_mapping,
):
    """parse string content with variables and functions mapping.
    Args:
        raw_string: raw string content to be parsed.
        variables_mapping: variables mapping.
        functions_mapping: functions mapping.
    Returns:
        str: parsed string content.
    Examples:
        >>> raw_string = "abc${add_one($num)}def"
        >>> variables_mapping = {"num": 3}
        >>> functions_mapping = {"add_one": lambda x: x + 1}
        >>> parse_string(raw_string, variables_mapping, functions_mapping)
            "abc4def"
    """
    try:
        match_start_position = raw_string.index("$", 0)
        parsed_string = raw_string[0:match_start_position]
    except ValueError:
        parsed_string = raw_string
        return parsed_string
    while match_start_position < len(raw_string):
        # Notice: notation priority
        # $$ > ${func($a, $b)} > $var
        # search $$
        dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
        if dollar_match:
            match_start_position = dollar_match.end()
            parsed_string += "$"
            continue
        # search function like ${func($a, $b)}
        func_match = function_regex_compile.match(raw_string, match_start_position)
        if func_match:
            func_name = func_match.group(1)
            func = get_mapping_function(func_name, functions_mapping)
            func_params_str = func_match.group(2)
            function_meta = parse_function_params(func_params_str)
            args = function_meta["args"]
            kwargs = function_meta["kwargs"]
            parsed_args = parse_data(args, variables_mapping, functions_mapping)
            parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
            try:
                func_eval_value = func(*parsed_args, **parsed_kwargs)
            except Exception as ex:
                logger.error(
                    f"call function error:\n"
                    f"func_name: {func_name}\n"
                    f"args: {parsed_args}\n"
                    f"kwargs: {parsed_kwargs}\n"
                    f"{type(ex).__name__}: {ex}"
                )
                raise
            func_raw_str = "${" + func_name + f"({func_params_str})" + "}"
            if func_raw_str == raw_string:
                # raw_string is a function, e.g. "${add_one(3)}", return its eval value directly
                return func_eval_value
            # raw_string contains one or many functions, e.g. "abc${add_one(3)}def"
            parsed_string += str(func_eval_value)
            match_start_position = func_match.end()
            continue
        # search variable like ${var} or $var
        var_match = variable_regex_compile.match(raw_string, match_start_position)
        if var_match:
            var_name = var_match.group(1) or var_match.group(2)
            var_value = get_mapping_variable(var_name, variables_mapping)
            if f"${var_name}" == raw_string or "${" + var_name + "}" == raw_string:
                # raw_string is a variable, $var or ${var}, return its value directly
                return var_value
            # raw_string contains one or many variables, e.g. "abc${var}def"
            parsed_string += str(var_value)
            match_start_position = var_match.end()
            continue
        curr_position = match_start_position
        try:
            # find next $ location
            match_start_position = raw_string.index("$", curr_position + 1)
            remain_string = raw_string[curr_position:match_start_position]
        except ValueError:
            remain_string = raw_string[curr_position:]
            # break while loop
            match_start_position = len(raw_string)
        parsed_string += remain_string
    return parsed_string
def parse_data(content, variables_mapping=None, functions_mapping=None):
    """parse content with variables mapping
    Args:
        content (str/dict/list/numeric/bool/type): content to be parsed
        variables_mapping (dict): variables mapping.
        functions_mapping (dict): functions mapping.
    Returns:
        parsed content.
    Examples:
        >>> content = {
                'request': {
                    'url': '/api/users/$uid',
                    'headers': {'token': '$token'}
                }
            }
        >>> variables_mapping = {"uid": 1000, "token": "abcdef"}
        >>> parse_data(content, variables_mapping)
            {
                'request': {
                    'url': '/api/users/1000',
                    'headers': {'token': 'abcdef'}
                }
            }
    """
    # TODO: refactor type check
    if content is None or isinstance(content, (numeric_types, bool, type)):
        return content
    if isinstance(content, (list, set, tuple)):
        return [
            parse_data(item, variables_mapping, functions_mapping) for item in content
        ]
    if isinstance(content, dict):
        parsed_content = {}
        for key, value in content.items():
            parsed_key = parse_data(key, variables_mapping, functions_mapping)
            parsed_value = parse_data(value, variables_mapping, functions_mapping)
            parsed_content[parsed_key] = parsed_value
        return parsed_content
    if isinstance(content, basestring):
        variables_mapping = variables_mapping or {}
        functions_mapping = functions_mapping or {}
        content = parse_string(content, variables_mapping, functions_mapping)
        # replace $$ notation with $ and consider it as normal char.
        # if '$$' in content:
        #     return content.replace("$$", "$")
        # content is in string format here
        # variables_mapping = variables_mapping or {}
        # functions_mapping = functions_mapping or {}
        # content = content.strip()
        # # replace functions with evaluated value
        # # Notice: _eval_content_functions must be called before _eval_content_variables
        # content = parse_string_functions(content, variables_mapping, functions_mapping)
        #
        # # replace variables with binding value
        # content = parse_string_variables(content, variables_mapping)
    return content
def parse_tests(testcases, variables_mapping=None):
    """parse testcases configs, including variables/parameters/name/request.
    Args:
        testcases (list): testcase list, with config unparsed.
            [
                {   # testcase data structure
                    "config": {
                        "name": "desc1",
                        "path": "testcase1_path",
                        "variables": [],         # optional
                        "request": {}            # optional
                        "refs": {
                            "debugtalk": {
                                "variables": {},
                                "functions": {}
                            },
                            "env": {},
                            "def-api": {},
                            "def-testcase": {}
                        }
                    },
                    "teststeps": [
                        # teststep data structure
                        {
                            'name': 'test step desc2',
                            'variables': [],    # optional
                            'extract': [],      # optional
                            'validate': [],
                            'request': {},
                            'function_meta': {}
                        },
                        teststep2   # another teststep dict
                    ]
                },
                testcase_dict_2     # another testcase dict
            ]
        variables_mapping (dict): if variables_mapping is specified, it will override variables in config block.
    Returns:
        list: parsed testcases list, with config variables/parameters/name/request parsed.
    """
    variables_mapping = variables_mapping or {}
    parsed_testcases_list = []
    for testcase in testcases:
        testcase_config = testcase.setdefault("config", {})
        project_mapping = testcase_config.pop(
            "refs",
            {
                "debugtalk": {"variables": {}, "functions": {}},
                "env": {},
                "def-api": {},
                "def-testcase": {},
            },
        )
        # parse config parameters
        config_parameters = testcase_config.pop("parameters", [])
        cartesian_product_parameters_list = parse_parameters(
            config_parameters,
            project_mapping["debugtalk"]["variables"],
            project_mapping["debugtalk"]["functions"],
        ) or [{}]
        for parameter_mapping in cartesian_product_parameters_list:
            testcase_dict = utils.deepcopy_dict(testcase)
            config = testcase_dict.get("config")
            # parse config variables
            raw_config_variables = config.get("variables", [])
            parsed_config_variables = parse_data(
                raw_config_variables,
                project_mapping["debugtalk"]["variables"],
                project_mapping["debugtalk"]["functions"],
            )
            # priority: passed in > debugtalk.py > parameters > variables
            # override variables mapping with parameters mapping
            config_variables = utils.override_mapping_list(
                parsed_config_variables, parameter_mapping
            )
            # merge debugtalk.py module variables
            config_variables.update(project_mapping["debugtalk"]["variables"])
            # override variables mapping with passed in variables_mapping
            config_variables = utils.override_mapping_list(
                config_variables, variables_mapping
            )
            testcase_dict["config"]["variables"] = config_variables
            # parse config name
            testcase_dict["config"]["name"] = parse_data(
                testcase_dict["config"].get("name", ""),
                config_variables,
                project_mapping["debugtalk"]["functions"],
            )
            # parse config request
            testcase_dict["config"]["request"] = parse_data(
                testcase_dict["config"].get("request", {}),
                config_variables,
                project_mapping["debugtalk"]["functions"],
            )
            # put loaded project functions to config
            testcase_dict["config"]["functions"] = project_mapping["debugtalk"][
                "functions"
            ]
            parsed_testcases_list.append(testcase_dict)
    return parsed_testcases_list
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/report.py
New file
@@ -0,0 +1,282 @@
# encoding: utf-8
import io
import logging
import os
import platform
import time
import unittest
from base64 import b64encode
from collections import Iterable
from datetime import datetime
from jinja2 import Template
from markupsafe import escape
from httprunner.__about__ import __version__
from httprunner.compat import basestring, bytes, json, numeric_types
logger = logging.getLogger(__name__)
def get_platform():
    return {
        "httprunner_version": __version__,
        "python_version": "{} {}".format(
            platform.python_implementation(), platform.python_version()
        ),
        "platform": platform.platform(),
    }
def get_summary(result):
    """get summary from test result"""
    summary = {
        "success": result.wasSuccessful(),
        "stat": {
            "testsRun": result.testsRun,
            "failures": len(result.failures),
            "errors": len(result.errors),
            "skipped": len(result.skipped),
            "expectedFailures": len(result.expectedFailures),
            "unexpectedSuccesses": len(result.unexpectedSuccesses),
        },
    }
    summary["stat"]["successes"] = (
        summary["stat"]["testsRun"]
        - summary["stat"]["failures"]
        - summary["stat"]["errors"]
        - summary["stat"]["skipped"]
        - summary["stat"]["expectedFailures"]
        - summary["stat"]["unexpectedSuccesses"]
    )
    if getattr(result, "records", None):
        summary["time"] = {
            "start_at": result.start_at,
            "setup_hooks_duration": result.setup_hooks_duration,
            "teardown_hooks_duration": result.teardown_hooks_duration,
            "duration": result.duration,
        }
        summary["records"] = result.records
    else:
        summary["records"] = []
    return summary
def aggregate_stat(origin_stat, new_stat):
    """aggregate new_stat to origin_stat.
    Args:
        origin_stat (dict): origin stat dict, will be updated with new_stat dict.
        new_stat (dict): new stat dict.
    """
    for key in new_stat:
        if key not in origin_stat:
            origin_stat[key] = new_stat[key]
        elif key == "start_at":
            # start datetime
            origin_stat[key] = min(origin_stat[key], new_stat[key])
        else:
            origin_stat[key] += new_stat[key]
def render_html_report(summary, html_report_name=None, html_report_template=None):
    """render html report with specified report name and template
    if html_report_name is not specified, use current datetime
    if html_report_template is not specified, use default report template
    """
    if not html_report_template:
        html_report_template = os.path.join(
            os.path.abspath(os.path.dirname(__file__)),
            "templates",
            "report_template.html",
        )
        logger.debug("No html report template specified, use default.")
    else:
        logger.info("render with html report template: {}".format(html_report_template))
    logger.info("Start to render Html report ...")
    logger.debug("render data: {}".format(summary))
    report_dir_path = os.path.join(os.getcwd(), "reports")
    start_at_timestamp = int(summary["time"]["start_at"])
    summary["time"]["start_datetime"] = datetime.fromtimestamp(
        start_at_timestamp
    ).strftime("%Y-%m-%d %H:%M:%S")
    if html_report_name:
        summary["html_report_name"] = html_report_name
        report_dir_path = os.path.join(report_dir_path, html_report_name)
        html_report_name += "-{}.html".format(start_at_timestamp)
    else:
        summary["html_report_name"] = ""
        html_report_name = "{}.html".format(start_at_timestamp)
    if not os.path.isdir(report_dir_path):
        os.makedirs(report_dir_path)
    for index, suite_summary in enumerate(summary["details"]):
        if not suite_summary.get("name"):
            suite_summary["name"] = "test suite {}".format(index)
        for record in suite_summary.get("records"):
            meta_data = record["meta_data"]
            stringify_data(meta_data, "request")
            stringify_data(meta_data, "response")
    with io.open(html_report_template, "r", encoding="utf-8") as fp_r:
        template_content = fp_r.read()
        report_path = os.path.join(report_dir_path, html_report_name)
        with io.open(report_path, "w", encoding="utf-8") as fp_w:
            rendered_content = Template(
                template_content, extensions=["jinja2.ext.loopcontrols"]
            ).render(summary)
            fp_w.write(rendered_content)
    logger.info("Generated Html report: {}".format(report_path))
    return report_path
def stringify_data(meta_data, request_or_response):
    """
    meta_data = {
        "request": {},
        "response": {}
    }
    """
    headers = meta_data[request_or_response]["headers"]
    request_or_response_dict = meta_data[request_or_response]
    for key, value in request_or_response_dict.items():
        if isinstance(value, list):
            value = json.dumps(value, indent=2, ensure_ascii=False)
        elif isinstance(value, bytes):
            try:
                encoding = meta_data["response"].get("encoding")
                if not encoding or encoding == "None":
                    encoding = "utf-8"
                if (
                    request_or_response == "response"
                    and key == "content"
                    and "image" in meta_data["response"]["content_type"]
                ):
                    # display image
                    value = "data:{};base64,{}".format(
                        meta_data["response"]["content_type"],
                        b64encode(value).decode(encoding),
                    )
                else:
                    value = escape(value.decode(encoding))
            except UnicodeDecodeError:
                pass
        elif not isinstance(value, (basestring, numeric_types, Iterable)):
            # class instance, e.g. MultipartEncoder()
            value = repr(value)
        meta_data[request_or_response][key] = value
class HtmlTestResult(unittest.TextTestResult):
    """A html result class that can generate formatted html results.
    Used by TextTestRunner.
    """
    def __init__(self, stream, descriptions, verbosity):
        super(HtmlTestResult, self).__init__(stream, descriptions, verbosity)
        self.records = []
    def _record_test(self, test, status, attachment=""):
        data = {
            "name": test.shortDescription(),
            "status": status,
            "attachment": attachment,
            "meta_data": {},
        }
        if hasattr(test, "meta_data"):
            data["meta_data"] = test.meta_data
        self.records.append(data)
    def startTestRun(self):
        self.start_at = time.time()
    def startTest(self, test):
        """add start test time"""
        super(HtmlTestResult, self).startTest(test)
        logger.info(test.shortDescription())
    def addSuccess(self, test):
        super(HtmlTestResult, self).addSuccess(test)
        self._record_test(test, "success")
        print("")
    def addError(self, test, err):
        super(HtmlTestResult, self).addError(test, err)
        self._record_test(test, "error", self._exc_info_to_string(err, test))
        print("")
    def addFailure(self, test, err):
        super(HtmlTestResult, self).addFailure(test, err)
        self._record_test(test, "failure", self._exc_info_to_string(err, test))
        print("")
    def addSkip(self, test, reason):
        super(HtmlTestResult, self).addSkip(test, reason)
        self._record_test(test, "skipped", reason)
        print("")
    def addExpectedFailure(self, test, err):
        super(HtmlTestResult, self).addExpectedFailure(test, err)
        self._record_test(test, "ExpectedFailure", self._exc_info_to_string(err, test))
        print("")
    def addUnexpectedSuccess(self, test):
        super(HtmlTestResult, self).addUnexpectedSuccess(test)
        self._record_test(test, "UnexpectedSuccess")
        print("")
    @property
    def duration(self):
        case_elapsed = 0
        for record in self.records:
            elapsed_ms = record["meta_data"]["response"]["elapsed_ms"]
            if isinstance(elapsed_ms, (int, float)):
                case_elapsed += record["meta_data"]["response"]["elapsed_ms"]
        # 毫秒转秒级,保留三位
        total_duration = (
            case_elapsed / 1000
            + self.setup_hooks_duration
            + self.teardown_hooks_duration
        )
        return round(total_duration, 3)
    @property
    def setup_hooks_duration(self):
        # 整个case的前置函数消耗时间
        res = 0
        for record in self.records:
            setup_hooks_duration = record["meta_data"]["request"].get(
                "setup_hooks_duration"
            )
            if setup_hooks_duration:
                res += setup_hooks_duration
        return res
    @property
    def teardown_hooks_duration(self):
        # 整个case的后置函数消耗时间
        res = 0
        for record in self.records:
            teardown_hooks_duration = record["meta_data"]["response"].get(
                "teardown_hooks_duration"
            )
            if teardown_hooks_duration:
                res += teardown_hooks_duration
        return res
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/response.py
New file
@@ -0,0 +1,327 @@
# encoding: utf-8
import json
import re
import logging
import pydash
import jsonpath
from httprunner import exceptions, utils
from loguru import logger as log
from httprunner.compat import OrderedDict, basestring, is_py2
text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
list_condition_extractor_regexp_compile = re.compile(r'^for#\w+.*#\w.*')
logger = logging.getLogger(__name__)
class ResponseObject(object):
    def __init__(self, resp_obj):
        """ initialize with a requests.Response object
        @param (requests.Response instance) resp_obj
        """
        self.resp_obj = resp_obj
    def __getattr__(self, key):
        try:
            if key == "json":
                value = self.resp_obj.json()
            else:
                value = getattr(self.resp_obj, key)
            self.__dict__[key] = value
            return value
        except AttributeError:
            err_msg = "ResponseObject does not have attribute: {}".format(key)
            logger.error(err_msg)
            raise exceptions.ParamsError(err_msg)
    def _extract_field_with_regex(self, field):
        """ extract field from response content with regex.
            requests.Response body could be json or html text.
        @param (str) field should only be regex string that matched r".*\(.*\).*"
        e.g.
            self.text: "LB123abcRB789"
            field: "LB[\d]*(.*)RB[\d]*"
            return: abc
        """
        matched = re.search(field, self.text)
        if not matched:
            err_msg = u"Failed to extract data with regex! => {}\n".format(field)
            err_msg += u"response body: {}\n".format(self.text)
            logger.error(err_msg)
            raise exceptions.ExtractFailure(err_msg)
        return matched.group(1)
    def _extract_field_with_delimiter(self, field):
        """ response content could be json or html text.
        @param (str) field should be string joined by delimiter.
        e.g.
            "status_code"
            "headers"
            "cookies"
            "content"
            "headers.content-type"
            "content.person.name.first_name"
        support request body
        e.g.
            "request.body"
            "request.body.key"
        """
        # string.split(sep=None, maxsplit=-1) -> list of strings
        # e.g. "content.person.name" => ["content", "person.name"]
        try:
            top_query, sub_query = field.split('.', 1)
        except ValueError:
            top_query = field
            sub_query = None
        # request
        if top_query == 'request' and sub_query is not None:
            req = self.resp_obj.request
            if hasattr(req, 'body'):
                body = json.loads(req.body)
                if sub_query == 'body':
                    return body
                query_path = sub_query.replace('body.', '', 1)
                err_msg = f"request body not found: {field}"
                res = pydash.get(body, query_path, exceptions.ExtractFailure(err_msg))
                if isinstance(res, exceptions.ExtractFailure):
                    raise res
                return res
        # status_code
        if top_query in ["status_code", "encoding", "ok", "reason", "url"]:
            if sub_query:
                # status_code.XX
                err_msg = u"Failed to extract: {}\n".format(field)
                logger.error(err_msg)
                raise exceptions.ParamsError(err_msg)
            return getattr(self, top_query)
        # cookies
        elif top_query == "cookies":
            cookies = self.cookies.get_dict()
            if not sub_query:
                # extract cookies
                return cookies
            try:
                return cookies[sub_query]
            except KeyError:
                err_msg = u"Failed to extract cookie! => {}\n".format(field)
                err_msg += u"response cookies: {}\n".format(cookies)
                logger.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
        # elapsed
        elif top_query == "elapsed":
            available_attributes = u"available attributes: days, seconds, microseconds, total_seconds"
            if not sub_query:
                err_msg = u"elapsed is datetime.timedelta instance, attribute should also be specified!\n"
                err_msg += available_attributes
                logger.error(err_msg)
                raise exceptions.ParamsError(err_msg)
            elif sub_query in ["days", "seconds", "microseconds"]:
                return getattr(self.elapsed, sub_query)
            elif sub_query == "total_seconds":
                return self.elapsed.total_seconds()
            else:
                err_msg = "{} is not valid datetime.timedelta attribute.\n".format(sub_query)
                err_msg += available_attributes
                logger.error(err_msg)
                raise exceptions.ParamsError(err_msg)
        # headers
        elif top_query == "headers":
            headers = self.headers
            if not sub_query:
                # extract headers
                return headers
            try:
                return headers[sub_query]
            except KeyError:
                err_msg = u"Failed to extract header! => {}\n".format(field)
                err_msg += u"response headers: {}\n".format(headers)
                logger.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
        # response body
        elif top_query in ["content", "text", "json"]:
            try:
                body = self.json
            except exceptions.JSONDecodeError:
                body = self.text
            if not sub_query:
                # extract response body
                return body
            # 当body是dict时,使用jsonpath替换原有的取值方式
            return self._extract_with_jsonpath(body, field)
            if isinstance(body, (dict, list)):
                # content = {"xxx": 123}, content.xxx
                return utils.query_json(body, sub_query)
            elif sub_query.isdigit():
                # content = "abcdefg", content.3 => d
                return utils.query_json(body, sub_query)
            else:
                # content = "<html>abcdefg</html>", content.xxx
                err_msg = u"Failed to extract attribute from response body! => {}\n".format(field)
                err_msg += u"response body: {}\n".format(body)
                logger.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
        # new set response attributes in teardown_hooks
        elif top_query in self.__dict__:
            attributes = self.__dict__[top_query]
            if not sub_query:
                # extract response attributes
                return attributes
            if isinstance(attributes, (dict, list)):
                # attributes = {"xxx": 123}, content.xxx
                return utils.query_json(attributes, sub_query)
            elif sub_query.isdigit():
                # attributes = "abcdefg", attributes.3 => d
                return utils.query_json(attributes, sub_query)
            else:
                # content = "attributes.new_attribute_not_exist"
                err_msg = u"Failed to extract cumstom set attribute from teardown hooks! => {}\n".format(field)
                err_msg += u"response set attributes: {}\n".format(attributes)
                logger.error(err_msg)
                raise exceptions.TeardownHooksFailure(err_msg)
        # others
        else:
            err_msg = u"Failed to extract attribute from response! => {}\n".format(field)
            err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n"
            err_msg += u"If you want to set attribute in teardown_hooks, take the following example as reference:\n"
            err_msg += u"response.new_attribute = 'new_attribute_value'\n"
            logger.error(err_msg)
            raise exceptions.ParamsError(err_msg)
    def _extract_with_condition(self, field: str):
        """ condition extract
         for#content.res.list,id==1#content.a
        """
        field = field.replace(" ", "")
        separator = '#'
        keyword, valuepath_and_expression, extract_path = field.split(separator)
        if keyword == 'for':
            try:
                content = self.json
            except exceptions.JSONDecodeError:
                err_msg = "按条件提取只支持json格式的响应"
                log.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
            condition_list_path, expression = valuepath_and_expression.split(",")
            # 取值的时候,需要移除content.前缀
            condition_list = pydash.get(content,  condition_list_path.replace('content.', "", 1), None)
            err_msg = ""
            if not condition_list:
                err_msg = f'抽取条件:{condition_list_path}取值不存在'
            elif isinstance(condition_list, list) is False:
                err_msg = f'抽取条件的值只能是list类型,实际是{type(condition_list)}'
            if err_msg:
                log.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
            try:
                expect_path, expect_value = expression.split("==")
            except ValueError:
                err_msg = '抽取条件的表达式错误,正确写法如:id==1'
                log.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
            extract_value = None
            for d in condition_list:
                if expect_value == str(pydash.get(d, expect_path, "")):
                    # 当抽取条件满足时
                    # 如果抽取路径以content.开头,就从整个json取
                    # 否则,从当前的对象取
                    if extract_path.startswith('content.'):
                        extract_value = pydash.get(content, extract_path.replace('content.', "", 1))
                    else:
                        extract_value = pydash.get(d, extract_path)
                    break
            if not extract_value:
                err_msg = '抽取结果不存在'
                log.error(err_msg)
                raise exceptions.ExtractFailure(err_msg)
            return extract_value
    @staticmethod
    def _extract_with_jsonpath(obj: dict, field: str):
        path = field.replace("content", "$", 1)
        res: list = jsonpath.jsonpath(obj, path)
        if not res:
            err_msg = u"Failed to extract attribute from response body! => {}\n".format(field)
            err_msg += u"response body: {}\n".format(obj)
            raise exceptions.ExtractFailure(err_msg)
        else:
            return res[0]
    def extract_field(self, field):
        """ extract value from requests.Response.
        """
        if not isinstance(field, basestring):
            err_msg = u"Invalid extractor! => {}\n".format(field)
            logger.error(err_msg)
            raise exceptions.ParamsError(err_msg)
        msg = "extract: {}".format(field)
        if text_extractor_regexp_compile.match(field) and field.startswith("content.") is False:
            value = self._extract_field_with_regex(field)
        elif list_condition_extractor_regexp_compile.match(field.replace(" ", "")):
            value = self._extract_with_condition(field)
        else:
            value = self._extract_field_with_delimiter(field)
        if is_py2 and isinstance(value, unicode):
            value = value.encode("utf-8")
        msg += "\t=> {}".format(value)
        logger.debug(msg)
        return value
    def extract_response(self, extractors, context):
        """ extract value from requests.Response and store in OrderedDict.
        @param (list) extractors
            [
                {"resp_status_code": "status_code"},
                {"resp_headers_content_type": "headers.content-type"},
                {"resp_content": "content"},
                {"resp_content_person_first_name": "content.person.name.first_name"}
            ]
        @return (OrderDict) variable binds ordered dict
        """
        if not extractors:
            return {}
        logger.info("start to extract from response object.")
        extracted_variables_mapping = OrderedDict()
        extract_binds_order_dict = utils.convert_mappinglist_to_orderdict(extractors)
        for key, field in extract_binds_order_dict.items():
            if '$' in field:
                field = context.eval_content(field)
            extracted_variables_mapping[key] = self.extract_field(field)
        return extracted_variables_mapping
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/runner.py
New file
@@ -0,0 +1,327 @@
# encoding: utf-8
import threading
import logging
import pydash
import time
from unittest.case import SkipTest
from httprunner import exceptions, response, utils
from httprunner.client import HttpSession
from httprunner.compat import OrderedDict
from httprunner.context import Context
logger = logging.getLogger(__name__)
class Runner(object):
    # 每个线程对应Runner类的实例
    instances = {}
    def __init__(self, config_dict=None, http_client_session=None):
        """ """
        self.http_client_session = http_client_session
        config_dict = config_dict or {}
        self.evaluated_validators = []
        # testcase variables
        config_variables = config_dict.get("variables", {})
        # testcase functions
        config_functions = config_dict.get("functions", {})
        # testcase setup hooks
        testcase_setup_hooks = config_dict.pop("setup_hooks", [])
        # testcase teardown hooks
        self.testcase_teardown_hooks = config_dict.pop("teardown_hooks", [])
        self.context = Context(config_variables, config_functions)
        self.init_test(config_dict, "testcase")
        if testcase_setup_hooks:
            self.do_hook_actions(testcase_setup_hooks)
        Runner.instances[threading.current_thread().name] = self
    def __del__(self):
        if self.testcase_teardown_hooks:
            self.do_hook_actions(self.testcase_teardown_hooks)
    def init_test(self, test_dict, level):
        """create/update context variables binds
        Args:
            test_dict (dict):
            level (enum): "testcase" or "teststep"
                testcase:
                    {
                        "name": "testcase description",
                        "variables": [],   # optional
                        "request": {
                            "base_url": "http://127.0.0.1:5000",
                            "headers": {
                                "User-Agent": "iOS/2.8.3"
                            }
                        }
                    }
                teststep:
                    {
                        "name": "teststep description",
                        "variables": [],   # optional
                        "request": {
                            "url": "/api/get-token",
                            "method": "POST",
                            "headers": {
                                "Content-Type": "application/json"
                            }
                        },
                        "json": {
                            "sign": "f1219719911caae89ccc301679857ebfda115ca2"
                        }
                    }
        Returns:
            dict: parsed request dict
        """
        test_dict = utils.lower_test_dict_keys(test_dict)
        self.context.init_context_variables(level)
        variables = test_dict.get("variables") or test_dict.get(
            "variable_binds", OrderedDict()
        )
        self.context.update_context_variables(variables, level)
        request_config = test_dict.get("request", {})
        parsed_request = self.context.get_parsed_request(request_config, level)
        base_url = parsed_request.pop("base_url", None)
        self.http_client_session = self.http_client_session or HttpSession(base_url)
        return parsed_request
    def _handle_skip_feature(self, teststep_dict):
        """handle skip feature for teststep
            - skip: skip current test unconditionally
            - skipIf: skip current test if condition is true
            - skipUnless: skip current test unless condition is true
        Args:
            teststep_dict (dict): teststep info
        Raises:
            SkipTest: skip teststep
        """
        # TODO: move skip to initialize
        skip_reason = None
        if "skip" in teststep_dict:
            skip_reason = teststep_dict["skip"]
        elif "skipIf" in teststep_dict:
            skip_if_condition = teststep_dict["skipIf"]
            if self.context.eval_content(skip_if_condition):
                skip_reason = "{} evaluate to True".format(skip_if_condition)
        elif "skipUnless" in teststep_dict:
            skip_unless_condition = teststep_dict["skipUnless"]
            if not self.context.eval_content(skip_unless_condition):
                skip_reason = "{} evaluate to False".format(skip_unless_condition)
        if skip_reason:
            raise SkipTest(skip_reason)
    def do_hook_actions(self, actions):
        for action in actions:
            logger.debug("call hook: {}".format(action))
            # TODO: check hook function if valid
            self.context.eval_content(action)
    def run_test(self, teststep_dict):
        """run single teststep.
        Args:
            teststep_dict (dict): teststep info
                {
                    "name": "teststep description",
                    "skip": "skip this test unconditionally",
                    "times": 3,
                    "variables": [],        # optional, override
                    "request": {
                        "url": "http://127.0.0.1:5000/api/users/1000",
                        "method": "POST",
                        "headers": {
                            "Content-Type": "application/json",
                            "authorization": "$authorization",
                            "random": "$random"
                        },
                        "body": '{"name": "user", "password": "123456"}'
                    },
                    "extract": [],              # optional
                    "validate": [],             # optional
                    "setup_hooks": [],          # optional
                    "teardown_hooks": []        # optional
                }
        Raises:
            exceptions.ParamsError
            exceptions.ValidationFailure
            exceptions.ExtractFailure
        """
        # check skip
        self._handle_skip_feature(teststep_dict)
        # prepare
        extractors = teststep_dict.get("extract", []) or teststep_dict.get(
            "extractors", []
        )
        validators = teststep_dict.get("validate", []) or teststep_dict.get(
            "validators", []
        )
        parsed_request = self.init_test(teststep_dict, level="teststep")
        self.context.update_teststep_variables_mapping("request", parsed_request)
        # setup hooks
        setup_hooks = teststep_dict.get("setup_hooks", [])
        setup_hooks.insert(0, "${setup_hook_prepare_kwargs($request)}")
        setup_hooks_start = time.time()
        self.do_hook_actions(setup_hooks)
        # 计算前置setup_hooks消耗的时间
        setup_hooks_duration = 0
        self.http_client_session.meta_data["request"][
            "setup_hooks_start"
        ] = setup_hooks_start
        if len(setup_hooks) > 1:
            setup_hooks_duration = time.time() - setup_hooks_start
        self.http_client_session.meta_data["request"][
            "setup_hooks_duration"
        ] = setup_hooks_duration
        try:
            url = parsed_request.pop("url")
            method = parsed_request.pop("method")
            group_name = parsed_request.pop("group", None)
        except KeyError:
            raise exceptions.ParamsError("URL or METHOD missed!")
        # TODO: move method validation to json schema
        valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
        if method.upper() not in valid_methods:
            err_msg = "Invalid HTTP method! => {}\n".format(method)
            err_msg += "Available HTTP methods: {}".format("/".join(valid_methods))
            logger.error(err_msg)
            raise exceptions.ParamsError(err_msg)
        logger.info("{method} {url}".format(method=method, url=url))
        logger.debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request))
        user_timeout: str = str(pydash.get(parsed_request, "headers.timeout"))
        if user_timeout and user_timeout.isdigit():
            parsed_request["timeout"] = int(user_timeout)
        # request
        resp = self.http_client_session.request(
            method, url, name=group_name, **parsed_request
        )
        resp_obj = response.ResponseObject(resp)
        # teardown hooks
        teardown_hooks = teststep_dict.get("teardown_hooks", [])
        # 计算teardown_hooks消耗的时间
        teardown_hooks_duration = 0
        teardown_hooks_start = time.time()
        if teardown_hooks:
            logger.info("start to run teardown hooks")
            self.context.update_teststep_variables_mapping("response", resp_obj)
            self.do_hook_actions(teardown_hooks)
            teardown_hooks_duration = time.time() - teardown_hooks_start
        self.http_client_session.meta_data["response"][
            "teardown_hooks_start"
        ] = teardown_hooks_start
        self.http_client_session.meta_data["response"][
            "teardown_hooks_duration"
        ] = teardown_hooks_duration
        # extract
        extracted_variables_mapping = resp_obj.extract_response(
            extractors, self.context
        )
        self.context.update_testcase_runtime_variables_mapping(
            extracted_variables_mapping
        )
        # validate
        try:
            self.evaluated_validators = self.context.validate(validators, resp_obj)
        except (
            exceptions.ParamsError,
            exceptions.ValidationFailure,
            exceptions.ExtractFailure,
        ):
            # log request
            err_req_msg = "request: \n"
            err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {}))
            for k, v in parsed_request.items():
                err_req_msg += "{}: {}\n".format(k, repr(v))
            logger.error(err_req_msg)
            # log response
            err_resp_msg = "response: \n"
            err_resp_msg += "status_code: {}\n".format(resp_obj.status_code)
            err_resp_msg += "headers: {}\n".format(resp_obj.headers)
            err_resp_msg += "body: {}\n".format(repr(resp_obj.text))
            logger.error(err_resp_msg)
            raise
    def extract_output(self, output_variables_list):
        """extract output variables"""
        variables_mapping = self.context.teststep_variables_mapping
        output = {}
        for variable in output_variables_list:
            if variable not in variables_mapping:
                logger.warning(
                    "variable '{}' can not be found in variables mapping, failed to output!".format(
                        variable
                    )
                )
                continue
            output[variable] = variables_mapping[variable]
        return output
class Hrun(object):
    """
    特殊关键字,提供给驱动函数中使用
    可以在驱动函数中,修改配置变量和用例步骤运行时变量
    """
    @staticmethod
    def get_current_context():
        current_thread = threading.current_thread().name
        if Runner.instances.get(current_thread):
            return Runner.instances[current_thread].context
        return Runner().context
    @staticmethod
    def set_config_var(name, value):
        # 在运行时修改配置变量
        current_context = Hrun.get_current_context()
        current_context.TESTCASE_SHARED_VARIABLES_MAPPING[name] = value
    @staticmethod
    def set_config_header(name, value):
        # 在运行时修改配置中请求头的信息
        # 比如: 用例中需要切换账号,实现同时请求头中token和userId
        current_context = Hrun.get_current_context()
        pydash.set_(
            current_context.TESTCASE_SHARED_REQUEST_MAPPING, f"headers.{name}", value
        )
    @staticmethod
    def set_step_var(name, value):
        current_context = Hrun.get_current_context()
        current_context.testcase_runtime_variables_mapping[name] = value
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/locustfile.py
New file
@@ -0,0 +1,41 @@
import random
import zmq
from httprunner.exceptions import MyBaseError, MyBaseFailure
from httprunner.loader import load_locust_tests
from httprunner.runner import Runner
from locust import HttpLocust, TaskSet, task
from locust.events import request_failure
class WebPageTasks(TaskSet):
    def on_start(self):
        self.test_runner = Runner(self.client)
        self.testcases = loader.load_locust_tests(self.locust.file_path)
    @task(weight=1)
    def test_any(self):
        teststeps = random.choice(self.locust.tests)
        for teststep in teststeps:
            try:
                test_runner.run_test(teststep)
            except (MyBaseError, MyBaseFailure) as ex:
                request_failure.fire(
                    request_type=teststep.get("request", {}).get("method"),
                    name=teststep.get("name"),
                    response_time=0,
                    exception=ex
                )
                break
            gevent.sleep(1)
class WebPageUser(HttpLocust):
    host = "$HOST"
    task_set = WebPageTasks
    min_wait = 10
    max_wait = 30
    # file_path = "$TESTCASE_FILE"
    file_path = "tests/data/demo_locust.yml"
    config, tests = load_locust_tests(file_path)
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/locustfile_template
New file
@@ -0,0 +1,46 @@
import logging
import random
import zmq
from httprunner.exceptions import MyBaseError, MyBaseFailure
from httprunner.loader import load_locust_tests
from httprunner.runner import Runner
from locust import HttpLocust, TaskSet, task
from locust.events import request_failure
logging.getLogger().setLevel(logging.CRITICAL)
logging.getLogger('locust.main').setLevel(logging.INFO)
logging.getLogger('locust.runners').setLevel(logging.INFO)
class WebPageTasks(TaskSet):
    def on_start(self):
        self.test_runner = Runner(self.locust.config, self.client)
        self.testcases = load_locust_tests(self.locust.file_path)
    @task(weight=1)
    def test_any(self):
        teststeps = random.choice(self.locust.tests)
        for teststep in teststeps:
            try:
                self.test_runner.run_test(teststep)
            except (MyBaseError, MyBaseFailure) as ex:
                request_failure.fire(
                    request_type=teststep.get("request", {}).get("method"),
                    name=teststep.get("name"),
                    response_time=0,
                    exception=ex
                )
class WebPageUser(HttpLocust):
    task_set = WebPageTasks
    min_wait = 10
    max_wait = 30
    file_path = "$TESTCASE_FILE"
    locust_tests = load_locust_tests(file_path)
    config = locust_tests["config"]
    tests = locust_tests["tests"]
    host = config.get('request', {}).get('base_url', '')
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/templates/report_template.html
New file
@@ -0,0 +1,368 @@
<head>
  <meta content="text/html; charset=utf-8" http-equiv="content-type" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{html_report_name}} - TestReport</title>
  <style>
    body {
      background-color: #f2f2f2;
      color: #333;
      margin: 0 auto;
      width: 960px;
    }
    #summary {
      width: 960px;
      margin-bottom: 20px;
    }
    #summary th {
      background-color: skyblue;
      padding: 5px 12px;
    }
    #summary td {
      background-color: lightblue;
      text-align: center;
      padding: 4px 8px;
    }
    .details {
      width: 960px;
      margin-bottom: 20px;
    }
    .details th {
      background-color: skyblue;
      padding: 5px 12px;
    }
    .details tr .passed {
      background-color: lightgreen;
    }
    .details tr .failed {
      background-color: red;
    }
    .details tr .unchecked {
      background-color: gray;
    }
    .details td {
      background-color: lightblue;
      padding: 5px 12px;
    }
    .details .detail {
      background-color: lightgrey;
      font-size: smaller;
      padding: 5px 10px;
      text-align: center;
    }
    .details .success {
      background-color: greenyellow;
    }
    .details .error {
      background-color: red;
    }
    .details .failure {
      background-color: salmon;
    }
    .details .skipped {
      background-color: gray;
    }
    .button {
      font-size: 1em;
      padding: 6px;
      width: 4em;
      text-align: center;
      background-color: #06d85f;
      border-radius: 20px/50px;
      cursor: pointer;
      transition: all 0.3s ease-out;
    }
    a.button{
      color: gray;
      text-decoration: none;
    }
    .button:hover {
      background: #2cffbd;
    }
    .overlay {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      background: rgba(0, 0, 0, 0.7);
      transition: opacity 500ms;
      visibility: hidden;
      opacity: 0;
    }
    .overlay:target {
      visibility: visible;
      opacity: 1;
    }
    .popup {
      margin: 70px auto;
      padding: 20px;
      background: #fff;
      border-radius: 10px;
      width: 50%;
      position: relative;
      transition: all 3s ease-in-out;
    }
    .popup h2 {
      margin-top: 0;
      color: #333;
      font-family: Tahoma, Arial, sans-serif;
    }
    .popup .close {
      position: absolute;
      top: 20px;
      right: 30px;
      transition: all 200ms;
      font-size: 30px;
      font-weight: bold;
      text-decoration: none;
      color: #333;
    }
    .popup .close:hover {
      color: #06d85f;
    }
    .popup .content {
      max-height: 80%;
      overflow: auto;
      text-align: left;
    }
    @media screen and (max-width: 700px) {
      .box {
        width: 70%;
      }
      .popup {
        width: 70%;
      }
    }
  </style>
</head>
<body>
  <h1>Test Report: {{html_report_name}}</h1>
  <h2>Summary</h2>
  <table id="summary">
    <tr>
      <th>START AT</th>
      <td colspan="4">{{time.start_datetime}}</td>
    </tr>
    <tr>
      <th>DURATION</th>
      <td colspan="4">{{ '%0.3f'| format(time.duration|float) }} seconds</td>
    </tr>
    <tr>
      <th>PLATFORM</th>
      <td>HttpRunner {{ platform.httprunner_version }} </td>
      <td>{{ platform.python_version }} </td>
      <td colspan="2">{{ platform.platform }}</td>
    </tr>
    <tr>
      <th>TOTAL</th>
      <th>SUCCESS</th>
      <th>FAILED</th>
      <th>ERROR</th>
      <th>SKIPPED</th>
      <!-- <th>ExpectedFailure</th>
      <th>UnexpectedSuccess</th> -->
    </tr>
    <tr>
      <td>{{stat.testsRun}}</td>
      <td>{{stat.successes}}</td>
      <td>{{stat.failures}}</td>
      <td>{{stat.errors}}</td>
      <td>{{stat.skipped}}</td>
      <!-- <td>{{stat.expectedFailures}}</td>
      <td>{{stat.unexpectedSuccesses}}</td> -->
    </tr>
  </table>
  <h2>Details</h2>
  {% for test_suite_summary in details %}
  {% set suite_index = loop.index %}
  <h3>{{test_suite_summary.name}}</h3>
  <table id="suite_{{suite_index}}" class="details">
    <tr>
      <th>base_url</th>
      <td colspan="2">{{test_suite_summary.base_url}}</td>
      <th colspan="2" class="detail">
        <a class="button" href="#suite_output_{{suite_index}}">parameters & output</a>
        <div id="suite_output_{{suite_index}}" class="overlay">
            <div class="popup">
                <h2>Parameters and Output</h2>
                <a class="close" href="#suite_{{suite_index}}">&times;</a>
                <div class="content">
                  <div style="overflow: auto">
                      <table>
                        <tr>
                          <th>variables</th>
                          <th>output</th>
                        </tr>
                        {% if in_out in test_suite_summary %}
                        <tr>
                          <td>{{test_suite_summary.in_out.in}}</td>
                          <td>{{test_suite_summary.in_out.out}}</td>
                        </tr>
                        {% endif %}
                      </table>
                  </div>
                </div>
            </div>
        </div>
      </td>
    </tr>
    <tr>
      <td>TOTAL: {{test_suite_summary.stat.testsRun}}</td>
      <td>SUCCESS: {{test_suite_summary.stat.successes}}</td>
      <td>FAILED: {{test_suite_summary.stat.failures}}</td>
      <td>ERROR: {{test_suite_summary.stat.errors}}</td>
      <td>SKIPPED: {{test_suite_summary.stat.skipped}}</td>
    </tr>
    <tr>
      <th>Status</th>
      <th colspan="2">Name</th>
      <th>Response Time</th>
      <th>Detail</th>
    </tr>
    {% for record in test_suite_summary.records %}
    {% set record_index = "{}_{}".format(suite_index, loop.index) %}
    <tr id="record_{{record_index}}">
      <th class="{{record.status}}" style="width:5em;">{{record.status}}</td>
      <td colspan="2">{{record.name}}</td>
      <td style="text-align:center;width:6em;">{{ record.meta_data.response.response_time_ms }} ms</td>
      <td class="detail">
        <a class="button" href="#popup_log_{{record_index}}">log</a>
        <div id="popup_log_{{record_index}}" class="overlay">
          <div class="popup">
            <h2>Request and Response data</h2>
            <a class="close" href="#record_{{record_index}}">&times;</a>
            <div class="content">
              <h3>Request:</h3>
              <div style="overflow: auto">
                <table>
                  {% for key, value in record.meta_data.request.items() %}
                    <tr>
                      <th>{{key}}</th>
                      <td>
                        {% if key == "headers" %}
                          {% for header_key, header_value in record.meta_data.request.headers.items() %}
                          <div>
                            <strong>{{ header_key }}</strong>: {{ header_value }}
                          </div>
                          {% endfor %}
                        {% else %}
                          {{value}}
                        {% endif %}
                      </td>
                    </tr>
                  {% endfor %}
                </table>
              </div>
              <h3>Response:</h3>
              <div style="overflow: auto">
                <table>
                    {% for key, value in record.meta_data.response.items() %}
                      {% if key in ["text", "json", "elapsed_ms", "response_time_ms", "content_size", "content_type"] %}
                        {% continue %}
                      {% endif %}
                      <tr>
                        <th>{{key}}</th>
                        <td>
                          {% if key == "headers" %}
                            {% for header_key, header_value in record.meta_data.response.headers.items() %}
                            <div>
                              <strong>{{ header_key }}</strong>: {{ header_value }}
                            </div>
                            {% endfor %}
                          {% elif key == "content" %}
                            {% if "image" in record.meta_data.response.content_type %}
                              <img src="{{ record.meta_data.response.content }}" />
                            {% else %}
                              <pre>{{ record.meta_data.response.text | e }}</pre>
                            {% endif %}
                          {% else %}
                            {{value}}
                          {% endif %}
                        </td>
                      </tr>
                    {% endfor %}
                  </table>
              </div>
              <h3>Validators:</h3>
              <div style="overflow: auto">
                  <table>
                    <tr>
                      <th>check</th>
                      <th>comparator</th>
                      <th>expect value</th>
                      <th>actual value</th>
                    </tr>
                    {% for validator in record.meta_data.validators %}
                    <tr>
                      {% if validator.check_result == "pass" %}
                      <td class="passed">
                      {% elif validator.check_result == "fail" %}
                      <td class="failed">
                      {% elif validator.check_result == "unchecked" %}
                      <td class="unchecked">
                      {% endif %}
                        {{validator.check | e}}
                      </td>
                      <td>{{validator.comparator}}</td>
                      <td>{{validator.expect | e}}</td>
                      <td>{{validator.check_value | e}}</td>
                    </tr>
                    {% endfor %}
                  </table>
              </div>
              <h3>Statistics:</h3>
              <div style="overflow: auto">
                <table>
                  <tr>
                      <th>content_size(bytes)</th>
                      <td>{{ record.meta_data.response.content_size }}</td>
                    </tr>
                  <tr>
                    <th>response_time(ms)</th>
                    <td>{{ record.meta_data.response.response_time_ms }}</td>
                  </tr>
                  <tr>
                    <th>elapsed(ms)</th>
                    <td>{{ record.meta_data.response.elapsed_ms }}</td>
                  </tr>
                </table>
              </div>
            </div>
          </div>
        </div>
        {% if record.attachment %}
          <a class="button" href="#popup_attachment_{{record_index}}">traceback</a>
          <div id="popup_attachment_{{record_index}}" class="overlay">
            <div class="popup">
              <h2>Traceback Message</h2>
              <a class="close" href="#record_{{record_index}}">&times;</a>
              <div class="content"><pre>{{ record.attachment }}</pre></div>
            </div>
          </div>
        {% endif %}
      </td>
    </tr>
  {% endfor %}
  </table>
  {% endfor %}
</body>
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/utils.py
New file
@@ -0,0 +1,521 @@
# encoding: utf-8
import copy
import io
import itertools
import json
import os.path
import string
from datetime import datetime
import logging
from httprunner import exceptions
from httprunner.compat import OrderedDict, basestring, is_py2
logger = logging.getLogger(__name__)
def remove_prefix(text, prefix):
    """ remove prefix from text
    """
    if text.startswith(prefix):
        return text[len(prefix):]
    return text
def set_os_environ(variables_mapping):
    """ set variables mapping to os.environ
    """
    for variable in variables_mapping:
        os.environ[variable] = variables_mapping[variable]
        logger.debug("Loaded variable: {}".format(variable))
def query_json(json_content, query, delimiter='.'):
    """ Do an xpath-like query with json_content.
    @param (dict/list/string) json_content
        json_content = {
            "ids": [1, 2, 3, 4],
            "person": {
                "name": {
                    "first_name": "Leo",
                    "last_name": "Lee",
                },
                "age": 29,
                "cities": ["Guangzhou", "Shenzhen"]
            }
        }
    @param (str) query
        "person.name.first_name"  =>  "Leo"
        "person.name.first_name.0"  =>  "L"
        "person.cities.0"         =>  "Guangzhou"
    @return queried result
    """
    raise_flag = False
    response_body = u"response body: {}\n".format(json_content)
    try:
        for key in query.split(delimiter):
            if isinstance(json_content, (list, basestring)):
                json_content = json_content[int(key)]
            elif isinstance(json_content, dict):
                json_content = json_content[key]
            else:
                logger.error(
                    "invalid type value: {}({})".format(json_content, type(json_content)))
                raise_flag = True
    except (KeyError, ValueError, IndexError):
        raise_flag = True
    if raise_flag:
        err_msg = u"Failed to extract! => {}\n".format(query)
        err_msg += response_body
        logger.error(err_msg)
        raise exceptions.ExtractFailure(err_msg)
    return json_content
def get_uniform_comparator(comparator):
    """ convert comparator alias to uniform name
    """
    if comparator in ["eq", "equals", "==", "is"]:
        return "equals"
    elif comparator in ["lt", "less_than"]:
        return "less_than"
    elif comparator in ["le", "less_than_or_equals"]:
        return "less_than_or_equals"
    elif comparator in ["gt", "greater_than"]:
        return "greater_than"
    elif comparator in ["ge", "greater_than_or_equals"]:
        return "greater_than_or_equals"
    elif comparator in ["ne", "not_equals"]:
        return "not_equals"
    elif comparator in ["str_eq", "string_equals"]:
        return "string_equals"
    elif comparator in ["len_eq", "length_equals", "count_eq"]:
        return "length_equals"
    elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]:
        return "length_greater_than"
    elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals", \
        "count_greater_than_or_equals"]:
        return "length_greater_than_or_equals"
    elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
        return "length_less_than"
    elif comparator in ["len_le", "count_le", "length_less_than_or_equals", \
        "count_less_than_or_equals"]:
        return "length_less_than_or_equals"
    else:
        return comparator
def deep_update_dict(origin_dict, override_dict):
    """ update origin dict with override dict recursively
    e.g. origin_dict = {'a': 1, 'b': {'c': 2, 'd': 4}}
         override_dict = {'b': {'c': 3}}
    return: {'a': 1, 'b': {'c': 3, 'd': 4}}
    """
    if not override_dict:
        return origin_dict
    for key, val in override_dict.items():
        if isinstance(val, dict):
            tmp = deep_update_dict(origin_dict.get(key, {}), val)
            origin_dict[key] = tmp
        elif val is None:
            # fix #64: when headers in test is None, it should inherit from config
            continue
        else:
            origin_dict[key] = override_dict[key]
    return origin_dict
def lower_dict_keys(origin_dict):
    """ convert keys in dict to lower case
    Args:
        origin_dict (dict): mapping data structure
    Returns:
        dict: mapping with all keys lowered.
    Examples:
        >>> origin_dict = {
            "Name": "",
            "Request": "",
            "URL": "",
            "METHOD": "",
            "Headers": "",
            "Data": ""
        }
        >>> lower_dict_keys(origin_dict)
            {
                "name": "",
                "request": "",
                "url": "",
                "method": "",
                "headers": "",
                "data": ""
            }
    """
    if not origin_dict or not isinstance(origin_dict, dict):
        return origin_dict
    return {
        key.lower(): value
        for key, value in origin_dict.items()
    }
def lower_test_dict_keys(test_dict):
    """ convert keys in test_dict to lower case, convertion will occur in two places:
        1, all keys in test_dict;
        2, all keys in test_dict["request"]
    """
    # convert keys in test_dict
    test_dict = lower_dict_keys(test_dict)
    if "request" in test_dict:
        # convert keys in test_dict["request"]
        test_dict["request"] = lower_dict_keys(test_dict["request"])
    return test_dict
def convert_mappinglist_to_orderdict(mapping_list):
    """ convert mapping list to ordered dict
    Args:
        mapping_list (list):
            [
                {"a": 1},
                {"b": 2}
            ]
    Returns:
        OrderedDict: converted mapping in OrderedDict
            OrderDict(
                {
                    "a": 1,
                    "b": 2
                }
            )
    """
    ordered_dict = OrderedDict()
    for map_dict in mapping_list:
        ordered_dict.update(map_dict)
    return ordered_dict
def deepcopy_dict(data):
    """ deepcopy dict data, ignore file object (_io.BufferedReader)
    Args:
        data (dict): dict data structure
            {
                'a': 1,
                'b': [2, 4],
                'c': lambda x: x+1,
                'd': open('LICENSE'),
                'f': {
                    'f1': {'a1': 2},
                    'f2': io.open('LICENSE', 'rb'),
                }
            }
    Returns:
        dict: deep copied dict data, with file object unchanged.
    """
    try:
        return copy.deepcopy(data)
    except TypeError:
        copied_data = {}
        for key, value in data.items():
            if isinstance(value, dict):
                copied_data[key] = deepcopy_dict(value)
            else:
                try:
                    copied_data[key] = copy.deepcopy(value)
                except TypeError:
                    copied_data[key] = value
        return copied_data
def update_ordered_dict(ordered_dict, override_mapping):
    """ override ordered_dict with new mapping.
    Args:
        ordered_dict (OrderDict): original ordered dict
        override_mapping (dict): new variables mapping
    Returns:
        OrderDict: new overrided variables mapping.
    Examples:
        >>> ordered_dict = OrderDict({"a": 1, "b": 2})
        >>> override_mapping = {"a": 3, "c": 4}
        >>> update_ordered_dict(ordered_dict, override_mapping)
            OrderDict({"a": 3, "b": 2, "c": 4})
    """
    new_ordered_dict = copy.copy(ordered_dict)
    for var, value in override_mapping.items():
        new_ordered_dict.update({var: value})
    return new_ordered_dict
def override_mapping_list(variables, new_mapping):
    """ override variables with new mapping.
    Args:
        variables (list): variables list
            [
                {"var_a": 1},
                {"var_b": "world"}
            ]
        new_mapping (dict): overrided variables mapping
            {
                "var_a": "hello"
            }
    Returns:
        OrderedDict: overrided variables mapping.
    Examples:
        >>> variables = [
                {"var_a": 1},
                {"var_b": "world"}
            ]
        >>> new_mapping = {
                "var_a": "hello"
            }
        >>> override_mapping_list(variables, new_mapping)
            OrderedDict(
                {
                    "var_a": "hello",
                    "var_b": "world"
                }
            )
    """
    if isinstance(variables, list):
        variables_ordered_dict = convert_mappinglist_to_orderdict(variables)
    elif isinstance(variables, (OrderedDict, dict)):
        variables_ordered_dict = variables
    else:
        raise exceptions.ParamsError("variables error!")
    return update_ordered_dict(
        variables_ordered_dict,
        new_mapping
    )
def get_testcase_io(testcase):
    """ get testcase input(variables) and output.
    Args:
        testcase (unittest.suite.TestSuite): corresponding to one YAML/JSON file, it has been set two attributes:
            config: parsed config block
            runner: initialized runner.Runner() with config
    Returns:
        dict: input(variables) and output mapping.
    """
    runner = testcase.runner
    variables = testcase.config.get("variables", [])
    output_list = testcase.config.get("output", [])
    return {
        "in": dict(variables),
        "out": runner.extract_output(output_list)
    }
def print_io(in_out):
    """ print input(variables) and output.
    Args:
        in_out (dict): input(variables) and output mapping.
    Examples:
        >>> in_out = {
                "in": {
                    "var_a": "hello",
                    "var_b": "world"
                },
                "out": {
                    "status_code": 500
                }
            }
        >>> print_io(in_out)
        ================== Variables & Output ==================
        Type   | Variable         :  Value
        ------ | ---------------- :  ---------------------------
        Var    | var_a            :  hello
        Var    | var_b            :  world
        Out    | status_code      :  500
        --------------------------------------------------------
    """
    content_format = "{:<6} | {:<16} :  {:<}\n"
    content = "\n================== Variables & Output ==================\n"
    content += content_format.format("Type", "Variable", "Value")
    content += content_format.format("-" * 6, "-" * 16, "-" * 27)
    def prepare_content(var_type, in_out):
        content = ""
        for variable, value in in_out.items():
            if isinstance(value, tuple):
                continue
            elif isinstance(value, (dict, list)):
                value = json.dumps(value)
            if is_py2:
                if isinstance(variable, unicode):
                    variable = variable.encode("utf-8")
                if isinstance(value, unicode):
                    value = value.encode("utf-8")
            if value is None:
                value = 'None'
            content += content_format.format(var_type, variable, value)
        return content
    _in = in_out["in"]
    _out = in_out["out"]
    content += prepare_content("Var", _in)
    content += "\n"
    content += prepare_content("Out", _out)
    content += "-" * 56 + "\n"
    logger.debug(content)
def create_scaffold(project_name):
    """ create scaffold with specified project name.
    """
    if os.path.isdir(project_name):
        logger.warning(u"Folder {} exists, please specify a new folder name.".format(project_name))
        return
    logger.info("Start to create new project: {}".format(project_name), "GREEN")
    logger.info("CWD: {}\n".format(os.getcwd()), "BLUE")
    def create_path(path, ptype):
        if ptype == "folder":
            os.makedirs(path)
        elif ptype == "file":
            open(path, 'w').close()
        msg = "created {}: {}".format(ptype, path)
        logger.info(msg, "BLUE")
    path_list = [
        (project_name, "folder"),
        (os.path.join(project_name, "api"), "folder"),
        (os.path.join(project_name, "testcases"), "folder"),
        (os.path.join(project_name, "testsuites"), "folder"),
        (os.path.join(project_name, "reports"), "folder"),
        (os.path.join(project_name, "debugtalk.py"), "file"),
        (os.path.join(project_name, ".env"), "file")
    ]
    [create_path(p[0], p[1]) for p in path_list]
def gen_cartesian_product(*args):
    """ generate cartesian product for lists
    @param
        (list) args
            [{"a": 1}, {"a": 2}],
            [
                {"x": 111, "y": 112},
                {"x": 121, "y": 122}
            ]
    @return
        cartesian product in list
        [
            {'a': 1, 'x': 111, 'y': 112},
            {'a': 1, 'x': 121, 'y': 122},
            {'a': 2, 'x': 111, 'y': 112},
            {'a': 2, 'x': 121, 'y': 122}
        ]
    """
    if not args:
        return []
    elif len(args) == 1:
        return args[0]
    product_list = []
    for product_item_tuple in itertools.product(*args):
        product_item_dict = {}
        for item in product_item_tuple:
            product_item_dict.update(item)
        product_list.append(product_item_dict)
    return product_list
def validate_json_file(file_list):
    """ validate JSON testcase format
    """
    for json_file in set(file_list):
        if not json_file.endswith(".json"):
            logger.warning("Only JSON file format can be validated, skip: {}".format(json_file))
            continue
        logger.info("Start to validate JSON file: {}".format(json_file), "GREEN")
        with io.open(json_file) as stream:
            try:
                json.load(stream)
            except ValueError as e:
                raise SystemExit(e)
        print("OK")
def prettify_json_file(file_list):
    """ prettify JSON testcase format
    """
    for json_file in set(file_list):
        if not json_file.endswith(".json"):
            logger.warning("Only JSON file format can be prettified, skip: {}".format(json_file))
            continue
        logger.info("Start to prettify JSON file: {}".format(json_file), "GREEN")
        dir_path = os.path.dirname(json_file)
        file_name, file_suffix = os.path.splitext(os.path.basename(json_file))
        outfile = os.path.join(dir_path, "{}.pretty.json".format(file_name))
        with io.open(json_file, 'r', encoding='utf-8') as stream:
            try:
                obj = json.load(stream)
            except ValueError as e:
                raise SystemExit(e)
        with io.open(outfile, 'w', encoding='utf-8') as out:
            json.dump(obj, out, indent=4, separators=(',', ': '))
            out.write('\n')
        print("success: {}".format(outfile))
def get_python2_retire_msg():
    retire_day = datetime(2020, 1, 1)
    today = datetime.now()
    left_days = (retire_day - today).days
    if left_days > 0:
        retire_msg = "Python 2 will retire in {} days, why not move to Python 3?".format(left_days)
    else:
        retire_msg = "Python 2 has been retired, you should move to Python 3."
    return retire_msg
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/validator.py
New file
@@ -0,0 +1,129 @@
# encoding: utf-8
import os
import types
""" validate data format
TODO: refactor with JSON schema validate
"""
def is_testcase(data_structure):
    """check if data_structure is a testcase.
    Args:
        data_structure (dict): testcase should always be in the following data structure:
            {
                "config": {
                    "name": "desc1",
                    "variables": [],    # optional
                    "request": {}       # optional
                },
                "teststeps": [
                    teststep1,
                    {   # teststep2
                        'name': 'test step desc2',
                        'variables': [],    # optional
                        'extract': [],      # optional
                        'validate': [],
                        'request': {},
                        'function_meta': {}
                    }
                ]
            }
    Returns:
        bool: True if data_structure is valid testcase, otherwise False.
    """
    # TODO: replace with JSON schema validation
    if not isinstance(data_structure, dict):
        return False
    if "teststeps" not in data_structure:
        return False
    if not isinstance(data_structure["teststeps"], list):
        return False
    return True
def is_testcases(data_structure):
    """check if data_structure is testcase or testcases list.
    Args:
        data_structure (dict): testcase(s) should always be in the following data structure:
            testcase_dict
            or
            [
                testcase_dict_1,
                testcase_dict_2
            ]
    Returns:
        bool: True if data_structure is valid testcase(s), otherwise False.
    """
    if not isinstance(data_structure, list):
        return is_testcase(data_structure)
    for item in data_structure:
        if not is_testcase(item):
            return False
    return True
def is_testcase_path(path):
    """check if path is testcase path or path list.
    Args:
        path (str/list): file path or file path list.
    Returns:
        bool: True if path is valid file path or path list, otherwise False.
    """
    if not isinstance(path, (str, list)):
        return False
    if isinstance(path, list):
        for p in path:
            if not is_testcase_path(p):
                return False
    if isinstance(path, str):
        if not os.path.exists(path):
            return False
    return True
###############################################################################
##   validate varibles and functions
###############################################################################
def is_function(tup):
    """Takes (name, object) tuple, returns True if it is a function."""
    name, item = tup
    return isinstance(item, types.FunctionType)
def is_variable(tup):
    """Takes (name, object) tuple, returns True if it is a variable."""
    name, item = tup
    if callable(item):
        # function or class
        return False
    if isinstance(item, types.ModuleType):
        # imported module
        return False
    if name.startswith("_"):
        # private property
        return False
    return True
测试组/Test_platform/Interface_automation/backend/gunicorn_conf.py
New file
@@ -0,0 +1,16 @@
# gunicorn配置文件
import multiprocessing
# 并行工作进程数, int,cpu数量*2+1 推荐进程数
workers = multiprocessing.cpu_count() * 2 + 1
# 指定每个进程开启的线程数
threads = 2
# 绑定ip和端口号
bind = "0.0.0.0:8000"
# 使用gevent工作进程类型来处理请求
worker_class = "gevent"
forwarded_allow_ips = "*"
x_forwarded_for_header = "X-FORWARDED-FOR"
测试组/Test_platform/Interface_automation/backend/log_message
测试组/Test_platform/Interface_automation/backend/manage.py
New file
@@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
if __name__ == '__main__':
    main()
测试组/Test_platform/Interface_automation/backend/proxy.py
New file
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
@File    : proxy.py
@Time    : 2023/9/8 17:06
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 启动录制代理
"""
import asyncio
from loguru import logger
from backend import settings
from record_proxy import start_proxy
if __name__ == "__main__":
    if settings.PROXY_ON:
        asyncio.run(start_proxy(logger))
测试组/Test_platform/Interface_automation/backend/record_proxy/__init__.py
New file
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
@File    : __init__.py.py
@Time    : 2023/9/7 19:09
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : -
"""
from backend import settings
from record_proxy.record import Recorder
async def start_proxy(log):
    """
    启动代理
    :param log: 日志
    :return:
    """
    try:
        from mitmproxy import options
        from mitmproxy.tools.dump import DumpMaster
    except ImportError:
        log.bind(name=None).warning(
            "mitmproxy未安装,请参见: https://docs.mitmproxy.org/stable/overview-installation/"
        )
        return
    addons = [Recorder()]
    try:
        opts = options.Options(listen_host="0.0.0.0", listen_port=settings.PROXY_PORT)
        m = DumpMaster(opts, with_termlog=False, with_dumper=False)
        block_addon = m.addons.get("block")
        m.addons.remove(block_addon)
        m.addons.add(*addons)
        log.bind(name=None).debug(
            f"proxy server is running at http://0.0.0.0:{settings.PROXY_PORT}"
        )
        await m.run()
    except Exception as e:
        log.bind(name=None).error(
            f"proxy server running failed, if all nodes run failed, please check: {e}"
        )
测试组/Test_platform/Interface_automation/backend/record_proxy/record.py
New file
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""
@File    : record.py
@Time    : 2023/9/7 19:10
@Author  : geekbing
@LastEditTime : -
@LastEditors : -
@Description : 流量录制->生成case功能
"""
import json
import re
from backend.utils.redis_manager import RedisHelper
from apps.schema.request import RequestInfo
class Recorder:
    def request(self, flow):
        flow.request.headers["X-Forwarded-For"] = flow.client_conn.address[0]
    async def response(self, flow):
        if (
            "47.119.28.171" in flow.request.url
            or flow.request.method.lower() == "options"
            or flow.request.url.endswith(("js", "css", "ttf", "jpg", "svg", "gif"))
        ):
            # 如果是options请求,js等url直接拒绝
            return
        addr = flow.client_conn.address[0]
        record = RedisHelper.get_address_record(addr)
        if not record:
            return
        data = json.loads(record)
        user_id = data.get("user_id", "")
        pattern = re.compile(data.get("regex"))
        if re.findall(pattern, flow.request.url):
            # 说明已开启录制开关,记录状态
            request_data = RequestInfo(flow)
            dump_data = request_data.dumps()
            await RedisHelper.cache_record(user_id=user_id, request=dump_data)
            # TODO: 下个版本需要加入ws协议的支持
Diff truncated after the above file
测试组/Test_platform/Interface_automation/backend/requirements.txt 测试组/Test_platform/Interface_automation/backend/static/extent.js 测试组/Test_platform/Interface_automation/backend/static/images/favicon.ico 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/autocomplete.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/base.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/changelists.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/dashboard.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/fonts.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/forms.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/login.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/nav_sidebar.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/responsive.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/responsive_rtl.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/rtl.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/ui.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/LICENSE-SELECT2.md 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/select2.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/vendor/select2/select2.min.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/css/widgets.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/LICENSE.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/README.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Bold-webfont.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Light-webfont.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/fonts/Roboto-Regular-webfont.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/LICENSE 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/README.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/calendar-icons.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/gis/move_vertex_off.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/gis/move_vertex_on.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-addlink.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-alert.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-calendar.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-changelink.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-clock.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-deletelink.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-no.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-unknown-alt.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-unknown.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-viewlink.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/icon-yes.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/inline-delete.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/search.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/selector-icons.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/sorting-icons.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/tooltag-add.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/img/tooltag-arrowright.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/SelectBox.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/SelectFilter2.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/actions.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/actions.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/admin/DateTimeShortcuts.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/admin/RelatedObjectLookups.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/autocomplete.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/calendar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/cancel.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/change_form.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/collapse.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/collapse.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/core.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/inlines.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/inlines.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/jquery.init.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/nav_sidebar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/popup_response.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/prepopulate_init.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/timeparse.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/urlify.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/LICENSE-JQUERY.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/LICENSE.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/jquery.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/jquery/jquery.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/LICENSE-SELECT2.md 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/LICENSE.md 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/af.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/az.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bg.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bn.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/bs.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ca.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/cs.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/da.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/de.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/dsb.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/el.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/en.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/es.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/et.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/eu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fa.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/fr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/gl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/he.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hsb.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/hy.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/id.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/is.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/it.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ja.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ka.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/km.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ko.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/lt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/lv.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/mk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ms.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/nb.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ne.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/nl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ps.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pt-BR.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/pt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ro.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/ru.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sq.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sr-Cyrl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/sv.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/th.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/tk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/tr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/uk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/vi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/zh-CN.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/i18n/zh-TW.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/select2.full.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/select2/select2.full.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/LICENSE-XREGEXP.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/LICENSE.txt 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/xregexp.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/js/vendor/xregexp/xregexp.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/automatic/dicts.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/automatic/segment.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/base.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/index.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/css/login.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/alert.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/aside.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/autocomplete.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/avatar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/backtop.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/badge.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/breadcrumb-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/breadcrumb.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/button-group.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/button.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/calendar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/card.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/carousel-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/carousel.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/cascader-panel.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/cascader.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox-button.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox-group.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/checkbox.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/col.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/collapse-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/collapse.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/color-picker.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/container.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/date-picker.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dialog.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/directives/mousewheel.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/directives/repeat-click.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/divider.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/drawer.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown-menu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/dropdown.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/element-ui.common.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/footer.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/form-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/form.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/header.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/icon.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/image.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/index.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/infinite-scroll.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/input-number.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/input.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/link.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/loading.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/format.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/index.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/af-ZA.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/bg.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ca.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/cs-CZ.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/da.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/de.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ee.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/el.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/en.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/es.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/eu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fa.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/fr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/he.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/hy-AM.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/id.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/it.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ja.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/kg.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/km.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ko.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ku.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/kz.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/lt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/lv.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/mn.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/nb-NO.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/nl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pt-br.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/pt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ro.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ru-RU.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/sv-SE.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ta.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/th.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/tk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/tr-TR.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ua.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/ug-CN.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/uz-UZ.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/vi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/zh-CN.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/locale/lang/zh-TW.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/main.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu-item-group.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/menu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/message-box.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/message.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/emitter.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/focus.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/locale.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/mixins/migrating.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/notification.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/option-group.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/option.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/page-header.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/pagination.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/popover.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/progress.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio-button.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio-group.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/radio.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/rate.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/row.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/scrollbar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/select.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/slider.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/spinner.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/step.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/steps.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/submenu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/switch.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tab-pane.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/table-column.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/table.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tabs.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tag.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/alert.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/aside.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/autocomplete.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/avatar.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/backtop.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/badge.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/base.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/breadcrumb-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/breadcrumb.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/button-group.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/button.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/calendar.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/card.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/carousel-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/carousel.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/cascader-panel.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/cascader.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox-button.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox-group.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/checkbox.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/col.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/collapse-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/collapse.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/color-picker.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/container.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/date-picker.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dialog.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/display.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/divider.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/drawer.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown-menu.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/dropdown.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/fonts/element-icons.ttf 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/fonts/element-icons.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/footer.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/form-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/form.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/header.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/icon.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/image.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/index.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/infinite-scroll.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/infiniteScroll.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/input-number.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/input.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/link.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/loading.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/main.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu-item-group.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/menu.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/message-box.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/message.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/notification.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/option-group.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/option.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/page-header.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/pagination.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/popover.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/popper.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/progress.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio-button.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio-group.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/radio.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/rate.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/reset.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/row.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/scrollbar.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/select-dropdown.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/select.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/slider.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/spinner.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/step.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/steps.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/submenu.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/switch.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tab-pane.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/table-column.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/table.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tabs.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tag.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/time-picker.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/time-select.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/timeline-item.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/timeline.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tooltip.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/transfer.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/tree.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/theme-chalk/upload.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/time-picker.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/time-select.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/timeline-item.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/timeline.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tooltip.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/transfer.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/transitions/collapse-transition.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/tree.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/af-ZA.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/bg.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ca.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/cs-CZ.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/da.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/de.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ee.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/el.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/en.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/es.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/eu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fa.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/fr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/he.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/hy-AM.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/id.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/it.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ja.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/kg.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/km.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ko.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ku.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/kz.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/lt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/lv.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/mn.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/nb-NO.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/nl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pt-br.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/pt.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ro.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ru-RU.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sl.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sr.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/sv-SE.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ta.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/th.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/tk.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/tr-TR.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ua.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/ug-CN.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/uz-UZ.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/vi.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/zh-CN.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/umd/locale/zh-TW.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/upload.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/after-leave.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/aria-dialog.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/aria-utils.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/clickoutside.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/date-util.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/date.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/dom.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-menubar.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-menuitem.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/menu/aria-submenu.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/merge.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popper.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popup/index.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/popup/popup-manager.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/resize-event.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/scroll-into-view.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/scrollbar-width.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/shared.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/types.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/util.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/vdom.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/elementui/utils/vue-popper.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/css/all.min.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.eot 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.ttf 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-brands-400.woff2 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.eot 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.ttf 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-regular-400.woff2 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.eot 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.ttf 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.woff 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/fontawesome-free-5.8.1-web/webfonts/fa-solid-900.woff2 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/bg.svg 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/favicon.png 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/img/logo.png 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/cookie.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/index.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/language.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/login.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/js/vue.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/en-us.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/pt-br.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/locale/zh-hans.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/particles/app.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/particles/particles.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/admin.lte.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/aircraft.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/ant.design.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/base.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/black.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/dark.green.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black-pro.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-black.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue-pro.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-blue.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green-pro.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-green.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple-pro.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-purple.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red-pro.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/e-red.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/element.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/gray.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/green.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/highdmin.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/layui.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/light.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/orange.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/purple.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/simpleui.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/theme.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-blue.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-green.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.css.map 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/theme/x-red.less 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.css 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.js 测试组/Test_platform/Interface_automation/backend/static_root/admin/simpleui-x/waves/waves.min.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/README 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/immutable.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/immutable.min.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/insQ.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/insQ.min.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-init.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/LICENSE 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/redoc.min.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc-old/redoc.min.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/LICENSE 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc-logo.png 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc.min.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/redoc/redoc.standalone.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/style.css 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/LICENSE 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/NOTICE 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/absolute-path.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/favicon-32x32.png 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/index.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/oauth2-redirect.html 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.css 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.css.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-dist/swagger-ui.js.map 测试组/Test_platform/Interface_automation/backend/static_root/drf-yasg/swagger-ui-init.js 测试组/Test_platform/Interface_automation/backend/static_root/extent.js 测试组/Test_platform/Interface_automation/backend/static_root/images/favicon.ico 测试组/Test_platform/Interface_automation/backend/static_root/import_export/action_formats.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-theme.min.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-theme.min.css.map 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap-tweaks.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap.min.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/bootstrap.min.css.map 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/default.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/font-awesome-4.0.3.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/css/prettify.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/base.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/highlight.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/css/jquery.json-view.min.css 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/img/favicon.ico 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/img/grid.png 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/api.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/highlight.pack.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/docs/js/jquery.json-view.min.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.eot 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.svg 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.ttf 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/fontawesome-webfont.woff 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.eot 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.svg 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.ttf 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.woff 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/fonts/glyphicons-halflings-regular.woff2 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/glyphicons-halflings-white.png 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/glyphicons-halflings.png 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/img/grid.png 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/ajax-form.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/bootstrap.min.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/coreapi-0.1.1.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/csrf.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/default.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/jquery-3.5.1.min.js 测试组/Test_platform/Interface_automation/backend/static_root/rest_framework/js/prettify-min.js 测试组/Test_platform/Interface_automation/backend/tempWorkDir/.gitkeep 测试组/Test_platform/Interface_automation/backend/templates/report_template.html 测试组/Test_platform/Interface_automation/deployment/README.md 测试组/Test_platform/Interface_automation/deployment/celery/Dockerfile 测试组/Test_platform/Interface_automation/deployment/django/Dockerfile 测试组/Test_platform/Interface_automation/deployment/mysql/conf.d/my.cnf 测试组/Test_platform/Interface_automation/deployment/mysql/init/init.sql 测试组/Test_platform/Interface_automation/deployment/proxy/Dockerfile 测试组/Test_platform/Interface_automation/deployment/redis/redis.conf 测试组/Test_platform/Interface_automation/deployment/web/Dockerfile 测试组/Test_platform/Interface_automation/deployment/web/nginx.conf 测试组/Test_platform/Interface_automation/docker-compose.yml 测试组/Test_platform/Interface_automation/frontend/.babelrc 测试组/Test_platform/Interface_automation/frontend/.editorconfig 测试组/Test_platform/Interface_automation/frontend/.gitignore 测试组/Test_platform/Interface_automation/frontend/.postcssrc.js 测试组/Test_platform/Interface_automation/frontend/README.md 测试组/Test_platform/Interface_automation/frontend/babel.config.js 测试组/Test_platform/Interface_automation/frontend/build/build.js 测试组/Test_platform/Interface_automation/frontend/build/check-versions.js 测试组/Test_platform/Interface_automation/frontend/build/utils.js 测试组/Test_platform/Interface_automation/frontend/build/vue-loader.conf.js 测试组/Test_platform/Interface_automation/frontend/build/webpack.base.conf.js 测试组/Test_platform/Interface_automation/frontend/build/webpack.dev.conf.js 测试组/Test_platform/Interface_automation/frontend/build/webpack.prod.conf.js 测试组/Test_platform/Interface_automation/frontend/config/dev.env.js 测试组/Test_platform/Interface_automation/frontend/config/index.js 测试组/Test_platform/Interface_automation/frontend/config/prod.env.js 测试组/Test_platform/Interface_automation/frontend/index.html 测试组/Test_platform/Interface_automation/frontend/jsconfig.json 测试组/Test_platform/Interface_automation/frontend/package.json 测试组/Test_platform/Interface_automation/frontend/src/App.vue 测试组/Test_platform/Interface_automation/frontend/src/api/user.js 测试组/Test_platform/Interface_automation/frontend/src/assets/images/img.png 测试组/Test_platform/Interface_automation/frontend/src/assets/images/logo.svg 测试组/Test_platform/Interface_automation/frontend/src/assets/images/side-logo.svg 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/home.css 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.eot 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.svg 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.ttf 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/icon-font/iconfont.woff 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/iconfont.css 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/iconfont.js 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/reports.css 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/swagger.css 测试组/Test_platform/Interface_automation/frontend/src/assets/styles/tree.css 测试组/Test_platform/Interface_automation/frontend/src/main.js 测试组/Test_platform/Interface_automation/frontend/src/pages/home/Home.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Side.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/DebugTalk.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Extract.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Headers.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Hooks.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Parameters.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Request.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/RunCodeResult.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Validate.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/components/Variables.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/login/Logging.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/login/LoginLog.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/RecordApi.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/components/ApiBody.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/api/components/ApiList.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/AutoTest.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/RecordCase.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/TestCase.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/EditTest.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/TestBody.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/case/components/TestList.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/RecordConfig.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/components/ConfigBody.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/lunarlink/config/components/ConfigList.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/BaseMonacoEditor.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/javascript-completion.js 测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/log-language.js 测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/python-completion.js 测试组/Test_platform/Interface_automation/frontend/src/pages/monaco-editor/util/sql-completion.js 测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDashBoard.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDetail.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/reports/DebugReport.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/reports/ReportList.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/task/AddTasks.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/task/Tasks.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/variables/GlobalEnv.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/Crontab.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabDay.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabHour.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabMin.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabMouth.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabResult.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabSecond.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabWeek.vue 测试组/Test_platform/Interface_automation/frontend/src/pages/vcrontab/components/CrontabYear.vue 测试组/Test_platform/Interface_automation/frontend/src/restful/api.js 测试组/Test_platform/Interface_automation/frontend/src/router/index.js 测试组/Test_platform/Interface_automation/frontend/src/store/actions.js 测试组/Test_platform/Interface_automation/frontend/src/store/index.js 测试组/Test_platform/Interface_automation/frontend/src/store/mutations.js 测试组/Test_platform/Interface_automation/frontend/src/store/state.js 测试组/Test_platform/Interface_automation/frontend/src/util/bus.js 测试组/Test_platform/Interface_automation/frontend/src/util/format.js 测试组/Test_platform/Interface_automation/frontend/src/validator.js 测试组/Test_platform/Interface_automation/frontend/static/.gitkeep 测试组/Test_platform/Interface_automation/frontend/static/favicon.ico 测试组/Test_platform/Interface_automation/frontend/yarn.lock 测试组/Test_platform/Interface_automation/screenshots/1.png 测试组/Test_platform/Interface_automation/screenshots/2.png 测试组/Test_platform/Interface_automation/screenshots/3.png 测试组/Test_platform/Interface_automation/screenshots/4.png 测试组/Test_platform/Interface_automation/screenshots/5.gif 测试组/脚本/造数脚本