├── .idea └── inspectionProfiles │ └── Project_Default.xml ├── .travis.yml ├── Dockerfile ├── FasterRunner ├── __init__.py ├── auth.py ├── celery.py ├── pagination.py ├── settings.py ├── urls.py └── wsgi.py ├── LICENSE ├── README.md ├── docker-compose.md ├── fastrunner ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── tasks.py ├── templatetags │ ├── __init__.py │ └── custom_tags.py ├── urls.py ├── utils │ ├── __init__.py │ ├── decorator.py │ ├── host.py │ ├── loader.py │ ├── parser.py │ ├── prepare.py │ ├── response.py │ ├── runner.py │ ├── task.py │ └── tree.py └── views │ ├── __init__.py │ ├── api.py │ ├── config.py │ ├── project.py │ ├── report.py │ ├── run.py │ ├── schedule.py │ └── suite.py ├── fastuser ├── __init__.py ├── admin.py ├── apps.py ├── common │ ├── __init__.py │ ├── response.py │ └── token.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── urls.py └── views.py ├── manage.py ├── nginx.conf ├── requirements.txt ├── start.sh ├── templates └── report_template.html └── uwsgi.ini /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 3.5 5 | - 3.6 6 | install: 7 | - pip install -r requirements.txt 8 | - pip install coverage 9 | - pip install coveralls 10 | script: 11 | - coverage run --source=. -m unittest discover 12 | after_success: 13 | - coveralls -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | MAINTAINER yinquanwang 4 | 5 | ENV LANG C.UTF-8 6 | ENV TZ=Asia/Shanghai 7 | # Install required packages and remove the apt packages cache when done. 8 | 9 | RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \ 10 | apt-get clean && \ 11 | ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \ 12 | apt-get update && \ 13 | apt-get upgrade -y && \ 14 | apt-get install -y \ 15 | python3 \ 16 | python3-dev \ 17 | python3-setuptools \ 18 | python3-pip \ 19 | libmysqlclient-dev \ 20 | nginx \ 21 | tzdata && \ 22 | dpkg-reconfigure --frontend noninteractive tzdata 23 | 24 | WORKDIR /opt/workspace/FasterRunner/ 25 | 26 | COPY . . 27 | 28 | RUN pip3 install -r ./requirements.txt -i \ 29 | https://pypi.tuna.tsinghua.edu.cn/simple \ 30 | --default-timeout=100 && \ 31 | ln -s /opt/workspace/FasterRunner/nginx.conf /etc/nginx/sites-enabled/ 32 | 33 | EXPOSE 5000 34 | 35 | CMD bash ./start.sh 36 | 37 | 38 | -------------------------------------------------------------------------------- /FasterRunner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/FasterRunner/__init__.py -------------------------------------------------------------------------------- /FasterRunner/auth.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from rest_framework import exceptions 4 | from rest_framework.authentication import BaseAuthentication 5 | 6 | from FasterRunner.settings import INVALID_TIME 7 | from fastuser import models 8 | 9 | 10 | class Authenticator(BaseAuthentication): 11 | """ 12 | 账户鉴权认证 token 13 | """ 14 | 15 | def authenticate(self, request): 16 | 17 | token = request.query_params.get("token", None) 18 | obj = models.UserToken.objects.filter(token=token).first() 19 | 20 | if not obj: 21 | raise exceptions.AuthenticationFailed({ 22 | "code": "9998", 23 | "msg": "用户未认证", 24 | "success": False 25 | }) 26 | 27 | update_time = int(obj.update_time.timestamp()) 28 | current_time = int(time.time()) 29 | 30 | if current_time - update_time >= INVALID_TIME: 31 | raise exceptions.AuthenticationFailed({ 32 | "code": "9997", 33 | "msg": "登陆超时,请重新登陆", 34 | "success": False 35 | }) 36 | 37 | # valid update valid time 38 | obj.token = token 39 | obj.save() 40 | 41 | return obj.user, obj 42 | 43 | def authenticate_header(self, request): 44 | return 'Auth Failed' 45 | -------------------------------------------------------------------------------- /FasterRunner/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | # set the default Django settings module for the 'celery' program. 5 | from django.conf import settings 6 | 7 | 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') 9 | 10 | app = Celery('FasterRunner') 11 | 12 | # Using a string here means the worker doesn't have to serialize 13 | # the configuration object to child processes. 14 | # - namespace='CELERY' means all celery-related configuration keys 15 | # should have a `CELERY_` prefix. 16 | app.config_from_object('django.conf:settings') 17 | 18 | # Load task modules from all registered Django app configs. 19 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 20 | 21 | 22 | -------------------------------------------------------------------------------- /FasterRunner/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework import pagination 2 | 3 | 4 | class MyCursorPagination(pagination.CursorPagination): 5 | """ 6 | Cursor 光标分页 性能高,安全 7 | """ 8 | page_size = 9 9 | ordering = '-update_time' 10 | page_size_query_param = "pages" 11 | max_page_size = 20 12 | 13 | 14 | class MyPageNumberPagination(pagination.PageNumberPagination): 15 | """ 16 | 普通分页,数据量越大性能越差 17 | """ 18 | page_size = 11 19 | page_size_query_param = 'size' 20 | page_query_param = 'page' 21 | max_page_size = 20 22 | 23 | 24 | -------------------------------------------------------------------------------- /FasterRunner/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for FasterRunner project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | import djcelery 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'e$od9f28jce8q47u3raik$(e%$@lff6r89ux+=f!e1a$e42+#7' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | # Token Settings 31 | INVALID_TIME = 60 * 60 * 24 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'fastrunner.apps.FastrunnerConfig', 43 | 'fastuser', 44 | 'rest_framework', 45 | 'corsheaders', 46 | 'djcelery' 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'corsheaders.middleware.CorsMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'FasterRunner.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'FasterRunner.wsgi.application' 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 81 | 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.mysql', 86 | 'NAME': 'db_name', 87 | 'USER': 'username', 88 | 'PASSWORD': 'password', 89 | 'HOST': '127.0.0.1', 90 | 'PORT': '3306', 91 | } 92 | } 93 | 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'zh-Hans' 117 | 118 | TIME_ZONE = 'Asia/Shanghai' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = False 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 128 | 129 | STATIC_URL = '/static/' 130 | 131 | # rest_framework config 132 | 133 | REST_FRAMEWORK = { 134 | 'DEFAULT_AUTHENTICATION_CLASSES': ['FasterRunner.auth.Authenticator'], 135 | 'UNAUTHENTICATED_USER': None, 136 | 'UNAUTHENTICATED_TOKEN': None, 137 | # json form 渲染 138 | 'DEFAULT_PARSER_CLASSES': ['rest_framework.parsers.JSONParser', 139 | 'rest_framework.parsers.FormParser', 140 | 'rest_framework.parsers.MultiPartParser', 141 | 'rest_framework.parsers.FileUploadParser', 142 | ], 143 | 'DEFAULT_PAGINATION_CLASS': 'FasterRunner.pagination.MyPageNumberPagination', 144 | } 145 | 146 | CORS_ALLOW_CREDENTIALS = True 147 | CORS_ORIGIN_ALLOW_ALL = True 148 | CORS_ORIGIN_WHITELIST = () 149 | 150 | CORS_ALLOW_METHODS = ( 151 | 'DELETE', 152 | 'GET', 153 | 'OPTIONS', 154 | 'PATCH', 155 | 'POST', 156 | 'PUT', 157 | 'VIEW', 158 | ) 159 | 160 | CORS_ALLOW_HEADERS = ( 161 | 'accept', 162 | 'accept-encoding', 163 | 'authorization', 164 | 'content-type', 165 | 'dnt', 166 | 'origin', 167 | 'user-agent', 168 | 'x-csrftoken', 169 | 'x-requested-with', 170 | ) 171 | 172 | djcelery.setup_loader() 173 | CELERY_ENABLE_UTC = True 174 | CELERY_TIMEZONE = 'Asia/Shanghai' 175 | BROKER_URL = 'amqp://username:password@IP:5672//' 176 | CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' 177 | CELERY_RESULT_BACKEND = 'djcelery.backends.database:DatabaseBackend' 178 | CELERY_ACCEPT_CONTENT = ['application/json'] 179 | CELERY_TASK_SERIALIZER = 'json' 180 | CELERY_RESULT_SERIALIZER = 'json' 181 | 182 | CELERY_TASK_RESULT_EXPIRES = 7200 183 | CELERYD_CONCURRENCY = 1 if DEBUG else 5 184 | CELERYD_MAX_TASKS_PER_CHILD = 40 185 | 186 | LOGGING = { 187 | 'version': 1, 188 | 'disable_existing_loggers': True, 189 | 'formatters': { 190 | 'standard': { 191 | 'format': '%(asctime)s [%(levelname)s] - %(message)s'} 192 | # 日志格式 193 | }, 194 | 'filters': { 195 | }, 196 | 'handlers': { 197 | 'mail_admins': { 198 | 'level': 'ERROR', 199 | 'class': 'django.utils.log.AdminEmailHandler', 200 | 'include_html': True, 201 | }, 202 | 'default': { 203 | 'level': 'DEBUG', 204 | 'class': 'logging.handlers.RotatingFileHandler', 205 | 'filename': os.path.join(BASE_DIR, 'logs/debug.log'), 206 | 'maxBytes': 1024 * 1024 * 50, 207 | 'backupCount': 5, 208 | 'formatter': 'standard', 209 | }, 210 | 'console': { 211 | 'level': 'DEBUG', 212 | 'class': 'logging.StreamHandler', 213 | 'formatter': 'standard' 214 | }, 215 | 'request_handler': { 216 | 'level': 'INFO', 217 | 'class': 'logging.handlers.RotatingFileHandler', 218 | 'filename': os.path.join(BASE_DIR, 'logs/run.log'), 219 | 'maxBytes': 1024 * 1024 * 50, 220 | 'backupCount': 5, 221 | 'formatter': 'standard', 222 | }, 223 | 'scprits_handler': { 224 | 'level': 'INFO', 225 | 'class': 'logging.handlers.RotatingFileHandler', 226 | 'filename': os.path.join(BASE_DIR, 'logs/run.log'), 227 | 'maxBytes': 1024 * 1024 * 100, 228 | 'backupCount': 5, 229 | 'formatter': 'standard', 230 | }, 231 | }, 232 | 'loggers': { 233 | 'django': { 234 | 'handlers': ['default', 'console'], 235 | 'level': 'INFO', 236 | 'propagate': True 237 | }, 238 | 'FasterRunner.app': { 239 | 'handlers': ['default', 'console'], 240 | 'level': 'INFO', 241 | 'propagate': True 242 | }, 243 | 'django.request': { 244 | 'handlers': ['request_handler'], 245 | 'level': 'INFO', 246 | 'propagate': True 247 | }, 248 | 'FasterRunner': { 249 | 'handlers': ['scprits_handler', 'console'], 250 | 'level': 'INFO', 251 | 'propagate': True 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /FasterRunner/urls.py: -------------------------------------------------------------------------------- 1 | """FasterRunner URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('api/user/', include('fastuser.urls')), 21 | path('api/fastrunner/', include('fastrunner.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /FasterRunner/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for FasterRunner project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yinquanwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FasterRunner 2 | 3 | [![LICENSE](https://img.shields.io/github/license/HttpRunner/FasterRunner.svg)](https://github.com/HttpRunner/FasterRunner/blob/master/LICENSE) [![travis-ci](https://travis-ci.org/HttpRunner/FasterRunner.svg?branch=master)](https://travis-ci.org/HttpRunner/FasterRunner) ![pyversions](https://img.shields.io/pypi/pyversions/Django.svg) 4 | 5 | > FasterRunner that depends FasterWeb 6 | 7 | ``` 8 | 9 | ## Docker 部署 uwsgi+nginx模式 10 | 1. docker pull docker.io/mysql:5.7 # 拉取mysql5.7镜像 11 | 2. docker run --name mysql --net=host -d --restart always -v /var/lib/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=lcc123456 docker.io/mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci # 运行mysql容器 12 | 3. 连接数据库, 新建一个db,例如fastrunner 13 | 4. 修改settings.py DATABASES 字典相关配置,NAME, USER, PASSWORD, HOST 14 | 5. 启动rabbitmq docker run -d --name --net=host --restart always rabbitmq -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password rabbitmq:3-management 15 | 6. 修改settings.py BROKER_URL(配置rabbittmq的IP,username,password) 16 | 7. 切换到FasterRunner目录,Linux环境执行下 dos2unix ./start.sh # 因为windos编写的bash有编码问题 17 | 8. docker build -t fastrunner:latest . # 构建docker镜像 18 | 9. docker run -d --name fastrunner --net=host --restart always fastrunner:latest # 后台运行docker容器,默认后台端口5000 19 | 10. docker exec -it fastrunner /bin/sh #进入容器内部 20 | 11. 应用数据库表 21 | ``` bash 22 | 23 | # make migrations for fastuser、fastrunner 24 | python3 manage.py makemigrations fastrunner fastuser 25 | 26 | # migrate for database 27 | python3 manage.py migrate fastrunner 28 | python3 manage.py migrate fastuser 29 | python3 manage.py migrate djcelery 30 | ``` 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docker-compose.md: -------------------------------------------------------------------------------- 1 | # FasterWeb+FasterRunner docker-compose部署 2 | 3 | ## 一.安装docker-compose 4 | ```python 5 | # pip安装docker-compose 6 | pip install docker-compose 7 | # 找到docker-compose安装目录 /usr/local/python37/bin/docker-compose 8 | find / -name docker-compose 9 | # 创建docker-compose软链接 10 | ln -s /usr/local/python37/bin/docker-compose /usr/bin/docker-compose 11 | ``` 12 | 13 | -------- 14 | 15 | ## 二.配置docker-compose.yml,FasterRunner和FasterWeb 16 | #### 2.1 保存如下配置到docker-compose.yml 17 | 18 | 需要自定义的配置: 19 | 20 | **MYSQL_ROOT_PASSWORD**为mysql root账号密码 21 | **/root/workspace/FasterRunner**为FasterRunner根目录 22 | **/root/workspace/FasterWeb**为FasterWeb根目录 23 | 24 | 请自行设置 25 | 26 | ```python 27 | version: '3' 28 | services: 29 | # 容器名 30 | db: 31 | # 镜像 32 | image: docker.io/mysql:5.7 33 | # 获取root权限 34 | privileged: true 35 | # 环境变量 36 | environment: 37 | - MYSQL_DATABASE=FasterRunner 38 | - MYSQL_ROOT_PASSWORD=123456 39 | # 目录共享,格式 宿主机目录:容器目录 40 | volumes: 41 | - /var/lib/mysql:/var/lib/mysql 42 | # 端口映射,格式 宿主机端口:容器端口 43 | ports: 44 | - 3306:3306 45 | # 容器开机启动命令 46 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --socket=/var/lib/mysql/mysql.sock 47 | 48 | fasterunner: 49 | build: /root/workspace/FasterRunner 50 | image: fasterunner:latest 51 | # 依赖 52 | depends_on: 53 | - db 54 | privileged: true 55 | # 共享目录到容器,每次启动容器会copy宿主机代码 56 | volumes: 57 | - /root/workspace/FasterRunner:/share/fasterunner 58 | ports: 59 | - 8000:8000 60 | command: /bin/sh -c '\cp -rf /share/fasterunner/* /usr/src/app/ && python manage.py runserver 0.0.0.0:8000' 61 | 62 | fasterweb: 63 | build: /root/workspace/FasterWeb 64 | image: fasterweb:latest 65 | # 依赖 66 | privileged: true 67 | # 共享目录到容器,每次启动容器会copy宿主机代码 68 | volumes: 69 | - /root/workspace/FasterWeb:/share/fasterweb 70 | ports: 71 | - 8082:8082 72 | command: /bin/sh -c '\cp -rf /share/fasterweb/default.conf /etc/nginx/conf.d/ && \cp -rf /share/fasterweb/dist/ /usr/share/nginx/html/ && nginx -g "daemon off;"' 73 | 74 | ``` 75 | #### 2.2 更改FasterRunner setting.py数据库配置 76 | 77 | PASSWORD=MYSQL_ROOT_PASSWORD 78 | HOST=db (docker-compose中设置的mysql容器名) 79 | 80 | #### 2.3 更改FasterWeb配置 81 | 82 | * 修改default.conf配置文件 server_name的ip,注意为当前docker服务宿主机的ip地址!!! 83 | * 修改/src/restful/api.js baseUrl地址, 即为fastrunner容器运行的宿主机地址 84 | * 执行npm install, npm run build # 生成生产环境包 85 | 86 | ---------- 87 | ## 三.构建FasterWeb & FasterRunner镜像 88 | 89 | 在docker-compose.yml所在目录执行命令: 90 | ```python 91 | docker-compose build 92 | ``` 93 | ------- 94 | ## 四.启动容器 95 | 在docker-compose.yml所在目录执行命令: 96 | ```python 97 | docker-compose up -d 98 | ``` 99 | 100 | 备注:首次启动无mysql镜像时,会先pull mysql镜像再启动 101 | 102 | ------- 103 | 104 | ## 五.应用数据库表 105 | ```python 106 | docker exec -it fastrunner容器id /bin/sh #进入容器内部 107 | 108 | # make migrations for fastuser、fastrunner 109 | python manage.py makemigrations fastrunner fastuser 110 | 111 | # migrate for database 112 | python manage.py migrate fastrunner 113 | python manage.py migrate fastuser 114 | ``` 115 | ------ 116 | 117 | ## 六.登陆FasterRunner 118 | open url: http://宿主机ip:8082/#/fastrunner/register 119 | 120 | ------- 121 | 备注: 122 | docker-compose命令: 123 | ```python 124 | docker-compose build # 构建镜像 125 | docker-compose up -d # 启动容器 126 | docker-compose stop # 停止容器 127 | docker-compose restart # 重启容器 128 | ``` -------------------------------------------------------------------------------- /fastrunner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastrunner/__init__.py -------------------------------------------------------------------------------- /fastrunner/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /fastrunner/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FastrunnerConfig(AppConfig): 5 | name = 'fastrunner' 6 | -------------------------------------------------------------------------------- /fastrunner/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastrunner/migrations/__init__.py -------------------------------------------------------------------------------- /fastrunner/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | from fastuser.models import BaseTable 5 | 6 | 7 | class Project(BaseTable): 8 | """ 9 | 项目信息表 10 | """ 11 | 12 | class Meta: 13 | verbose_name = "项目信息" 14 | db_table = "Project" 15 | 16 | name = models.CharField("项目名称", unique=True, null=False, max_length=100) 17 | desc = models.CharField("简要介绍", max_length=100, null=False) 18 | responsible = models.CharField("创建人", max_length=20, null=False) 19 | 20 | 21 | class Debugtalk(models.Model): 22 | """ 23 | 驱动文件表 24 | """ 25 | 26 | class Meta: 27 | verbose_name = "驱动库" 28 | db_table = "Debugtalk" 29 | 30 | code = models.TextField("python代码", default="# write you code", null=False) 31 | project = models.OneToOneField(to=Project, on_delete=models.CASCADE) 32 | 33 | 34 | class Config(BaseTable): 35 | """ 36 | 环境信息表 37 | """ 38 | 39 | class Meta: 40 | verbose_name = "环境信息" 41 | db_table = "Config" 42 | 43 | name = models.CharField("环境名称", null=False, max_length=100) 44 | body = models.TextField("主体信息", null=False) 45 | base_url = models.CharField("请求地址", null=False, max_length=100) 46 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 47 | 48 | 49 | class API(BaseTable): 50 | """ 51 | API信息表 52 | """ 53 | 54 | class Meta: 55 | verbose_name = "接口信息" 56 | db_table = "API" 57 | 58 | name = models.CharField("接口名称", null=False, max_length=100) 59 | body = models.TextField("主体信息", null=False) 60 | url = models.CharField("请求地址", null=False, max_length=200) 61 | method = models.CharField("请求方式", null=False, max_length=10) 62 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 63 | relation = models.IntegerField("节点id", null=False) 64 | 65 | 66 | class Case(BaseTable): 67 | """ 68 | 用例信息表 69 | """ 70 | 71 | class Meta: 72 | verbose_name = "用例信息" 73 | db_table = "Case" 74 | 75 | tag = ( 76 | (1, "冒烟用例"), 77 | (2, "集成用例"), 78 | (3, "监控脚本") 79 | ) 80 | name = models.CharField("用例名称", null=False, max_length=100) 81 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 82 | relation = models.IntegerField("节点id", null=False) 83 | length = models.IntegerField("API个数", null=False) 84 | tag = models.IntegerField("用例标签", choices=tag, default=2) 85 | 86 | 87 | class CaseStep(BaseTable): 88 | """ 89 | Test Case Step 90 | """ 91 | 92 | class Meta: 93 | verbose_name = "用例信息 Step" 94 | db_table = "CaseStep" 95 | 96 | name = models.CharField("用例名称", null=False, max_length=100) 97 | body = models.TextField("主体信息", null=False) 98 | url = models.CharField("请求地址", null=False, max_length=200) 99 | method = models.CharField("请求方式", null=False, max_length=10) 100 | case = models.ForeignKey(Case, on_delete=models.CASCADE) 101 | step = models.IntegerField("顺序", null=False) 102 | 103 | 104 | class HostIP(BaseTable): 105 | """ 106 | 全局变量 107 | """ 108 | 109 | class Meta: 110 | verbose_name = "HOST配置" 111 | db_table = "HostIP" 112 | 113 | name = models.CharField(null=False, max_length=100) 114 | value = models.TextField(null=False) 115 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 116 | 117 | 118 | class Variables(BaseTable): 119 | """ 120 | 全局变量 121 | """ 122 | 123 | class Meta: 124 | verbose_name = "全局变量" 125 | db_table = "Variables" 126 | 127 | key = models.CharField(null=False, max_length=100) 128 | value = models.CharField(null=False, max_length=1024) 129 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 130 | 131 | 132 | 133 | class Report(BaseTable): 134 | """ 135 | 报告存储 136 | """ 137 | report_type = ( 138 | (1, "调试"), 139 | (2, "异步"), 140 | (3, "定时") 141 | ) 142 | 143 | class Meta: 144 | verbose_name = "测试报告" 145 | db_table = "Report" 146 | 147 | name = models.CharField("报告名称", null=False, max_length=100) 148 | type = models.IntegerField("报告类型", choices=report_type) 149 | summary = models.TextField("主体信息", null=False) 150 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 151 | 152 | 153 | class Relation(models.Model): 154 | """ 155 | 树形结构关系 156 | """ 157 | 158 | class Meta: 159 | verbose_name = "树形结构关系" 160 | db_table = "Relation" 161 | 162 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 163 | tree = models.TextField("结构主题", null=False, default=[]) 164 | type = models.IntegerField("树类型", default=1) 165 | 166 | [ 167 | { 168 | "name": "testcase", 169 | "body": "body", 170 | "url": "https://www.baidu.com", 171 | "method": "post", 172 | "project": "1", 173 | "relation": 1 174 | } 175 | ] 176 | -------------------------------------------------------------------------------- /fastrunner/serializers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework import serializers 4 | from fastrunner import models 5 | from fastrunner.utils.parser import Parse 6 | from djcelery import models as celery_models 7 | 8 | 9 | class ProjectSerializer(serializers.ModelSerializer): 10 | """ 11 | 项目信息序列化 12 | """ 13 | 14 | class Meta: 15 | model = models.Project 16 | fields = ['id', 'name', 'desc', 'responsible', 'update_time'] 17 | 18 | 19 | class DebugTalkSerializer(serializers.ModelSerializer): 20 | """ 21 | 驱动代码序列化 22 | """ 23 | 24 | class Meta: 25 | model = models.Debugtalk 26 | fields = ['id', 'code'] 27 | 28 | 29 | class RelationSerializer(serializers.ModelSerializer): 30 | """ 31 | 树形结构序列化 32 | """ 33 | 34 | class Meta: 35 | model = models.Relation 36 | fields = '__all__' 37 | 38 | 39 | class APISerializer(serializers.ModelSerializer): 40 | """ 41 | 接口信息序列化 42 | """ 43 | body = serializers.SerializerMethodField() 44 | 45 | class Meta: 46 | model = models.API 47 | fields = ['id', 'name', 'url', 'method', 'project', 'relation', 'body'] 48 | 49 | def get_body(self, obj): 50 | parse = Parse(eval(obj.body)) 51 | parse.parse_http() 52 | return parse.testcase 53 | 54 | 55 | class CaseSerializer(serializers.ModelSerializer): 56 | """ 57 | 用例信息序列化 58 | """ 59 | tag = serializers.CharField(source="get_tag_display") 60 | 61 | class Meta: 62 | model = models.Case 63 | fields = '__all__' 64 | 65 | 66 | class CaseStepSerializer(serializers.ModelSerializer): 67 | """ 68 | 用例步骤序列化 69 | """ 70 | body = serializers.SerializerMethodField() 71 | 72 | class Meta: 73 | model = models.CaseStep 74 | fields = ['id', 'name', 'url', 'method', 'body', 'case'] 75 | depth = 1 76 | 77 | def get_body(self, obj): 78 | body = eval(obj.body) 79 | if "base_url" in body["request"].keys(): 80 | return { 81 | "name": body["name"], 82 | "method": "config" 83 | } 84 | else: 85 | parse = Parse(eval(obj.body)) 86 | parse.parse_http() 87 | return parse.testcase 88 | 89 | 90 | class ConfigSerializer(serializers.ModelSerializer): 91 | """ 92 | 配置信息序列化 93 | """ 94 | body = serializers.SerializerMethodField() 95 | 96 | class Meta: 97 | model = models.Config 98 | fields = ['id', 'base_url', 'body', 'name', 'update_time'] 99 | depth = 1 100 | 101 | def get_body(self, obj): 102 | parse = Parse(eval(obj.body), level='config') 103 | parse.parse_http() 104 | return parse.testcase 105 | 106 | 107 | class ReportSerializer(serializers.ModelSerializer): 108 | """ 109 | 报告信息序列化 110 | """ 111 | type = serializers.CharField(source="get_type_display") 112 | time = serializers.SerializerMethodField() 113 | stat = serializers.SerializerMethodField() 114 | platform = serializers.SerializerMethodField() 115 | success = serializers.SerializerMethodField() 116 | 117 | class Meta: 118 | model = models.Report 119 | fields = ["id", "name", "type", "time", "stat", "platform", "success"] 120 | 121 | def get_time(self, obj): 122 | return json.loads(obj.summary)["time"] 123 | 124 | def get_stat(self, obj): 125 | return json.loads(obj.summary)["stat"] 126 | 127 | def get_platform(self, obj): 128 | return json.loads(obj.summary)["platform"] 129 | 130 | def get_success(self, obj): 131 | return json.loads(obj.summary)["success"] 132 | 133 | 134 | class VariablesSerializer(serializers.ModelSerializer): 135 | """ 136 | 变量信息序列化 137 | """ 138 | 139 | class Meta: 140 | model = models.Variables 141 | fields = '__all__' 142 | 143 | 144 | class HostIPSerializer(serializers.ModelSerializer): 145 | """ 146 | 变量信息序列化 147 | """ 148 | 149 | class Meta: 150 | model = models.HostIP 151 | fields = '__all__' 152 | 153 | 154 | class PeriodicTaskSerializer(serializers.ModelSerializer): 155 | """ 156 | 定时任务信列表序列化 157 | """ 158 | kwargs = serializers.SerializerMethodField() 159 | args = serializers.SerializerMethodField() 160 | 161 | class Meta: 162 | model = celery_models.PeriodicTask 163 | fields = ['id', 'name', 'args', 'kwargs', 'enabled', 'date_changed', 'enabled', 'description'] 164 | 165 | def get_kwargs(self, obj): 166 | return json.loads(obj.kwargs) 167 | 168 | def get_args(self, obj): 169 | return json.loads(obj.args) 170 | -------------------------------------------------------------------------------- /fastrunner/tasks.py: -------------------------------------------------------------------------------- 1 | 2 | from celery import shared_task 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from fastrunner import models 5 | from fastrunner.utils.loader import save_summary, debug_suite, debug_api 6 | 7 | 8 | 9 | @shared_task 10 | def async_debug_api(api, project, name, config=None): 11 | """异步执行api 12 | """ 13 | summary = debug_api(api, project, config=config, save=False) 14 | save_summary(name, summary, project) 15 | 16 | 17 | @shared_task 18 | def async_debug_suite(suite, project, obj, report, config): 19 | """异步执行suite 20 | """ 21 | summary = debug_suite(suite, project, obj, config=config, save=False) 22 | save_summary(report, summary, project) 23 | 24 | 25 | 26 | @shared_task 27 | def schedule_debug_suite(*args, **kwargs): 28 | """定时任务 29 | """ 30 | 31 | project = kwargs["project"] 32 | suite = [] 33 | test_sets = [] 34 | config_list = [] 35 | for pk in args: 36 | try: 37 | name = models.Case.objects.get(id=pk).name 38 | suite.append({ 39 | "name": name, 40 | "id": pk 41 | }) 42 | except ObjectDoesNotExist: 43 | pass 44 | 45 | for content in suite: 46 | test_list = models.CaseStep.objects. \ 47 | filter(case__id=content["id"]).order_by("step").values("body") 48 | 49 | testcase_list = [] 50 | config = None 51 | for content in test_list: 52 | body = eval(content["body"]) 53 | if "base_url" in body["request"].keys(): 54 | config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) 55 | continue 56 | testcase_list.append(body) 57 | config_list.append(config) 58 | test_sets.append(testcase_list) 59 | 60 | summary = debug_suite(test_sets, project, suite, config_list, save=False) 61 | save_summary("", summary, project, type=3) 62 | -------------------------------------------------------------------------------- /fastrunner/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastrunner/templatetags/__init__.py -------------------------------------------------------------------------------- /fastrunner/templatetags/custom_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from django import template 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.filter(name='json_dumps') 10 | def json_dumps(value): 11 | try: 12 | return json.dumps(json.loads(value), indent=4, separators=(',', ': '), ensure_ascii=False) 13 | except Exception: 14 | return value 15 | 16 | 17 | @register.filter(name='convert_timestamp') 18 | def convert_timestamp(value): 19 | try: 20 | return time.strftime("%Y--%m--%d %H:%M:%S", time.localtime(int(float(value)))) 21 | except: 22 | return value -------------------------------------------------------------------------------- /fastrunner/urls.py: -------------------------------------------------------------------------------- 1 | """FasterRunner URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.urls import path 18 | from fastrunner.views import project, api, config, schedule, run, suite, report 19 | 20 | urlpatterns = [ 21 | # 项目相关接口地址 22 | path('project/', project.ProjectView.as_view({ 23 | "get": "list", 24 | "post": "add", 25 | "patch": "update", 26 | "delete": "delete" 27 | })), 28 | path('project//', project.ProjectView.as_view({"get": "single"})), 29 | 30 | # 定时任务相关接口 31 | path('schedule/', schedule.ScheduleView.as_view({ 32 | "get": "list", 33 | "post": "add", 34 | })), 35 | 36 | path('schedule//', schedule.ScheduleView.as_view({ 37 | "delete": "delete" 38 | })), 39 | 40 | 41 | 42 | # debugtalk.py相关接口地址 43 | path('debugtalk//', project.DebugTalkView.as_view({"get": "debugtalk"})), 44 | path('debugtalk/', project.DebugTalkView.as_view({ 45 | "patch": "update", 46 | "post": "run" 47 | })), 48 | 49 | # 二叉树接口地址 50 | path('tree//', project.TreeView.as_view()), 51 | 52 | # 文件上传 修改 删除接口地址 53 | # path('file/', project.FileView.as_view()), 54 | 55 | # api接口模板地址 56 | path('api/', api.APITemplateView.as_view({ 57 | "post": "add", 58 | "get": "list" 59 | })), 60 | 61 | path('api//', api.APITemplateView.as_view({ 62 | "delete": "delete", 63 | "get": "single", 64 | "patch": "update", 65 | "post": "copy" 66 | })), 67 | 68 | # test接口地址 69 | path('test/', suite.TestCaseView.as_view({ 70 | "get": "get", 71 | "post": "post", 72 | "delete": "delete" 73 | })), 74 | 75 | path('test//', suite.TestCaseView.as_view({ 76 | "delete": "delete", 77 | "post": "copy" 78 | })), 79 | 80 | path('teststep//', suite.CaseStepView.as_view()), 81 | 82 | # config接口地址 83 | path('config/', config.ConfigView.as_view({ 84 | "post": "add", 85 | "get": "list", 86 | "delete": "delete" 87 | })), 88 | 89 | path('config//', config.ConfigView.as_view({ 90 | "post": "copy", 91 | "delete": "delete", 92 | "patch": "update", 93 | "get": "all" 94 | })), 95 | 96 | path('variables/', config.VariablesView.as_view({ 97 | "post": "add", 98 | "get": "list", 99 | "delete": "delete" 100 | })), 101 | 102 | path('variables//', config.VariablesView.as_view({ 103 | "delete": "delete", 104 | "patch": "update" 105 | })), 106 | 107 | # run api 108 | path('run_api_pk//', run.run_api_pk), 109 | path('run_api_tree/', run.run_api_tree), 110 | path('run_api/', run.run_api), 111 | 112 | # run testsuite 113 | path('run_testsuite/', run.run_testsuite), 114 | path('run_test/', run.run_test), 115 | path('run_testsuite_pk//', run.run_testsuite_pk), 116 | path('run_suite_tree/', run.run_suite_tree), 117 | path('automation_test/', run.automation_test), 118 | 119 | # 报告地址 120 | path('reports/', report.ReportView.as_view({ 121 | "get": "list" 122 | })), 123 | 124 | path('reports//', report.ReportView.as_view({ 125 | "delete": "delete", 126 | "get": "look" 127 | })), 128 | 129 | path('host_ip/', config.HostIPView.as_view({ 130 | "post": "add", 131 | "get": "list" 132 | })), 133 | 134 | path('host_ip//', config.HostIPView.as_view({ 135 | "delete": "delete", 136 | "patch": "update", 137 | "get": "all" 138 | })), 139 | ] 140 | -------------------------------------------------------------------------------- /fastrunner/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastrunner/utils/__init__.py -------------------------------------------------------------------------------- /fastrunner/utils/decorator.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | from fastrunner.utils import parser 5 | 6 | logger = logging.getLogger('FasterRunner') 7 | 8 | 9 | def request_log(level): 10 | def wrapper(func): 11 | @functools.wraps(func) 12 | def inner_wrapper(request, *args, **kwargs): 13 | msg_data = "before process request data:\n{data}".format(data=parser.format_json(request.data)) 14 | msg_params = "before process request params:\n{params}".format( 15 | params=parser.format_json(request.query_params)) 16 | if level is 'INFO': 17 | if request.data: 18 | logger.info(msg_data) 19 | if request.query_params: 20 | logger.info(msg_params) 21 | elif level is 'DEBUG': 22 | if request.data: 23 | logger.debug(msg_data) 24 | if request.query_params: 25 | logger.debug(msg_params) 26 | return func(request, *args, **kwargs) 27 | 28 | return inner_wrapper 29 | 30 | return wrapper 31 | -------------------------------------------------------------------------------- /fastrunner/utils/host.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | import re 3 | 4 | 5 | def parse_host(ip, api): 6 | 7 | if not isinstance(ip, list): 8 | return api 9 | if not api: 10 | return api 11 | try: 12 | parts = urlparse(api["request"]["url"]) 13 | except KeyError: 14 | parts = urlparse(api["request"]["base_url"]) 15 | host = parts.netloc 16 | if host: 17 | for content in ip: 18 | content = content.strip() 19 | if host in content and not content.startswith("#"): 20 | ip = re.findall(r'\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b', content) 21 | if ip: 22 | if "headers" in api["request"].keys(): 23 | api["request"]["headers"]["Host"] = host 24 | else: 25 | api["request"].setdefault("headers", {"Host": host}) 26 | try: 27 | api["request"]["url"] = api["request"]["url"].replace(host, ip[-1]) 28 | except KeyError: 29 | api["request"]["base_url"] = api["request"]["base_url"].replace(host, ip[-1]) 30 | return api 31 | -------------------------------------------------------------------------------- /fastrunner/utils/loader.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | import importlib 4 | import io 5 | import json 6 | import os 7 | import shutil 8 | import sys 9 | import tempfile 10 | import types 11 | import requests 12 | import yaml 13 | from bs4 import BeautifulSoup 14 | from httprunner import HttpRunner, logger 15 | from requests.cookies import RequestsCookieJar 16 | 17 | from fastrunner import models 18 | from fastrunner.utils.parser import Format 19 | 20 | logger.setup_logger('INFO') 21 | 22 | TEST_NOT_EXISTS = { 23 | "code": "0102", 24 | "status": False, 25 | "msg": "节点下没有接口或者用例集" 26 | } 27 | 28 | 29 | def is_function(tup): 30 | """ Takes (name, object) tuple, returns True if it is a function. 31 | """ 32 | name, item = tup 33 | return isinstance(item, types.FunctionType) 34 | 35 | 36 | def is_variable(tup): 37 | """ Takes (name, object) tuple, returns True if it is a variable. 38 | """ 39 | name, item = tup 40 | if callable(item): 41 | # function or class 42 | return False 43 | 44 | if isinstance(item, types.ModuleType): 45 | # imported module 46 | return False 47 | 48 | if name.startswith("_"): 49 | # private property 50 | return False 51 | 52 | return True 53 | 54 | 55 | class FileLoader(object): 56 | 57 | @staticmethod 58 | def dump_yaml_file(yaml_file, data): 59 | """ dump yaml file 60 | """ 61 | with io.open(yaml_file, 'w', encoding='utf-8') as stream: 62 | yaml.dump(data, stream, indent=4, default_flow_style=False, encoding='utf-8', allow_unicode=True) 63 | 64 | @staticmethod 65 | def dump_json_file(json_file, data): 66 | """ dump json file 67 | """ 68 | with io.open(json_file, 'w', encoding='utf-8') as stream: 69 | json.dump(data, stream, indent=4, separators=(',', ': '), ensure_ascii=False) 70 | 71 | @staticmethod 72 | def dump_python_file(python_file, data): 73 | """dump python file 74 | """ 75 | with io.open(python_file, 'w', encoding='utf-8') as stream: 76 | stream.write(data) 77 | 78 | @staticmethod 79 | def dump_binary_file(binary_file, data): 80 | """dump file 81 | """ 82 | with io.open(binary_file, 'wb') as stream: 83 | stream.write(data) 84 | 85 | @staticmethod 86 | def load_python_module(file_path): 87 | """ load python module. 88 | 89 | Args: 90 | file_path: python path 91 | 92 | Returns: 93 | dict: variables and functions mapping for specified python module 94 | 95 | { 96 | "variables": {}, 97 | "functions": {} 98 | } 99 | 100 | """ 101 | debugtalk_module = { 102 | "variables": {}, 103 | "functions": {} 104 | } 105 | 106 | sys.path.insert(0, file_path) 107 | module = importlib.import_module("debugtalk") 108 | # 修复重载bug 109 | importlib.reload(module) 110 | sys.path.pop(0) 111 | 112 | for name, item in vars(module).items(): 113 | if is_function((name, item)): 114 | debugtalk_module["functions"][name] = item 115 | elif is_variable((name, item)): 116 | if isinstance(item, tuple): 117 | continue 118 | debugtalk_module["variables"][name] = item 119 | else: 120 | pass 121 | 122 | return debugtalk_module 123 | 124 | 125 | def parse_tests(testcases, debugtalk, project, name=None, config=None): 126 | """get test case structure 127 | testcases: list 128 | config: none or dict 129 | debugtalk: dict 130 | """ 131 | refs = { 132 | "env": {}, 133 | "def-api": {}, 134 | "def-testcase": {}, 135 | "debugtalk": debugtalk 136 | } 137 | testset = { 138 | "config": { 139 | "name": testcases[-1]["name"], 140 | "variables": [] 141 | }, 142 | "teststeps": testcases, 143 | } 144 | 145 | if config: 146 | if "parameters" in config.keys(): 147 | for content in config["parameters"]: 148 | for key, value in content.items(): 149 | try: 150 | content[key] = eval(value.replace("\n", "")) 151 | except: 152 | content[key] = value 153 | testset["config"] = config 154 | 155 | if name: 156 | testset["config"]["name"] = name 157 | 158 | global_variables = [] 159 | 160 | for variables in models.Variables.objects.filter(project__id=project).values("key", "value"): 161 | if testset["config"].get("variables"): 162 | for content in testset["config"]["variables"]: 163 | if variables["key"] not in content.keys(): 164 | global_variables.append({variables["key"]: variables["value"]}) 165 | else: 166 | global_variables.append({variables["key"]: variables["value"]}) 167 | 168 | if not testset["config"].get("variables"): 169 | testset["config"]["variables"] = global_variables 170 | else: 171 | testset["config"]["variables"].extend(global_variables) 172 | 173 | testset["config"]["refs"] = refs 174 | 175 | return testset 176 | 177 | 178 | def load_debugtalk(project): 179 | """import debugtalk.py in sys.path and reload 180 | project: int 181 | """ 182 | # debugtalk.py 183 | code = models.Debugtalk.objects.get(project__id=project).code 184 | 185 | file_path = os.path.join(tempfile.mkdtemp(prefix='FasterRunner'), "debugtalk.py") 186 | FileLoader.dump_python_file(file_path, code) 187 | debugtalk = FileLoader.load_python_module(os.path.dirname(file_path)) 188 | 189 | shutil.rmtree(os.path.dirname(file_path)) 190 | return debugtalk 191 | 192 | 193 | def debug_suite(suite, project, obj, config, save=True): 194 | """debug suite 195 | suite :list 196 | pk: int 197 | project: int 198 | """ 199 | if len(suite) == 0: 200 | return TEST_NOT_EXISTS 201 | 202 | test_sets = [] 203 | debugtalk = load_debugtalk(project) 204 | for index in range(len(suite)): 205 | # copy.deepcopy 修复引用bug 206 | testcases = copy.deepcopy( 207 | parse_tests(suite[index], debugtalk, project, name=obj[index]['name'], config=config[index])) 208 | test_sets.append(testcases) 209 | 210 | kwargs = { 211 | "failfast": False 212 | } 213 | runner = HttpRunner(**kwargs) 214 | runner.run(test_sets) 215 | summary = parse_summary(runner.summary) 216 | if save: 217 | save_summary("", summary, project, type=1) 218 | 219 | return summary 220 | 221 | 222 | def debug_api(api, project, name=None, config=None, save=True): 223 | """debug api 224 | api :dict or list 225 | project: int 226 | """ 227 | if len(api) == 0: 228 | return TEST_NOT_EXISTS 229 | 230 | # testcases 231 | if isinstance(api, dict): 232 | """ 233 | httprunner scripts or teststeps 234 | """ 235 | api = [api] 236 | 237 | testcase_list = [parse_tests(api, load_debugtalk(project), project, name=name, config=config)] 238 | 239 | kwargs = { 240 | "failfast": False 241 | } 242 | 243 | runner = HttpRunner(**kwargs) 244 | runner.run(testcase_list) 245 | 246 | summary = parse_summary(runner.summary) 247 | if save: 248 | save_summary("", summary, project, type=1) 249 | 250 | return summary 251 | 252 | 253 | def load_test(test, project=None): 254 | """ 255 | format testcase 256 | """ 257 | 258 | try: 259 | format_http = Format(test['newBody']) 260 | format_http.parse() 261 | testcase = format_http.testcase 262 | 263 | except KeyError: 264 | if 'case' in test.keys(): 265 | if test["body"]["method"] == "config": 266 | case_step = models.Config.objects.get(name=test["body"]["name"], project=project) 267 | else: 268 | case_step = models.CaseStep.objects.get(id=test['id']) 269 | else: 270 | if test["body"]["method"] == "config": 271 | case_step = models.Config.objects.get(name=test["body"]["name"], project=project) 272 | else: 273 | case_step = models.API.objects.get(id=test['id']) 274 | 275 | testcase = eval(case_step.body) 276 | name = test['body']['name'] 277 | 278 | if case_step.name != name: 279 | testcase['name'] = name 280 | 281 | return testcase 282 | 283 | 284 | def parse_summary(summary): 285 | """序列化summary 286 | """ 287 | for detail in summary["details"]: 288 | 289 | for record in detail["records"]: 290 | 291 | for key, value in record["meta_data"]["request"].items(): 292 | if isinstance(value, bytes): 293 | record["meta_data"]["request"][key] = value.decode("utf-8") 294 | if isinstance(value, RequestsCookieJar): 295 | record["meta_data"]["request"][key] = requests.utils.dict_from_cookiejar(value) 296 | 297 | for key, value in record["meta_data"]["response"].items(): 298 | if isinstance(value, bytes): 299 | record["meta_data"]["response"][key] = value.decode("utf-8") 300 | if isinstance(value, RequestsCookieJar): 301 | record["meta_data"]["response"][key] = requests.utils.dict_from_cookiejar(value) 302 | 303 | if "text/html" in record["meta_data"]["response"]["content_type"]: 304 | record["meta_data"]["response"]["content"] = \ 305 | BeautifulSoup(record["meta_data"]["response"]["content"], features="html.parser").prettify() 306 | 307 | return summary 308 | 309 | 310 | def save_summary(name, summary, project, type=2): 311 | """保存报告信息 312 | """ 313 | if "status" in summary.keys(): 314 | return 315 | if name is "": 316 | name = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 317 | 318 | models.Report.objects.create(**{ 319 | "project": models.Project.objects.get(id=project), 320 | "name": name, 321 | "type": type, 322 | "summary": json.dumps(summary, ensure_ascii=False), 323 | }) 324 | -------------------------------------------------------------------------------- /fastrunner/utils/parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from enum import Enum 4 | 5 | logger = logging.getLogger('FasterRunner') 6 | 7 | 8 | 9 | class FileType(Enum): 10 | """ 11 | 文件类型枚举 12 | """ 13 | string = 1 14 | int = 2 15 | float = 3 16 | bool = 4 17 | list = 5 18 | dict = 6 19 | file = 7 20 | 21 | 22 | class Format(object): 23 | """ 24 | 解析标准HttpRunner脚本 前端->后端 25 | """ 26 | 27 | def __init__(self, body, level='test'): 28 | """ 29 | body => { 30 | header: header -> [{key:'', value:'', desc:''},], 31 | request: request -> { 32 | form: formData - > [{key: '', value: '', type: 1, desc: ''},], 33 | json: jsonData -> {},- 34 | params: paramsData -> [{key: '', value: '', type: 1, desc: ''},] 35 | files: files -> {"fields","binary"} 36 | }, 37 | extract: extract -> [{key:'', value:'', desc:''}], 38 | validate: validate -> [{expect: '', actual: '', comparator: 'equals', type: 1},], 39 | variables: variables -> [{key: '', value: '', type: 1, desc: ''},], 40 | hooks: hooks -> [{setup: '', teardown: ''},], 41 | url: url -> string 42 | method: method -> string 43 | name: name -> string 44 | } 45 | """ 46 | try: 47 | self.name = body.pop('name') 48 | 49 | self.__headers = body['header'].pop('header') 50 | self.__params = body['request']['params'].pop('params') 51 | self.__data = body['request']['form'].pop('data') 52 | self.__json = body['request'].pop('json') 53 | self.__files = body['request']['files'].pop('files') 54 | self.__variables = body['variables'].pop('variables') 55 | self.__setup_hooks = body['hooks'].pop('setup_hooks') 56 | self.__teardown_hooks = body['hooks'].pop('teardown_hooks') 57 | 58 | self.__desc = { 59 | "header": body['header'].pop('desc'), 60 | "data": body['request']['form'].pop('desc'), 61 | "files": body['request']['files'].pop('desc'), 62 | "params": body['request']['params'].pop('desc'), 63 | "variables": body['variables'].pop('desc'), 64 | } 65 | 66 | if level is 'test': 67 | self.url = body.pop('url') 68 | self.method = body.pop('method') 69 | 70 | self.__times = body.pop('times') 71 | self.__extract = body['extract'].pop('extract') 72 | self.__validate = body.pop('validate').pop('validate') 73 | self.__desc['extract'] = body['extract'].pop('desc') 74 | 75 | elif level is 'config': 76 | self.base_url = body.pop('base_url') 77 | self.__parameters = body['parameters'].pop('parameters') 78 | self.__desc["parameters"] = body['parameters'].pop('desc') 79 | 80 | self.__level = level 81 | self.testcase = None 82 | 83 | self.project = body.pop('project') 84 | self.relation = body.pop('nodeId') 85 | 86 | except KeyError: 87 | # project or relation 88 | pass 89 | 90 | def parse(self): 91 | """ 92 | 返回标准化HttpRunner "desc" 字段运行需去除 93 | """ 94 | 95 | if self.__level is 'test': 96 | test = { 97 | "name": self.name, 98 | "times": self.__times, 99 | "request": { 100 | "url": self.url, 101 | "method": self.method, 102 | "verify": False 103 | }, 104 | "desc": self.__desc 105 | } 106 | 107 | if self.__extract: 108 | test["extract"] = self.__extract 109 | if self.__validate: 110 | test['validate'] = self.__validate 111 | 112 | elif self.__level is 'config': 113 | test = { 114 | "name": self.name, 115 | "request": { 116 | "base_url": self.base_url, 117 | }, 118 | "desc": self.__desc 119 | } 120 | 121 | if self.__parameters: 122 | test['parameters'] = self.__parameters 123 | 124 | if self.__headers: 125 | test["request"]["headers"] = self.__headers 126 | if self.__params: 127 | test["request"]["params"] = self.__params 128 | if self.__data: 129 | test["request"]["data"] = self.__data 130 | if self.__json: 131 | test["request"]["json"] = self.__json 132 | if self.__files: 133 | test["request"]["files"] = self.__files 134 | if self.__variables: 135 | test["variables"] = self.__variables 136 | if self.__setup_hooks: 137 | test['setup_hooks'] = self.__setup_hooks 138 | if self.__teardown_hooks: 139 | test['teardown_hooks'] = self.__teardown_hooks 140 | 141 | self.testcase = test 142 | 143 | 144 | class Parse(object): 145 | """ 146 | 标准HttpRunner脚本解析至前端 后端->前端 147 | """ 148 | 149 | def __init__(self, body, level='test'): 150 | """ 151 | body: => { 152 | "name": "get token with $user_agent, $os_platform, $app_version", 153 | "request": { 154 | "url": "/api/get-token", 155 | "method": "POST", 156 | "headers": { 157 | "app_version": "$app_version", 158 | "os_platform": "$os_platform", 159 | "user_agent": "$user_agent" 160 | }, 161 | "json": { 162 | "sign": "${get_sign($user_agent, $device_sn, $os_platform, $app_version)}" 163 | }, 164 | "extract": [ 165 | {"token": "content.token"} 166 | ], 167 | "validate": [ 168 | {"eq": ["status_code", 200]}, 169 | {"eq": ["headers.Content-Type", "application/json"]}, 170 | {"eq": ["content.success", true]} 171 | ], 172 | "setup_hooks": [], 173 | "teardown_hooks": [] 174 | } 175 | """ 176 | self.name = body.get('name') 177 | self.__request = body.get('request') # header files params json data 178 | self.__variables = body.get('variables') 179 | self.__setup_hooks = body.get('setup_hooks', []) 180 | self.__teardown_hooks = body.get('teardown_hooks', []) 181 | self.__desc = body.get('desc') 182 | 183 | if level is 'test': 184 | self.__times = body.get('times', 1) # 如果导入没有times 默认为1 185 | self.__extract = body.get('extract') 186 | self.__validate = body.get('validate') 187 | 188 | elif level is "config": 189 | self.__parameters = body.get("parameters") 190 | 191 | self.__level = level 192 | self.testcase = None 193 | 194 | @staticmethod 195 | def __get_type(content): 196 | """ 197 | 返回data_type 默认string 198 | """ 199 | var_type = { 200 | "str": 1, 201 | "int": 2, 202 | "float": 3, 203 | "bool": 4, 204 | "list": 5, 205 | "dict": 6, 206 | } 207 | 208 | key = str(type(content).__name__) 209 | 210 | if key in ["list", "dict"]: 211 | content = json.dumps(content, ensure_ascii=False) 212 | else: 213 | content = str(content) 214 | return var_type[key], content 215 | 216 | def parse_http(self): 217 | """ 218 | 标准前端脚本格式 219 | """ 220 | init = [{ 221 | "key": "", 222 | "value": "", 223 | "desc": "" 224 | }] 225 | 226 | init_p = [{ 227 | "key": "", 228 | "value": "", 229 | "desc": "", 230 | "type": 1 231 | }] 232 | 233 | # 初始化test结构 234 | test = { 235 | "name": self.name, 236 | "header": init, 237 | "request": { 238 | "data": init_p, 239 | "params": init_p, 240 | "json_data": '' 241 | }, 242 | "variables": init_p, 243 | "hooks": [{ 244 | "setup": "", 245 | "teardown": "" 246 | }] 247 | } 248 | 249 | if self.__level is 'test': 250 | test["times"] = self.__times 251 | test["method"] = self.__request['method'] 252 | test["url"] = self.__request['url'] 253 | test["validate"] = [{ 254 | "expect": "", 255 | "actual": "", 256 | "comparator": "equals", 257 | "type": 1 258 | }] 259 | test["extract"] = init 260 | 261 | if self.__extract: 262 | test["extract"] = [] 263 | for content in self.__extract: 264 | for key, value in content.items(): 265 | test['extract'].append({ 266 | "key": key, 267 | "value": value, 268 | "desc": self.__desc["extract"][key] 269 | }) 270 | 271 | if self.__validate: 272 | test["validate"] = [] 273 | for content in self.__validate: 274 | for key, value in content.items(): 275 | obj = Parse.__get_type(value[1]) 276 | test["validate"].append({ 277 | "expect": obj[1], 278 | "actual": value[0], 279 | "comparator": key, 280 | "type": obj[0] 281 | }) 282 | 283 | elif self.__level is "config": 284 | test["base_url"] = self.__request["base_url"] 285 | test["parameters"] = init 286 | 287 | if self.__parameters: 288 | test["parameters"] = [] 289 | for content in self.__parameters: 290 | for key, value in content.items(): 291 | test["parameters"].append({ 292 | "key": key, 293 | "value": Parse.__get_type(value)[1], 294 | "desc": self.__desc["parameters"][key] 295 | }) 296 | 297 | if self.__request.get('headers'): 298 | test["header"] = [] 299 | for key, value in self.__request.pop('headers').items(): 300 | test['header'].append({ 301 | "key": key, 302 | "value": value, 303 | "desc": self.__desc["header"][key] 304 | }) 305 | 306 | if self.__request.get('data'): 307 | test["request"]["data"] = [] 308 | for key, value in self.__request.pop('data').items(): 309 | obj = Parse.__get_type(value) 310 | 311 | test['request']['data'].append({ 312 | "key": key, 313 | "value": obj[1], 314 | "type": obj[0], 315 | "desc": self.__desc["data"][key] 316 | }) 317 | 318 | # if self.__request.get('files'): 319 | # for key, value in self.__request.pop("files").items(): 320 | # size = FileBinary.objects.get(name=value).size 321 | # test['request']['data'].append({ 322 | # "key": key, 323 | # "value": value, 324 | # "size": size, 325 | # "type": 5, 326 | # "desc": self.__desc["files"][key] 327 | # }) 328 | 329 | if self.__request.get('params'): 330 | test["request"]["params"] = [] 331 | for key, value in self.__request.pop('params').items(): 332 | test['request']['params'].append({ 333 | "key": key, 334 | "value": value, 335 | "type": 1, 336 | "desc": self.__desc["params"][key] 337 | }) 338 | 339 | if self.__request.get('json'): 340 | test["request"]["json_data"] = \ 341 | json.dumps(self.__request.pop("json"), indent=4, 342 | separators=(',', ': '), ensure_ascii=False) 343 | if self.__variables: 344 | test["variables"] = [] 345 | for content in self.__variables: 346 | for key, value in content.items(): 347 | obj = Parse.__get_type(value) 348 | test["variables"].append({ 349 | "key": key, 350 | "value": obj[1], 351 | "desc": self.__desc["variables"][key], 352 | "type": obj[0] 353 | }) 354 | 355 | if self.__setup_hooks or self.__teardown_hooks: 356 | test["hooks"] = [] 357 | if len(self.__setup_hooks) > len(self.__teardown_hooks): 358 | for index in range(0, len(self.__setup_hooks)): 359 | teardown = "" 360 | if index < len(self.__teardown_hooks): 361 | teardown = self.__teardown_hooks[index] 362 | test["hooks"].append({ 363 | "setup": self.__setup_hooks[index], 364 | "teardown": teardown 365 | }) 366 | else: 367 | for index in range(0, len(self.__teardown_hooks)): 368 | setup = "" 369 | if index < len(self.__setup_hooks): 370 | setup = self.__setup_hooks[index] 371 | test["hooks"].append({ 372 | "setup": setup, 373 | "teardown": self.__teardown_hooks[index] 374 | }) 375 | self.testcase = test 376 | 377 | 378 | def format_json(value): 379 | try: 380 | return json.dumps(value, indent=4, separators=(',', ': '), ensure_ascii=False) 381 | except: 382 | return value 383 | -------------------------------------------------------------------------------- /fastrunner/utils/prepare.py: -------------------------------------------------------------------------------- 1 | from fastrunner import models 2 | from fastrunner.utils.parser import Format 3 | from djcelery import models as celery_models 4 | 5 | 6 | def get_counter(model, pk=None): 7 | """ 8 | 统计相关表长度 9 | """ 10 | if pk: 11 | return model.objects.filter(project__id=pk).count() 12 | else: 13 | return model.objects.count() 14 | 15 | 16 | def get_project_detail(pk): 17 | """ 18 | 项目详细统计信息 19 | """ 20 | api_count = get_counter(models.API, pk=pk) 21 | case_count = get_counter(models.Case, pk=pk) 22 | config_count = get_counter(models.Config, pk=pk) 23 | variables_count = get_counter(models.Variables, pk=pk) 24 | report_count = get_counter(models.Report, pk=pk) 25 | host_count = get_counter(models.HostIP, pk=pk) 26 | task_count = celery_models.PeriodicTask.objects.filter(description=pk).count() 27 | 28 | return { 29 | "api_count": api_count, 30 | "case_count": case_count, 31 | "task_count": task_count, 32 | "config_count": config_count, 33 | "variables_count": variables_count, 34 | "report_count": report_count, 35 | "host_count":host_count 36 | } 37 | 38 | 39 | def project_init(project): 40 | """新建项目初始化 41 | """ 42 | 43 | # 自动生成默认debugtalk.py 44 | models.Debugtalk.objects.create(project=project) 45 | # 自动生成API tree 46 | models.Relation.objects.create(project=project) 47 | # 自动生成Test Tree 48 | models.Relation.objects.create(project=project, type=2) 49 | 50 | 51 | def project_end(project): 52 | """删除项目相关表 filter不会报异常 最好不用get 53 | """ 54 | models.Debugtalk.objects.filter(project=project).delete() 55 | models.Config.objects.filter(project=project).delete() 56 | models.API.objects.filter(project=project).delete() 57 | models.Relation.objects.filter(project=project).delete() 58 | models.Report.objects.filter(project=project).delete() 59 | models.Variables.objects.filter(project=project).delete() 60 | celery_models.PeriodicTask.objects.filter(description=project).delete() 61 | 62 | case = models.Case.objects.filter(project=project).values_list('id') 63 | 64 | for case_id in case: 65 | models.CaseStep.objects.filter(case__id=case_id).delete() 66 | 67 | 68 | def tree_end(params, project): 69 | """ 70 | project: Project Model 71 | params: { 72 | node: int, 73 | type: int 74 | } 75 | """ 76 | type = params['type'] 77 | node = params['node'] 78 | 79 | if type == 1: 80 | models.API.objects. \ 81 | filter(relation=node, project=project).delete() 82 | 83 | # remove node testcase 84 | elif type == 2: 85 | case = models.Case.objects. \ 86 | filter(relation=node, project=project).values('id') 87 | 88 | for case_id in case: 89 | models.CaseStep.objects.filter(case__id=case_id['id']).delete() 90 | models.Case.objects.filter(id=case_id['id']).delete() 91 | 92 | 93 | def update_casestep(body, case): 94 | step_list = list(models.CaseStep.objects.filter(case=case).values('id')) 95 | 96 | for index in range(len(body)): 97 | 98 | test = body[index] 99 | try: 100 | format_http = Format(test['newBody']) 101 | format_http.parse() 102 | name = format_http.name 103 | new_body = format_http.testcase 104 | url = format_http.url 105 | method = format_http.method 106 | 107 | except KeyError: 108 | if 'case' in test.keys(): 109 | case_step = models.CaseStep.objects.get(id=test['id']) 110 | elif test["body"]["method"] == "config": 111 | case_step = models.Config.objects.get(name=test['body']['name']) 112 | else: 113 | case_step = models.API.objects.get(id=test['id']) 114 | 115 | new_body = eval(case_step.body) 116 | name = test['body']['name'] 117 | 118 | if case_step.name != name: 119 | new_body['name'] = name 120 | 121 | if test["body"]["method"] == "config": 122 | url = "" 123 | method = "config" 124 | else: 125 | url = test['body']['url'] 126 | method = test['body']['method'] 127 | 128 | kwargs = { 129 | "name": name, 130 | "body": new_body, 131 | "url": url, 132 | "method": method, 133 | "step": index, 134 | } 135 | if 'case' in test.keys(): 136 | models.CaseStep.objects.filter(id=test['id']).update(**kwargs) 137 | step_list.remove({"id": test['id']}) 138 | else: 139 | kwargs['case'] = case 140 | models.CaseStep.objects.create(**kwargs) 141 | 142 | # 去掉多余的step 143 | for content in step_list: 144 | models.CaseStep.objects.filter(id=content['id']).delete() 145 | 146 | 147 | def generate_casestep(body, case): 148 | """ 149 | 生成用例集步骤 150 | [{ 151 | id: int, 152 | project: int, 153 | name: str, 154 | method: str, 155 | url: str 156 | }] 157 | 158 | """ 159 | # index也是case step的执行顺序 160 | 161 | for index in range(len(body)): 162 | 163 | test = body[index] 164 | try: 165 | format_http = Format(test['newBody']) 166 | format_http.parse() 167 | name = format_http.name 168 | new_body = format_http.testcase 169 | url = format_http.url 170 | method = format_http.method 171 | 172 | except KeyError: 173 | if test["body"]["method"] == "config": 174 | name = test["body"]["name"] 175 | method = test["body"]["method"] 176 | config = models.Config.objects.get(name=name) 177 | url = config.base_url 178 | new_body = eval(config.body) 179 | else: 180 | api = models.API.objects.get(id=test['id']) 181 | new_body = eval(api.body) 182 | name = test['body']['name'] 183 | 184 | if api.name != name: 185 | new_body['name'] = name 186 | 187 | url = test['body']['url'] 188 | method = test['body']['method'] 189 | 190 | kwargs = { 191 | "name": name, 192 | "body": new_body, 193 | "url": url, 194 | "method": method, 195 | "step": index, 196 | "case": case 197 | } 198 | 199 | models.CaseStep.objects.create(**kwargs) 200 | 201 | 202 | def case_end(pk): 203 | """ 204 | pk: int case id 205 | """ 206 | models.CaseStep.objects.filter(case__id=pk).delete() 207 | models.Case.objects.filter(id=pk).delete() 208 | -------------------------------------------------------------------------------- /fastrunner/utils/response.py: -------------------------------------------------------------------------------- 1 | PROJECT_ADD_SUCCESS = { 2 | "code": "0001", 3 | "success": True, 4 | "msg": "项目添加成功" 5 | } 6 | 7 | PROJECT_EXISTS = { 8 | "code": "0101", 9 | "success": False, 10 | "msg": "项目已存在" 11 | } 12 | 13 | PROJECT_NOT_EXISTS = { 14 | "code": "0102", 15 | "success": False, 16 | "msg": "项目不存在" 17 | } 18 | 19 | DEBUGTALK_NOT_EXISTS = { 20 | "code": "0102", 21 | "success": False, 22 | "msg": "miss debugtalk" 23 | } 24 | 25 | DEBUGTALK_UPDATE_SUCCESS = { 26 | "code": "0002", 27 | "success": True, 28 | "msg": "debugtalk更新成功" 29 | } 30 | 31 | PROJECT_UPDATE_SUCCESS = { 32 | "code": "0002", 33 | "success": True, 34 | "msg": "项目更新成功" 35 | } 36 | 37 | PROJECT_DELETE_SUCCESS = { 38 | "code": "0003", 39 | "success": True, 40 | "msg": "项目删除成功" 41 | } 42 | 43 | SYSTEM_ERROR = { 44 | "code": "9999", 45 | "success": False, 46 | "msg": "System Error" 47 | } 48 | 49 | TREE_ADD_SUCCESS = { 50 | "code": "0001", 51 | "success": True, 52 | "msg": "树形结构添加成功" 53 | } 54 | 55 | TREE_UPDATE_SUCCESS = { 56 | "code": "0002", 57 | "success": True, 58 | "msg": "树形结构更新成功" 59 | } 60 | 61 | KEY_MISS = { 62 | "code": "0100", 63 | "success": False, 64 | "msg": "请求数据非法" 65 | } 66 | 67 | FILE_UPLOAD_SUCCESS = { 68 | 'code': '0001', 69 | 'success': True, 70 | 'msg': '文件上传成功' 71 | } 72 | 73 | FILE_EXISTS = { 74 | 'code': '0101', 75 | 'success': False, 76 | 'msg': '文件已存在,默认使用已有文件' 77 | } 78 | 79 | API_ADD_SUCCESS = { 80 | 'code': '0001', 81 | 'success': True, 82 | 'msg': '接口添加成功' 83 | } 84 | 85 | DATA_TO_LONG = { 86 | 'code': '0100', 87 | 'success': False, 88 | 'msg': '数据信息过长!' 89 | } 90 | 91 | API_NOT_FOUND = { 92 | 'code': '0102', 93 | 'success': False, 94 | 'msg': '未查询到该API' 95 | } 96 | 97 | API_DEL_SUCCESS = { 98 | 'code': '0003', 99 | 'success': True, 100 | 'msg': 'API删除成功' 101 | } 102 | 103 | REPORT_DEL_SUCCESS = { 104 | 'code': '0003', 105 | 'success': True, 106 | 'msg': '报告删除成功' 107 | } 108 | 109 | API_UPDATE_SUCCESS = { 110 | 'code': '0002', 111 | 'success': True, 112 | 'msg': 'API更新成功' 113 | } 114 | 115 | SUITE_ADD_SUCCESS = { 116 | 'code': '0001', 117 | 'success': True, 118 | 'msg': 'Suite添加成功' 119 | } 120 | 121 | SUITE_DEL_SUCCESS = { 122 | 'code': '0003', 123 | 'success': True, 124 | 'msg': 'Suite删除成功' 125 | } 126 | 127 | CASE_ADD_SUCCESS = { 128 | 'code': '0001', 129 | 'success': True, 130 | 'msg': '用例添加成功' 131 | } 132 | 133 | CASE_EXISTS = { 134 | "code": "0101", 135 | "success": False, 136 | "msg": "此节点下已存在该用例集,请重新命名" 137 | } 138 | 139 | CASE_NOT_EXISTS = { 140 | "code": "0102", 141 | "success": False, 142 | "msg": "此用例集不存在" 143 | } 144 | 145 | CASE_DELETE_SUCCESS = { 146 | "code": "0003", 147 | "success": True, 148 | "msg": "用例集删除成功" 149 | } 150 | 151 | CASE_UPDATE_SUCCESS = { 152 | 'code': '0002', 153 | 'success': True, 154 | 'msg': '用例集更新成功' 155 | } 156 | 157 | CONFIG_EXISTS = { 158 | "code": "0101", 159 | "success": False, 160 | "msg": "此配置已存在,请重新命名" 161 | } 162 | 163 | VARIABLES_EXISTS = { 164 | "code": "0101", 165 | "success": False, 166 | "msg": "此变量已存在,请重新命名" 167 | } 168 | 169 | CONFIG_ADD_SUCCESS = { 170 | 'code': '0001', 171 | 'success': True, 172 | 'msg': '环境添加成功' 173 | } 174 | 175 | VARIABLES_ADD_SUCCESS = { 176 | 'code': '0001', 177 | 'success': True, 178 | 'msg': '变量添加成功' 179 | } 180 | 181 | CONFIG_NOT_EXISTS = { 182 | "code": "0102", 183 | "success": False, 184 | "msg": "指定的环境不存在" 185 | } 186 | 187 | REPORT_NOT_EXISTS = { 188 | "code": "0102", 189 | "success": False, 190 | "msg": "指定的报告不存在" 191 | } 192 | 193 | VARIABLES_NOT_EXISTS = { 194 | "code": "0102", 195 | "success": False, 196 | "msg": "指定的全局变量不存在" 197 | } 198 | 199 | CONFIG_UPDATE_SUCCESS = { 200 | "code": "0002", 201 | "success": True, 202 | "msg": "环境更新成功" 203 | } 204 | 205 | VARIABLES_UPDATE_SUCCESS = { 206 | "code": "0002", 207 | "success": True, 208 | "msg": "全局变量更新成功" 209 | } 210 | 211 | TASK_ADD_SUCCESS = { 212 | "code": "0001", 213 | "success": True, 214 | "msg": "定时任务新增成功" 215 | } 216 | 217 | TASK_TIME_ILLEGAL = { 218 | "code": "0101", 219 | "success": False, 220 | "msg": "时间表达式非法" 221 | } 222 | 223 | TASK_HAS_EXISTS = { 224 | "code": "0102", 225 | "success": False, 226 | "msg": "定时任务已存在" 227 | } 228 | 229 | TASK_EMAIL_ILLEGAL = { 230 | "code": "0102", 231 | "success": False, 232 | "msg": "请指定邮件接收人列表" 233 | } 234 | 235 | TASK_DEL_SUCCESS = { 236 | "code": "0003", 237 | "success": True, 238 | "msg": "任务删除成功" 239 | } 240 | 241 | PLAN_DEL_SUCCESS = { 242 | "code": "0003", 243 | "success": True, 244 | "msg": "集成计划删除成功" 245 | } 246 | 247 | PLAN_ADD_SUCCESS = { 248 | "code": "0001", 249 | "success": True, 250 | "msg": "计划添加成功" 251 | } 252 | 253 | PLAN_KEY_EXIST = { 254 | "code": "0101", 255 | "success": False, 256 | "msg": "该KEY值已存在,请修改KEY值" 257 | } 258 | 259 | PLAN_ILLEGAL = { 260 | "code": "0101", 261 | "success": False, 262 | "msg": "提取字段格式错误,请检查" 263 | } 264 | 265 | PLAN_UPDATE_SUCCESS = { 266 | "code": "0002", 267 | "success": True, 268 | "msg": "计划更新成功" 269 | } 270 | 271 | HOSTIP_EXISTS = { 272 | "code": "0101", 273 | "success": False, 274 | "msg": "此域名已存在,请重新命名" 275 | } 276 | 277 | HOSTIP_ADD_SUCCESS = { 278 | 'code': '0001', 279 | 'success': True, 280 | 'msg': '域名添加成功' 281 | } 282 | 283 | HOSTIP_NOT_EXISTS = { 284 | "code": "0102", 285 | "success": False, 286 | "msg": "指定的域名不存在" 287 | } 288 | 289 | HOSTIP_EXISTS = { 290 | "code": "0101", 291 | "success": False, 292 | "msg": "此域名已存在,请重新命名" 293 | } 294 | 295 | HOSTIP_UPDATE_SUCCESS = { 296 | "code": "0002", 297 | "success": True, 298 | "msg": "域名更新成功" 299 | } 300 | HOST_DEL_SUCCESS = { 301 | 'code': '0003', 302 | 'success': True, 303 | 'msg': '域名删除成功' 304 | } 305 | 306 | 307 | -------------------------------------------------------------------------------- /fastrunner/utils/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import shutil 5 | import sys 6 | import os 7 | import subprocess 8 | import tempfile 9 | from fastrunner.utils import loader 10 | 11 | EXEC = sys.executable 12 | 13 | if 'uwsgi' in EXEC: 14 | EXEC = "/usr/bin/python3" 15 | 16 | 17 | class DebugCode(object): 18 | 19 | def __init__(self, code): 20 | self.__code = code 21 | self.resp = None 22 | self.temp = tempfile.mkdtemp(prefix='FasterRunner') 23 | 24 | def run(self): 25 | """ dumps debugtalk.py and run 26 | """ 27 | try: 28 | file_path = os.path.join(self.temp, "debugtalk.py") 29 | loader.FileLoader.dump_python_file(file_path, self.__code) 30 | self.resp = decode(subprocess.check_output([EXEC, file_path], stderr=subprocess.STDOUT, timeout=60)) 31 | 32 | except subprocess.CalledProcessError as e: 33 | self.resp = decode(e.output) 34 | 35 | except subprocess.TimeoutExpired: 36 | self.resp = 'RunnerTimeOut' 37 | 38 | shutil.rmtree(self.temp) 39 | 40 | 41 | def decode(s): 42 | try: 43 | return s.decode('utf-8') 44 | 45 | except UnicodeDecodeError: 46 | return s.decode('gbk') 47 | -------------------------------------------------------------------------------- /fastrunner/utils/task.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from djcelery import models as celery_models 4 | 5 | from fastrunner.utils import response 6 | from fastrunner.utils.parser import format_json 7 | 8 | logger = logging.getLogger('FasterRunner') 9 | 10 | 11 | class Task(object): 12 | """ 13 | 定时任务操作 14 | """ 15 | 16 | def __init__(self, **kwargs): 17 | logger.info("before process task data:\n {kwargs}".format(kwargs=format_json(kwargs))) 18 | self.__name = kwargs["name"] 19 | self.__data = kwargs["data"] 20 | self.__corntab = kwargs["corntab"] 21 | self.__switch = kwargs["switch"] 22 | self.__task = "fastrunner.tasks.schedule_debug_suite" 23 | self.__project = kwargs["project"] 24 | self.__email = { 25 | "strategy": kwargs["strategy"], 26 | "copy": kwargs["copy"], 27 | "receiver": kwargs["receiver"], 28 | "corntab": self.__corntab, 29 | "project": self.__project 30 | } 31 | self.__corntab_time = None 32 | 33 | def format_corntab(self): 34 | """ 35 | 格式化时间 36 | """ 37 | corntab = self.__corntab.split(' ') 38 | if len(corntab) > 5: 39 | return response.TASK_TIME_ILLEGAL 40 | try: 41 | self.__corntab_time = { 42 | 'day_of_week': corntab[4], 43 | 'month_of_year': corntab[3], 44 | 'day_of_month': corntab[2], 45 | 'hour': corntab[1], 46 | 'minute': corntab[0] 47 | } 48 | except Exception: 49 | return response.TASK_TIME_ILLEGAL 50 | 51 | return response.TASK_ADD_SUCCESS 52 | 53 | def add_task(self): 54 | """ 55 | add tasks 56 | """ 57 | if celery_models.PeriodicTask.objects.filter(name__exact=self.__name).count() > 0: 58 | logger.info("{name} tasks exist".format(name=self.__name)) 59 | return response.TASK_HAS_EXISTS 60 | 61 | if self.__email["strategy"] == '始终发送' or self.__email["strategy"] == '仅失败发送': 62 | if self.__email["receiver"] == '': 63 | return response.TASK_EMAIL_ILLEGAL 64 | 65 | resp = self.format_corntab() 66 | if resp["success"]: 67 | task, created = celery_models.PeriodicTask.objects.get_or_create(name=self.__name, task=self.__task) 68 | crontab = celery_models.CrontabSchedule.objects.filter(**self.__corntab_time).first() 69 | if crontab is None: 70 | crontab = celery_models.CrontabSchedule.objects.create(**self.__corntab_time) 71 | task.crontab = crontab 72 | task.enabled = self.__switch 73 | task.args = json.dumps(self.__data, ensure_ascii=False) 74 | task.kwargs = json.dumps(self.__email, ensure_ascii=False) 75 | task.description = self.__project 76 | task.save() 77 | logger.info("{name} tasks save success".format(name=self.__name)) 78 | return response.TASK_ADD_SUCCESS 79 | else: 80 | return resp 81 | 82 | 83 | -------------------------------------------------------------------------------- /fastrunner/utils/tree.py: -------------------------------------------------------------------------------- 1 | def get_tree_max_id(value, list_id=[]): 2 | """ 3 | 得到最大Tree max id 4 | """ 5 | if not value: 6 | return 0 # the first node id 7 | 8 | if isinstance(value, list): 9 | for content in value: # content -> dict 10 | try: 11 | children = content['children'] 12 | except KeyError: 13 | """ 14 | 待返回错误信息 15 | """ 16 | pass 17 | 18 | if children: 19 | get_tree_max_id(children) 20 | 21 | list_id.append(content['id']) 22 | 23 | return max(list_id) 24 | 25 | 26 | def get_file_size(size): 27 | """计算大小 28 | """ 29 | 30 | if size >= 1048576: 31 | size = str(round(size / 1048576, 2)) + 'MB' 32 | elif size >= 1024: 33 | size = str(round(size / 1024, 2)) + 'KB' 34 | else: 35 | size = str(size) + 'Byte' 36 | 37 | return size 38 | -------------------------------------------------------------------------------- /fastrunner/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastrunner/views/__init__.py -------------------------------------------------------------------------------- /fastrunner/views/api.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.utils.decorators import method_decorator 3 | from rest_framework.viewsets import GenericViewSet 4 | from fastrunner import models, serializers 5 | from rest_framework.response import Response 6 | from fastrunner.utils import response 7 | from fastrunner.utils.decorator import request_log 8 | from fastrunner.utils.parser import Format, Parse 9 | from django.db import DataError 10 | 11 | 12 | class APITemplateView(GenericViewSet): 13 | """ 14 | API操作视图 15 | """ 16 | serializer_class = serializers.APISerializer 17 | queryset = models.API.objects 18 | 19 | @method_decorator(request_log(level='DEBUG')) 20 | def list(self, request): 21 | """ 22 | 接口列表 { 23 | project: int, 24 | node: int 25 | } 26 | """ 27 | 28 | node = request.query_params["node"] 29 | project = request.query_params["project"] 30 | search = request.query_params["search"] 31 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 32 | 33 | if search != '': 34 | queryset = queryset.filter(name__contains=search) 35 | 36 | if node != '': 37 | queryset = queryset.filter(relation=node) 38 | 39 | pagination_queryset = self.paginate_queryset(queryset) 40 | serializer = self.get_serializer(pagination_queryset, many=True) 41 | 42 | return self.get_paginated_response(serializer.data) 43 | 44 | @method_decorator(request_log(level='INFO')) 45 | def add(self, request): 46 | """ 47 | 新增一个接口 48 | """ 49 | 50 | api = Format(request.data) 51 | api.parse() 52 | 53 | api_body = { 54 | 'name': api.name, 55 | 'body': api.testcase, 56 | 'url': api.url, 57 | 'method': api.method, 58 | 'project': models.Project.objects.get(id=api.project), 59 | 'relation': api.relation 60 | } 61 | 62 | try: 63 | models.API.objects.create(**api_body) 64 | except DataError: 65 | return Response(response.DATA_TO_LONG) 66 | 67 | return Response(response.API_ADD_SUCCESS) 68 | 69 | @method_decorator(request_log(level='INFO')) 70 | def update(self, request, **kwargs): 71 | """ 72 | 更新接口 73 | """ 74 | pk = kwargs['pk'] 75 | api = Format(request.data) 76 | api.parse() 77 | 78 | api_body = { 79 | 'name': api.name, 80 | 'body': api.testcase, 81 | 'url': api.url, 82 | 'method': api.method, 83 | } 84 | 85 | try: 86 | models.API.objects.filter(id=pk).update(**api_body) 87 | except ObjectDoesNotExist: 88 | return Response(response.API_NOT_FOUND) 89 | 90 | return Response(response.API_UPDATE_SUCCESS) 91 | 92 | @method_decorator(request_log(level='INFO')) 93 | def copy(self, request, **kwargs): 94 | """ 95 | pk int: test id 96 | { 97 | name: api name 98 | } 99 | """ 100 | pk = kwargs['pk'] 101 | name = request.data['name'] 102 | api = models.API.objects.get(id=pk) 103 | body = eval(api.body) 104 | body["name"] = name 105 | api.body = body 106 | api.id = None 107 | api.name = name 108 | api.save() 109 | return Response(response.API_ADD_SUCCESS) 110 | 111 | @method_decorator(request_log(level='INFO')) 112 | def delete(self, request, **kwargs): 113 | """ 114 | 删除一个接口 pk 115 | 删除多个 116 | [{ 117 | id:int 118 | }] 119 | """ 120 | 121 | try: 122 | if kwargs.get('pk'): # 单个删除 123 | models.API.objects.get(id=kwargs['pk']).delete() 124 | else: 125 | for content in request.data: 126 | models.API.objects.get(id=content['id']).delete() 127 | 128 | except ObjectDoesNotExist: 129 | return Response(response.API_NOT_FOUND) 130 | 131 | return Response(response.API_DEL_SUCCESS) 132 | 133 | @method_decorator(request_log(level='INFO')) 134 | def single(self, request, **kwargs): 135 | """ 136 | 查询单个api,返回body信息 137 | """ 138 | try: 139 | api = models.API.objects.get(id=kwargs['pk']) 140 | except ObjectDoesNotExist: 141 | return Response(response.API_NOT_FOUND) 142 | 143 | parse = Parse(eval(api.body)) 144 | parse.parse_http() 145 | 146 | resp = { 147 | 'id': api.id, 148 | 'body': parse.testcase, 149 | 'success': True, 150 | } 151 | 152 | return Response(resp) 153 | -------------------------------------------------------------------------------- /fastrunner/views/config.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.utils.decorators import method_decorator 3 | from rest_framework.viewsets import GenericViewSet 4 | from fastrunner import models, serializers 5 | from rest_framework.response import Response 6 | from fastrunner.utils import response 7 | from fastrunner.utils.decorator import request_log 8 | from fastrunner.utils.parser import Format 9 | 10 | 11 | class ConfigView(GenericViewSet): 12 | serializer_class = serializers.ConfigSerializer 13 | queryset = models.Config.objects 14 | 15 | @method_decorator(request_log(level='DEBUG')) 16 | def list(self, request): 17 | project = request.query_params['project'] 18 | search = request.query_params["search"] 19 | 20 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 21 | 22 | if search != '': 23 | queryset = queryset.filter(name__contains=search) 24 | 25 | pagination_queryset = self.paginate_queryset(queryset) 26 | serializer = self.get_serializer(pagination_queryset, many=True) 27 | 28 | return self.get_paginated_response(serializer.data) 29 | 30 | @method_decorator(request_log(level='DEBUG')) 31 | def all(self, request, **kwargs): 32 | """ 33 | get all config 34 | """ 35 | pk = kwargs["pk"] 36 | 37 | queryset = self.get_queryset().filter(project__id=pk). \ 38 | order_by('-update_time').values("id", "name") 39 | 40 | return Response(queryset) 41 | 42 | @method_decorator(request_log(level='INFO')) 43 | def add(self, request): 44 | """ 45 | add new config 46 | { 47 | name: str 48 | project: int 49 | body: dict 50 | } 51 | """ 52 | 53 | config = Format(request.data, level='config') 54 | config.parse() 55 | 56 | try: 57 | config.project = models.Project.objects.get(id=config.project) 58 | except ObjectDoesNotExist: 59 | return Response(response.PROJECT_NOT_EXISTS) 60 | 61 | if models.Config.objects.filter(name=config.name, project=config.project).first(): 62 | return Response(response.CONFIG_EXISTS) 63 | 64 | config_body = { 65 | "name": config.name, 66 | "base_url": config.base_url, 67 | "body": config.testcase, 68 | "project": config.project 69 | } 70 | 71 | models.Config.objects.create(**config_body) 72 | return Response(response.CONFIG_ADD_SUCCESS) 73 | 74 | @method_decorator(request_log(level='INFO')) 75 | def update(self, request, **kwargs): 76 | """ 77 | pk: int 78 | { 79 | name: str, 80 | base_url: str, 81 | variables: [] 82 | parameters: [] 83 | request: [] 84 | } 85 | } 86 | """ 87 | pk = kwargs['pk'] 88 | 89 | try: 90 | config = models.Config.objects.get(id=pk) 91 | 92 | except ObjectDoesNotExist: 93 | return Response(response.CONFIG_NOT_EXISTS) 94 | 95 | format = Format(request.data, level="config") 96 | format.parse() 97 | 98 | if models.Config.objects.exclude(id=pk).filter(name=format.name).first(): 99 | return Response(response.CONFIG_EXISTS) 100 | 101 | case_step = models.CaseStep.objects.filter(method="config", name=config.name) 102 | 103 | for case in case_step: 104 | case.name = format.name 105 | case.body = format.testcase 106 | case.save() 107 | 108 | config.name = format.name 109 | config.body = format.testcase 110 | config.base_url = format.base_url 111 | config.save() 112 | 113 | return Response(response.CONFIG_UPDATE_SUCCESS) 114 | 115 | @method_decorator(request_log(level='INFO')) 116 | def copy(self, request, **kwargs): 117 | """ 118 | pk: int 119 | { 120 | name: str 121 | } 122 | """ 123 | pk = kwargs['pk'] 124 | try: 125 | config = models.Config.objects.get(id=pk) 126 | except ObjectDoesNotExist: 127 | return Response(response.CONFIG_NOT_EXISTS) 128 | 129 | if models.Config.objects.filter(**request.data).first(): 130 | return Response(response.CONFIG_EXISTS) 131 | 132 | config.id = None 133 | 134 | body = eval(config.body) 135 | name = request.data['name'] 136 | 137 | body['name'] = name 138 | config.name = name 139 | config.body = body 140 | config.save() 141 | 142 | return Response(response.CONFIG_ADD_SUCCESS) 143 | 144 | @method_decorator(request_log(level='INFO')) 145 | def delete(self, request, **kwargs): 146 | """ 147 | 删除一个配置 pk 148 | 删除多个 149 | [{ 150 | id:int 151 | }] 152 | """ 153 | 154 | try: 155 | if kwargs.get('pk'): # 单个删除 156 | models.Config.objects.get(id=kwargs['pk']).delete() 157 | else: 158 | for content in request.data: 159 | models.Config.objects.get(id=content['id']).delete() 160 | 161 | except ObjectDoesNotExist: 162 | return Response(response.CONFIG_NOT_EXISTS) 163 | 164 | return Response(response.API_DEL_SUCCESS) 165 | 166 | 167 | class VariablesView(GenericViewSet): 168 | serializer_class = serializers.VariablesSerializer 169 | queryset = models.Variables.objects 170 | 171 | @method_decorator(request_log(level='DEBUG')) 172 | def list(self, request): 173 | project = request.query_params['project'] 174 | search = request.query_params["search"] 175 | 176 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 177 | 178 | if search != '': 179 | queryset = queryset.filter(key__contains=search) 180 | 181 | pagination_queryset = self.paginate_queryset(queryset) 182 | serializer = self.get_serializer(pagination_queryset, many=True) 183 | 184 | return self.get_paginated_response(serializer.data) 185 | 186 | @method_decorator(request_log(level='INFO')) 187 | def add(self, request): 188 | """ 189 | add new variables 190 | { 191 | key: str 192 | value: str 193 | project: int 194 | } 195 | """ 196 | 197 | try: 198 | project = models.Project.objects.get(id=request.data["project"]) 199 | except ObjectDoesNotExist: 200 | return Response(response.PROJECT_NOT_EXISTS) 201 | 202 | if models.Variables.objects.filter(key=request.data["key"], project=project).first(): 203 | return Response(response.VARIABLES_EXISTS) 204 | 205 | request.data["project"] = project 206 | 207 | models.Variables.objects.create(**request.data) 208 | return Response(response.CONFIG_ADD_SUCCESS) 209 | 210 | @method_decorator(request_log(level='INFO')) 211 | def update(self, request, **kwargs): 212 | """ 213 | pk: int 214 | { 215 | key: str 216 | value:str 217 | } 218 | """ 219 | pk = kwargs['pk'] 220 | 221 | try: 222 | variables = models.Variables.objects.get(id=pk) 223 | 224 | except ObjectDoesNotExist: 225 | return Response(response.VARIABLES_NOT_EXISTS) 226 | 227 | if models.Variables.objects.exclude(id=pk).filter(key=request.data['key']).first(): 228 | return Response(response.VARIABLES_EXISTS) 229 | 230 | variables.key = request.data["key"] 231 | variables.value = request.data["value"] 232 | variables.save() 233 | 234 | return Response(response.VARIABLES_UPDATE_SUCCESS) 235 | 236 | @method_decorator(request_log(level='INFO')) 237 | def delete(self, request, **kwargs): 238 | """ 239 | 删除一个变量 pk 240 | 删除多个 241 | [{ 242 | id:int 243 | }] 244 | """ 245 | 246 | try: 247 | if kwargs.get('pk'): # 单个删除 248 | models.Variables.objects.get(id=kwargs['pk']).delete() 249 | else: 250 | for content in request.data: 251 | models.Variables.objects.get(id=content['id']).delete() 252 | 253 | except ObjectDoesNotExist: 254 | return Response(response.VARIABLES_NOT_EXISTS) 255 | 256 | return Response(response.API_DEL_SUCCESS) 257 | 258 | 259 | class HostIPView(GenericViewSet): 260 | serializer_class = serializers.HostIPSerializer 261 | queryset = models.HostIP.objects 262 | 263 | @method_decorator(request_log(level='DEBUG')) 264 | def list(self, request): 265 | project = request.query_params['project'] 266 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 267 | pagination_queryset = self.paginate_queryset(queryset) 268 | serializer = self.get_serializer(pagination_queryset, many=True) 269 | 270 | return self.get_paginated_response(serializer.data) 271 | 272 | @method_decorator(request_log(level='INFO')) 273 | def add(self, request): 274 | """ 275 | add new variables 276 | { 277 | name: str 278 | value: str 279 | project: int 280 | } 281 | """ 282 | 283 | try: 284 | project = models.Project.objects.get(id=request.data["project"]) 285 | except ObjectDoesNotExist: 286 | return Response(response.PROJECT_NOT_EXISTS) 287 | 288 | if models.HostIP.objects.filter(name=request.data["name"], project=project).first(): 289 | return Response(response.HOSTIP_EXISTS) 290 | 291 | request.data["project"] = project 292 | 293 | models.HostIP.objects.create(**request.data) 294 | return Response(response.HOSTIP_ADD_SUCCESS) 295 | 296 | @method_decorator(request_log(level='INFO')) 297 | def update(self, request, **kwargs): 298 | """pk: int{ 299 | name: str 300 | value:str 301 | } 302 | """ 303 | pk = kwargs['pk'] 304 | 305 | try: 306 | host = models.HostIP.objects.get(id=pk) 307 | 308 | except ObjectDoesNotExist: 309 | return Response(response.HOSTIP_NOT_EXISTS) 310 | 311 | if models.HostIP.objects.exclude(id=pk).filter(name=request.data['name']).first(): 312 | return Response(response.HOSTIP_EXISTS) 313 | 314 | host.name = request.data["name"] 315 | host.value = request.data["value"] 316 | host.save() 317 | 318 | return Response(response.HOSTIP_UPDATE_SUCCESS) 319 | 320 | @method_decorator(request_log(level='INFO')) 321 | def delete(self, request, **kwargs): 322 | """删除host 323 | """ 324 | try: 325 | models.HostIP.objects.get(id=kwargs['pk']).delete() 326 | except ObjectDoesNotExist: 327 | return Response(response.HOSTIP_NOT_EXISTS) 328 | 329 | return Response(response.HOST_DEL_SUCCESS) 330 | 331 | @method_decorator(request_log(level='DEBUG')) 332 | def all(self, request, **kwargs): 333 | """ 334 | get all config 335 | """ 336 | pk = kwargs["pk"] 337 | 338 | queryset = self.get_queryset().filter(project__id=pk). \ 339 | order_by('-update_time').values("id", "name") 340 | 341 | return Response(queryset) 342 | -------------------------------------------------------------------------------- /fastrunner/views/project.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.utils.decorators import method_decorator 3 | from rest_framework.views import APIView 4 | from rest_framework.viewsets import GenericViewSet 5 | from fastrunner import models, serializers 6 | from FasterRunner import pagination 7 | from rest_framework.response import Response 8 | from fastrunner.utils import response 9 | from fastrunner.utils import prepare 10 | from fastrunner.utils.decorator import request_log 11 | from fastrunner.utils.runner import DebugCode 12 | from fastrunner.utils.tree import get_tree_max_id 13 | 14 | 15 | class ProjectView(GenericViewSet): 16 | """ 17 | 项目增删改查 18 | """ 19 | queryset = models.Project.objects.all().order_by('-update_time') 20 | serializer_class = serializers.ProjectSerializer 21 | pagination_class = pagination.MyCursorPagination 22 | 23 | @method_decorator(request_log(level='DEBUG')) 24 | def list(self, request): 25 | """ 26 | 查询项目信息 27 | """ 28 | projects = self.get_queryset() 29 | page_projects = self.paginate_queryset(projects) 30 | serializer = self.get_serializer(page_projects, many=True) 31 | return self.get_paginated_response(serializer.data) 32 | 33 | @method_decorator(request_log(level='INFO')) 34 | def add(self, request): 35 | """添加项目 { 36 | name: str 37 | } 38 | """ 39 | 40 | name = request.data["name"] 41 | 42 | if models.Project.objects.filter(name=name).first(): 43 | response.PROJECT_EXISTS["name"] = name 44 | return Response(response.PROJECT_EXISTS) 45 | # 反序列化 46 | serializer = serializers.ProjectSerializer(data=request.data) 47 | 48 | if serializer.is_valid(): 49 | serializer.save() 50 | project = models.Project.objects.get(name=name) 51 | prepare.project_init(project) 52 | return Response(response.PROJECT_ADD_SUCCESS) 53 | 54 | return Response(response.SYSTEM_ERROR) 55 | 56 | @method_decorator(request_log(level='INFO')) 57 | def update(self, request): 58 | """ 59 | 编辑项目 60 | """ 61 | 62 | try: 63 | project = models.Project.objects.get(id=request.data['id']) 64 | except (KeyError, ObjectDoesNotExist): 65 | return Response(response.SYSTEM_ERROR) 66 | 67 | if request.data['name'] != project.name: 68 | if models.Project.objects.filter(name=request.data['name']).first(): 69 | return Response(response.PROJECT_EXISTS) 70 | 71 | # 调用save方法update_time字段才会自动更新 72 | project.name = request.data['name'] 73 | project.desc = request.data['desc'] 74 | project.save() 75 | 76 | return Response(response.PROJECT_UPDATE_SUCCESS) 77 | 78 | @method_decorator(request_log(level='INFO')) 79 | def delete(self, request): 80 | """ 81 | 删除项目 82 | """ 83 | try: 84 | project = models.Project.objects.get(id=request.data['id']) 85 | 86 | project.delete() 87 | prepare.project_end(project) 88 | 89 | return Response(response.PROJECT_DELETE_SUCCESS) 90 | except ObjectDoesNotExist: 91 | return Response(response.SYSTEM_ERROR) 92 | 93 | @method_decorator(request_log(level='INFO')) 94 | def single(self, request, **kwargs): 95 | """ 96 | 得到单个项目相关统计信息 97 | """ 98 | pk = kwargs.pop('pk') 99 | 100 | try: 101 | queryset = models.Project.objects.get(id=pk) 102 | except ObjectDoesNotExist: 103 | return Response(response.PROJECT_NOT_EXISTS) 104 | 105 | serializer = self.get_serializer(queryset, many=False) 106 | 107 | project_info = prepare.get_project_detail(pk) 108 | project_info.update(serializer.data) 109 | 110 | return Response(project_info) 111 | 112 | 113 | class DebugTalkView(GenericViewSet): 114 | """ 115 | DebugTalk update 116 | """ 117 | 118 | serializer_class = serializers.DebugTalkSerializer 119 | 120 | @method_decorator(request_log(level='INFO')) 121 | def debugtalk(self, request, **kwargs): 122 | """ 123 | 得到debugtalk code 124 | """ 125 | pk = kwargs.pop('pk') 126 | try: 127 | queryset = models.Debugtalk.objects.get(project__id=pk) 128 | except ObjectDoesNotExist: 129 | return Response(response.DEBUGTALK_NOT_EXISTS) 130 | 131 | serializer = self.get_serializer(queryset, many=False) 132 | 133 | return Response(serializer.data) 134 | 135 | @method_decorator(request_log(level='INFO')) 136 | def update(self, request): 137 | """ 138 | 编辑debugtalk.py 代码并保存 139 | """ 140 | pk = request.data['id'] 141 | try: 142 | models.Debugtalk.objects.filter(id=pk). \ 143 | update(code=request.data['code']) 144 | 145 | except ObjectDoesNotExist: 146 | return Response(response.SYSTEM_ERROR) 147 | 148 | return Response(response.DEBUGTALK_UPDATE_SUCCESS) 149 | 150 | @method_decorator(request_log(level='INFO')) 151 | def run(self, request): 152 | try: 153 | code = request.data["code"] 154 | except KeyError: 155 | return Response(response.KEY_MISS) 156 | debug = DebugCode(code) 157 | debug.run() 158 | resp = { 159 | "msg": debug.resp, 160 | "success": True, 161 | "code": "0001" 162 | } 163 | return Response(resp) 164 | 165 | 166 | class TreeView(APIView): 167 | """ 168 | 树形结构操作 169 | """ 170 | 171 | @method_decorator(request_log(level='INFO')) 172 | def get(self, request, **kwargs): 173 | """ 174 | 返回树形结构 175 | 当前最带节点ID 176 | """ 177 | 178 | try: 179 | tree_type = request.query_params['type'] 180 | tree = models.Relation.objects.get(project__id=kwargs['pk'], type=tree_type) 181 | except KeyError: 182 | return Response(response.KEY_MISS) 183 | 184 | except ObjectDoesNotExist: 185 | return Response(response.SYSTEM_ERROR) 186 | 187 | body = eval(tree.tree) # list 188 | tree = { 189 | "tree": body, 190 | "id": tree.id, 191 | "success": True, 192 | "max": get_tree_max_id(body) 193 | } 194 | return Response(tree) 195 | 196 | @method_decorator(request_log(level='INFO')) 197 | def patch(self, request, **kwargs): 198 | """ 199 | 修改树形结构,ID不能重复 200 | """ 201 | try: 202 | body = request.data['body'] 203 | mode = request.data['mode'] 204 | 205 | relation = models.Relation.objects.get(id=kwargs['pk']) 206 | relation.tree = body 207 | relation.save() 208 | 209 | except KeyError: 210 | return Response(response.KEY_MISS) 211 | 212 | except ObjectDoesNotExist: 213 | return Response(response.SYSTEM_ERROR) 214 | 215 | # mode -> True remove node 216 | if mode: 217 | prepare.tree_end(request.data, relation.project) 218 | 219 | response.TREE_UPDATE_SUCCESS['tree'] = body 220 | response.TREE_UPDATE_SUCCESS['max'] = get_tree_max_id(body) 221 | 222 | return Response(response.TREE_UPDATE_SUCCESS) 223 | 224 | # 225 | # class FileView(APIView): 226 | # 227 | # def post(self, request): 228 | # """ 229 | # 接收文件并保存 230 | # """ 231 | # file = request.FILES['file'] 232 | # 233 | # if models.FileBinary.objects.filter(name=file.name).first(): 234 | # return Response(response.FILE_EXISTS) 235 | # 236 | # body = { 237 | # "name": file.name, 238 | # "body": file.file.read(), 239 | # "size": get_file_size(file.size) 240 | # } 241 | # 242 | # models.FileBinary.objects.create(**body) 243 | # 244 | # return Response(response.FILE_UPLOAD_SUCCESS) 245 | -------------------------------------------------------------------------------- /fastrunner/views/report.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.shortcuts import render_to_response 5 | from django.utils.decorators import method_decorator 6 | from rest_framework.response import Response 7 | from rest_framework.viewsets import GenericViewSet 8 | 9 | from FasterRunner import pagination 10 | from fastrunner import models, serializers 11 | from fastrunner.utils import response 12 | from fastrunner.utils.decorator import request_log 13 | 14 | 15 | class ReportView(GenericViewSet): 16 | """ 17 | 报告视图 18 | """ 19 | authentication_classes = () 20 | queryset = models.Report.objects 21 | serializer_class = serializers.ReportSerializer 22 | pagination_class = pagination.MyPageNumberPagination 23 | 24 | @method_decorator(request_log(level='DEBUG')) 25 | def list(self, request): 26 | """报告列表 27 | """ 28 | 29 | project = request.query_params['project'] 30 | search = request.query_params["search"] 31 | 32 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 33 | 34 | if search != '': 35 | queryset = queryset.filter(name__contains=search) 36 | 37 | page_report = self.paginate_queryset(queryset) 38 | serializer = self.get_serializer(page_report, many=True) 39 | return self.get_paginated_response(serializer.data) 40 | 41 | @method_decorator(request_log(level='INFO')) 42 | def delete(self, request, **kwargs): 43 | """删除报告 44 | """ 45 | """ 46 | 删除一个报告pk 47 | 删除多个 48 | [{ 49 | id:int 50 | }] 51 | """ 52 | try: 53 | if kwargs.get('pk'): # 单个删除 54 | models.Report.objects.get(id=kwargs['pk']).delete() 55 | else: 56 | for content in request.data: 57 | models.Report.objects.get(id=content['id']).delete() 58 | 59 | except ObjectDoesNotExist: 60 | return Response(response.REPORT_NOT_EXISTS) 61 | 62 | return Response(response.REPORT_DEL_SUCCESS) 63 | 64 | @method_decorator(request_log(level='INFO')) 65 | def look(self, request, **kwargs): 66 | """查看报告 67 | """ 68 | pk = kwargs["pk"] 69 | report = models.Report.objects.get(id=pk) 70 | summary = json.loads(report.summary, encoding="utf-8") 71 | summary["html_report_name"] = report.name 72 | return render_to_response('report_template.html', summary) 73 | 74 | def download(self, request, **kwargs): 75 | """下载报告 76 | """ 77 | pass 78 | -------------------------------------------------------------------------------- /fastrunner/views/run.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from django.core.exceptions import ObjectDoesNotExist 5 | from rest_framework.decorators import api_view, authentication_classes 6 | from fastrunner.utils import loader 7 | from fastrunner import tasks 8 | from rest_framework.response import Response 9 | from fastrunner.utils.decorator import request_log 10 | from fastrunner.utils.host import parse_host 11 | from fastrunner.utils.parser import Format 12 | from fastrunner import models 13 | 14 | """运行方式 15 | """ 16 | logger = logging.getLogger('FasterRunner') 17 | 18 | config_err = { 19 | "success": False, 20 | "msg": "指定的配置文件不存在", 21 | "code": "9999" 22 | } 23 | 24 | 25 | @api_view(['POST']) 26 | @request_log(level='INFO') 27 | def run_api(request): 28 | """ run api by body 29 | """ 30 | name = request.data.pop('config') 31 | host = request.data.pop("host") 32 | api = Format(request.data) 33 | api.parse() 34 | 35 | config = None 36 | if name != '请选择': 37 | try: 38 | config = eval(models.Config.objects.get(name=name, project__id=api.project).body) 39 | 40 | except ObjectDoesNotExist: 41 | logger.error("指定配置文件不存在:{name}".format(name=name)) 42 | return Response(config_err) 43 | 44 | if host != "请选择": 45 | host = models.HostIP.objects.get(name=host, project__id=api.project).value.splitlines() 46 | api.testcase = parse_host(host, api.testcase) 47 | 48 | summary = loader.debug_api(api.testcase, api.project, config=parse_host(host, config)) 49 | 50 | return Response(summary) 51 | 52 | 53 | @api_view(['GET']) 54 | @request_log(level='INFO') 55 | def run_api_pk(request, **kwargs): 56 | """run api by pk and config 57 | """ 58 | host = request.query_params["host"] 59 | api = models.API.objects.get(id=kwargs['pk']) 60 | name = request.query_params["config"] 61 | config = None if name == '请选择' else eval(models.Config.objects.get(name=name, project=api.project).body) 62 | 63 | test_case = eval(api.body) 64 | if host != "请选择": 65 | host = models.HostIP.objects.get(name=host, project=api.project).value.splitlines() 66 | test_case = parse_host(host, test_case) 67 | 68 | summary = loader.debug_api(test_case, api.project.id, config=parse_host(host, config)) 69 | 70 | return Response(summary) 71 | 72 | 73 | @api_view(['POST']) 74 | @request_log(level='INFO') 75 | def run_api_tree(request): 76 | """run api by tree 77 | { 78 | project: int 79 | relation: list 80 | name: str 81 | async: bool 82 | host: str 83 | } 84 | """ 85 | # order by id default 86 | host = request.data["host"] 87 | project = request.data['project'] 88 | relation = request.data["relation"] 89 | back_async = request.data["async"] 90 | name = request.data["name"] 91 | config = request.data["config"] 92 | 93 | config = None if config == '请选择' else eval(models.Config.objects.get(name=config, project__id=project).body) 94 | test_case = [] 95 | 96 | if host != "请选择": 97 | host = models.HostIP.objects.get(name=host, project=project).value.splitlines() 98 | 99 | for relation_id in relation: 100 | api = models.API.objects.filter(project__id=project, relation=relation_id).order_by('id').values('body') 101 | for content in api: 102 | api = eval(content['body']) 103 | test_case.append(parse_host(host, api)) 104 | 105 | if back_async: 106 | tasks.async_debug_api.delay(test_case, project, name, config=parse_host(host, config)) 107 | summary = loader.TEST_NOT_EXISTS 108 | summary["msg"] = "接口运行中,请稍后查看报告" 109 | else: 110 | summary = loader.debug_api(test_case, project, config=parse_host(host, config)) 111 | 112 | return Response(summary) 113 | 114 | 115 | @api_view(["POST"]) 116 | @request_log(level='INFO') 117 | def run_testsuite(request): 118 | """debug testsuite 119 | { 120 | name: str, 121 | body: dict 122 | host: str 123 | } 124 | """ 125 | body = request.data["body"] 126 | project = request.data["project"] 127 | name = request.data["name"] 128 | host = request.data["host"] 129 | 130 | test_case = [] 131 | config = None 132 | 133 | if host != "请选择": 134 | host = models.HostIP.objects.get(name=host, project=project).value.splitlines() 135 | 136 | for test in body: 137 | test = loader.load_test(test, project=project) 138 | if "base_url" in test["request"].keys(): 139 | config = test 140 | continue 141 | 142 | test_case.append(parse_host(host, test)) 143 | 144 | summary = loader.debug_api(test_case, project, name=name, config=parse_host(host, config)) 145 | 146 | return Response(summary) 147 | 148 | 149 | @api_view(["GET"]) 150 | @request_log(level='INFO') 151 | def run_testsuite_pk(request, **kwargs): 152 | """run testsuite by pk 153 | { 154 | project: int, 155 | name: str, 156 | host: str 157 | } 158 | """ 159 | pk = kwargs["pk"] 160 | 161 | test_list = models.CaseStep.objects. \ 162 | filter(case__id=pk).order_by("step").values("body") 163 | 164 | project = request.query_params["project"] 165 | name = request.query_params["name"] 166 | host = request.query_params["host"] 167 | 168 | test_case = [] 169 | config = None 170 | 171 | if host != "请选择": 172 | host = models.HostIP.objects.get(name=host, project=project).value.splitlines() 173 | 174 | for content in test_list: 175 | body = eval(content["body"]) 176 | 177 | if "base_url" in body["request"].keys(): 178 | config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) 179 | continue 180 | 181 | test_case.append(parse_host(host, body)) 182 | 183 | summary = loader.debug_api(test_case, project, name=name, config=parse_host(host, config)) 184 | 185 | return Response(summary) 186 | 187 | 188 | @api_view(['POST']) 189 | @request_log(level='INFO') 190 | def run_suite_tree(request): 191 | """run suite by tree 192 | { 193 | project: int 194 | relation: list 195 | name: str 196 | async: bool 197 | host: str 198 | } 199 | """ 200 | # order by id default 201 | project = request.data['project'] 202 | relation = request.data["relation"] 203 | back_async = request.data["async"] 204 | report = request.data["name"] 205 | host = request.data["host"] 206 | 207 | if host != "请选择": 208 | host = models.HostIP.objects.get(name=host, project=project).value.splitlines() 209 | 210 | test_sets = [] 211 | suite_list = [] 212 | config_list = [] 213 | for relation_id in relation: 214 | suite = list(models.Case.objects.filter(project__id=project, 215 | relation=relation_id).order_by('id').values('id', 'name')) 216 | for content in suite: 217 | test_list = models.CaseStep.objects. \ 218 | filter(case__id=content["id"]).order_by("step").values("body") 219 | 220 | testcase_list = [] 221 | config = None 222 | for content in test_list: 223 | body = eval(content["body"]) 224 | if "base_url" in body["request"].keys(): 225 | config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) 226 | continue 227 | testcase_list.append(parse_host(host, body)) 228 | # [[{scripts}, {scripts}], [{scripts}, {scripts}]] 229 | config_list.append(parse_host(host, config)) 230 | test_sets.append(testcase_list) 231 | suite_list = suite_list + suite 232 | 233 | if back_async: 234 | tasks.async_debug_suite.delay(test_sets, project, suite_list, report, config_list) 235 | summary = loader.TEST_NOT_EXISTS 236 | summary["msg"] = "用例运行中,请稍后查看报告" 237 | else: 238 | summary = loader.debug_suite(test_sets, project, suite_list, config_list) 239 | 240 | return Response(summary) 241 | 242 | 243 | @api_view(["POST"]) 244 | @request_log(level='INFO') 245 | def run_test(request): 246 | """debug single test 247 | { 248 | host: str 249 | body: dict 250 | project :int 251 | config: null or dict 252 | } 253 | """ 254 | 255 | body = request.data["body"] 256 | config = request.data.get("config", None) 257 | project = request.data["project"] 258 | host = request.data["host"] 259 | 260 | if host != "请选择": 261 | host = models.HostIP.objects.get(name=host, project=project).value.splitlines() 262 | 263 | if config: 264 | config = eval(models.Config.objects.get(project=project, name=config["name"]).body) 265 | 266 | summary = loader.debug_api(parse_host(host, loader.load_test(body)), project, config=parse_host(host, config)) 267 | 268 | return Response(summary) 269 | 270 | 271 | @api_view(["POST"]) 272 | @request_log(level='INFO') 273 | @authentication_classes([]) 274 | def automation_test(request): 275 | """kafka automation test 276 | { 277 | "key": 业务线key 278 | "task_id":"任务id", 279 | "business_line" : "业务线" 280 | "ip" : "ip地址", 281 | "app_name" : "应用名", 282 | "dev_manager":"开发负责人", #发布的开发姓名 283 | "demand_id":"需求ID", 284 | "version_id":"版本ID", 285 | "env_flag":"环境标识", 286 | } 287 | """ 288 | 289 | plan = models.Plan.objects.all() 290 | 291 | for plan_sub in plan: 292 | if plan_sub.key == request.data["key"] and plan_sub.switch: 293 | host = None 294 | if plan_sub.host != "请选择": 295 | host = models.HostIP.objects.get(name=plan_sub.host, project=plan_sub.project).value.splitlines() 296 | 297 | test_sets = [] 298 | config_list = [] 299 | 300 | suite = [] 301 | 302 | for index in json.loads(plan_sub.case): 303 | try: 304 | case = models.Case.objects.get(id=index) 305 | suite.append({ 306 | "name": case.name, 307 | "id": index 308 | }) 309 | except ObjectDoesNotExist: 310 | pass 311 | 312 | for content in suite: 313 | test_list = models.CaseStep.objects. \ 314 | filter(case__id=content["id"]).order_by("step").values("body") 315 | 316 | testcase_list = [] 317 | config = None 318 | for content in test_list: 319 | body = eval(content["body"]) 320 | if "base_url" in body["request"].keys(): 321 | config = eval(models.Config.objects.get(name=body["name"], project=plan_sub.project).body) 322 | continue 323 | testcase_list.append(parse_host(host, body)) 324 | 325 | config_list.append(parse_host(host, config)) 326 | test_sets.append(testcase_list) 327 | 328 | tags = { 329 | "project": plan_sub.project.id, 330 | "tag": plan_sub.tag 331 | } 332 | tasks.async_automation_suite.delay(test_sets, tags, suite, request.data, config_list) 333 | break 334 | 335 | return Response({ 336 | "success": True, 337 | "msg": "集成自动化用例运行中", 338 | "code": "0001" 339 | }) 340 | -------------------------------------------------------------------------------- /fastrunner/views/schedule.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from rest_framework.viewsets import GenericViewSet 3 | from djcelery import models 4 | from rest_framework.response import Response 5 | from FasterRunner import pagination 6 | from fastrunner import serializers 7 | from fastrunner.utils import response 8 | from fastrunner.utils.decorator import request_log 9 | from fastrunner.utils.task import Task 10 | 11 | 12 | class ScheduleView(GenericViewSet): 13 | """ 14 | 定时任务增删改查 15 | """ 16 | queryset = models.PeriodicTask.objects 17 | serializer_class = serializers.PeriodicTaskSerializer 18 | pagination_class = pagination.MyPageNumberPagination 19 | 20 | @method_decorator(request_log(level='DEBUG')) 21 | def list(self, request): 22 | """ 23 | 查询项目信息 24 | """ 25 | project = request.query_params.get("project") 26 | schedule = self.get_queryset().filter(description=project).order_by('-date_changed') 27 | page_schedule = self.paginate_queryset(schedule) 28 | serializer = self.get_serializer(page_schedule, many=True) 29 | return self.get_paginated_response(serializer.data) 30 | 31 | @method_decorator(request_log(level='INFO')) 32 | def add(self, request): 33 | """新增定时任务{ 34 | name: str 35 | corntab: str 36 | switch: bool 37 | data: [int,int] 38 | strategy: str 39 | receiver: str 40 | copy: str 41 | project: int 42 | } 43 | """ 44 | task = Task(**request.data) 45 | resp = task.add_task() 46 | return Response(resp) 47 | 48 | # 49 | # @method_decorator(request_log(level='INFO')) 50 | # def update(self, request): 51 | # """ 52 | # 编辑项目 53 | # """ 54 | # 55 | # try: 56 | # project = models.Project.objects.get(id=request.data['id']) 57 | # except (KeyError, ObjectDoesNotExist): 58 | # return Response(response.SYSTEM_ERROR) 59 | # 60 | # if request.data['name'] != project.name: 61 | # if models.Project.objects.filter(name=request.data['name']).first(): 62 | # return Response(response.PROJECT_EXISTS) 63 | # 64 | # # 调用save方法update_time字段才会自动更新 65 | # project.name = request.data['name'] 66 | # project.desc = request.data['desc'] 67 | # project.save() 68 | # 69 | # return Response(response.PROJECT_UPDATE_SUCCESS) 70 | # 71 | # @method_decorator(request_log(level='INFO')) 72 | def delete(self, request, **kwargs): 73 | """删除任务 74 | """ 75 | task = models.PeriodicTask.objects.get(id=kwargs["pk"]) 76 | task.enabled = False 77 | task.delete() 78 | return Response(response.TASK_DEL_SUCCESS) -------------------------------------------------------------------------------- /fastrunner/views/suite.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.utils.decorators import method_decorator 3 | from rest_framework.views import APIView 4 | from rest_framework.viewsets import GenericViewSet 5 | from fastrunner import models, serializers 6 | 7 | from rest_framework.response import Response 8 | from fastrunner.utils import response 9 | from fastrunner.utils import prepare 10 | from fastrunner.utils.decorator import request_log 11 | 12 | 13 | class TestCaseView(GenericViewSet): 14 | queryset = models.Case.objects 15 | serializer_class = serializers.CaseSerializer 16 | tag_options = { 17 | "冒烟用例": 1, 18 | "集成用例": 2, 19 | "监控脚本": 3 20 | } 21 | 22 | @method_decorator(request_log(level='INFO')) 23 | def get(self, request): 24 | """ 25 | 查询指定CASE列表,不包含CASE STEP 26 | { 27 | "project": int, 28 | "node": int 29 | } 30 | """ 31 | node = request.query_params["node"] 32 | project = request.query_params["project"] 33 | search = request.query_params["search"] 34 | # update_time 降序排列 35 | queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') 36 | 37 | if search != '': 38 | queryset = queryset.filter(name__contains=search) 39 | 40 | if node != '': 41 | queryset = queryset.filter(relation=node) 42 | 43 | pagination_query = self.paginate_queryset(queryset) 44 | serializer = self.get_serializer(pagination_query, many=True) 45 | 46 | return self.get_paginated_response(serializer.data) 47 | 48 | @method_decorator(request_log(level='INFO')) 49 | def copy(self, request, **kwargs): 50 | """ 51 | pk int: test id 52 | { 53 | name: test name 54 | relation: int 55 | project: int 56 | } 57 | """ 58 | pk = kwargs['pk'] 59 | name = request.data['name'] 60 | case = models.Case.objects.get(id=pk) 61 | case.id = None 62 | case.name = name 63 | case.save() 64 | 65 | case_step = models.CaseStep.objects.filter(case__id=pk) 66 | 67 | for step in case_step: 68 | step.id = None 69 | step.case = case 70 | step.save() 71 | 72 | return Response(response.CASE_ADD_SUCCESS) 73 | 74 | @method_decorator(request_log(level='INFO')) 75 | def patch(self, request, **kwargs): 76 | """ 77 | 更新测试用例集 78 | { 79 | name: str 80 | id: int 81 | body: [] 82 | project: int 83 | } 84 | """ 85 | 86 | pk = kwargs['pk'] 87 | project = request.data.pop("project") 88 | body = request.data.pop('body') 89 | relation = request.data.pop("relation") 90 | 91 | if models.Case.objects.exclude(id=pk). \ 92 | filter(name=request.data['name'], 93 | project__id=project, 94 | relation=relation).first(): 95 | return Response(response.CASE_EXISTS) 96 | 97 | case = models.Case.objects.get(id=pk) 98 | 99 | prepare.update_casestep(body, case) 100 | 101 | request.data['tag'] = self.tag_options[request.data['tag']] 102 | models.Case.objects.filter(id=pk).update(**request.data) 103 | 104 | return Response(response.CASE_UPDATE_SUCCESS) 105 | 106 | @method_decorator(request_log(level='INFO')) 107 | def post(self, request): 108 | """ 109 | 新增测试用例集 110 | { 111 | name: str 112 | project: int, 113 | relation: int, 114 | tag:str 115 | body: [{ 116 | id: int, 117 | project: int, 118 | name: str, 119 | method: str, 120 | url: str 121 | }] 122 | } 123 | """ 124 | 125 | try: 126 | pk = request.data['project'] 127 | request.data['project'] = models.Project.objects.get(id=pk) 128 | 129 | except KeyError: 130 | return Response(response.KEY_MISS) 131 | 132 | except ObjectDoesNotExist: 133 | return Response(response.PROJECT_NOT_EXISTS) 134 | 135 | body = request.data.pop('body') 136 | 137 | request.data['tag'] = self.tag_options[request.data['tag']] 138 | models.Case.objects.create(**request.data) 139 | 140 | case = models.Case.objects.filter(**request.data).first() 141 | 142 | prepare.generate_casestep(body, case) 143 | 144 | return Response(response.CASE_ADD_SUCCESS) 145 | 146 | @method_decorator(request_log(level='INFO')) 147 | def delete(self, request, **kwargs): 148 | """ 149 | pk: test id delete single 150 | [{id:int}] delete batch 151 | """ 152 | pk = kwargs.get('pk') 153 | 154 | try: 155 | if pk: 156 | prepare.case_end(pk) 157 | else: 158 | for content in request.data: 159 | prepare.case_end(content['id']) 160 | 161 | except ObjectDoesNotExist: 162 | return Response(response.SYSTEM_ERROR) 163 | 164 | return Response(response.CASE_DELETE_SUCCESS) 165 | 166 | 167 | class CaseStepView(APIView): 168 | """ 169 | 测试用例step操作视图 170 | """ 171 | 172 | @method_decorator(request_log(level='INFO')) 173 | def get(self, request, **kwargs): 174 | """ 175 | 返回用例集信息 176 | """ 177 | pk = kwargs['pk'] 178 | 179 | queryset = models.CaseStep.objects.filter(case__id=pk).order_by('step') 180 | 181 | serializer = serializers.CaseStepSerializer(instance=queryset, many=True) 182 | 183 | resp = { 184 | "case": serializers.CaseSerializer(instance=models.Case.objects.get(id=pk), many=False).data, 185 | "step": serializer.data 186 | } 187 | return Response(resp) 188 | -------------------------------------------------------------------------------- /fastuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastuser/__init__.py -------------------------------------------------------------------------------- /fastuser/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /fastuser/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsermanagerConfig(AppConfig): 5 | name = 'fastuser' 6 | -------------------------------------------------------------------------------- /fastuser/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastuser/common/__init__.py -------------------------------------------------------------------------------- /fastuser/common/response.py: -------------------------------------------------------------------------------- 1 | KEY_MISS = { 2 | "code": "0100", 3 | "success": False, 4 | "msg": "请求数据非法" 5 | } 6 | 7 | REGISTER_USERNAME_EXIST = { 8 | "code": "0101", 9 | "success": False, 10 | "msg": "用户名已被注册" 11 | } 12 | 13 | REGISTER_EMAIL_EXIST = { 14 | "code": "0101", 15 | "success": False, 16 | "msg": "邮箱已被注册" 17 | } 18 | 19 | SYSTEM_ERROR = { 20 | "code": "9999", 21 | "success": False, 22 | "msg": "System Error" 23 | } 24 | 25 | REGISTER_SUCCESS = { 26 | "code": "0001", 27 | "success": True, 28 | "msg": "register success" 29 | } 30 | 31 | LOGIN_FAILED = { 32 | "code": "0103", 33 | "success": False, 34 | "msg": "用户名或密码错误" 35 | } 36 | 37 | USER_NOT_EXISTS = { 38 | "code": "0104", 39 | "success": False, 40 | "msg": "该用户未注册" 41 | } 42 | 43 | LOGIN_SUCCESS = { 44 | "code": "0001", 45 | "success": True, 46 | "msg": "login success" 47 | } 48 | -------------------------------------------------------------------------------- /fastuser/common/token.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import time 3 | 4 | 5 | def generate_token(username): 6 | """ 7 | 生成token 8 | """ 9 | timestamp = str(time.time()) 10 | 11 | token = hashlib.md5(bytes(username, encoding='utf-8')) 12 | token.update(bytes(timestamp, encoding='utf-8')) 13 | 14 | return token.hexdigest() 15 | 16 | -------------------------------------------------------------------------------- /fastuser/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httprunner/FasterRunner/2c4a2e8b82e78b830475df3823f501d3f0a4371e/fastuser/migrations/__init__.py -------------------------------------------------------------------------------- /fastuser/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Create your models here. 5 | 6 | class BaseTable(models.Model): 7 | """ 8 | 公共字段列 9 | """ 10 | 11 | class Meta: 12 | abstract = True 13 | verbose_name = "公共字段表" 14 | db_table = 'BaseTable' 15 | 16 | create_time = models.DateTimeField('创建时间', auto_now_add=True) 17 | update_time = models.DateTimeField('更新时间', auto_now=True) 18 | 19 | 20 | class UserInfo(BaseTable): 21 | """ 22 | 用户注册信息表 23 | """ 24 | 25 | class Meta: 26 | verbose_name = "用户信息" 27 | db_table = "UserInfo" 28 | 29 | username = models.CharField('用户名', max_length=20, unique=True, null=False) 30 | password = models.CharField('登陆密码', max_length=100, null=False) 31 | email = models.EmailField('用户邮箱', unique=True, null=False) 32 | 33 | 34 | class UserToken(BaseTable): 35 | """ 36 | 用户登陆token 37 | """ 38 | 39 | class Meta: 40 | verbose_name = "用户登陆token" 41 | db_table = "UserToken" 42 | 43 | user = models.OneToOneField(to=UserInfo, on_delete=models.CASCADE) 44 | token = models.CharField('token', max_length=50) 45 | -------------------------------------------------------------------------------- /fastuser/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from fastuser import models 3 | 4 | 5 | class UserInfoSerializer(serializers.Serializer): 6 | """ 7 | 用户信息序列化 8 | 建议实现其他方法,否则会有警告 9 | """ 10 | username = serializers.CharField(required=True, error_messages={ 11 | "code": "2001", 12 | "msg": "用户名校验失败" 13 | }) 14 | 15 | password = serializers.CharField(required=True, error_messages={ 16 | "code": "2001", 17 | "msg": "密码校验失败" 18 | }) 19 | 20 | email = serializers.CharField(required=True, error_messages={ 21 | "code": "2001", 22 | "msg": "邮箱校验失败" 23 | }) 24 | 25 | def create(self, validated_data): 26 | """ 27 | 实现create方法 28 | """ 29 | return models.UserInfo.objects.create(**validated_data) 30 | -------------------------------------------------------------------------------- /fastuser/urls.py: -------------------------------------------------------------------------------- 1 | """FasterRunner URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.urls import path 18 | from fastuser import views 19 | 20 | urlpatterns = [ 21 | path('register/', views.RegisterView.as_view()), 22 | path('login/', views.LoginView.as_view()) 23 | ] 24 | -------------------------------------------------------------------------------- /fastuser/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | from fastuser.common import response 4 | from fastuser import models 5 | from fastuser import serializers 6 | import logging 7 | # Create your views here. 8 | from fastuser.common.token import generate_token 9 | from django.contrib.auth.hashers import make_password, check_password 10 | from django.core.exceptions import ObjectDoesNotExist 11 | 12 | logger = logging.getLogger('FastRunner') 13 | 14 | 15 | class RegisterView(APIView): 16 | 17 | authentication_classes = () 18 | permission_classes = () 19 | 20 | """ 21 | 注册:{ 22 | "user": "demo" 23 | "password": "1321" 24 | "email": "1@1.com" 25 | } 26 | """ 27 | 28 | def post(self, request): 29 | 30 | try: 31 | username = request.data["username"] 32 | password = request.data["password"] 33 | email = request.data["email"] 34 | except KeyError: 35 | return Response(response.KEY_MISS) 36 | 37 | if models.UserInfo.objects.filter(username=username).first(): 38 | return Response(response.REGISTER_USERNAME_EXIST) 39 | 40 | if models.UserInfo.objects.filter(email=email).first(): 41 | return Response(response.REGISTER_EMAIL_EXIST) 42 | 43 | request.data["password"] = make_password(password) 44 | 45 | serializer = serializers.UserInfoSerializer(data=request.data) 46 | 47 | if serializer.is_valid(): 48 | serializer.save() 49 | return Response(response.REGISTER_SUCCESS) 50 | else: 51 | return Response(response.SYSTEM_ERROR) 52 | 53 | 54 | class LoginView(APIView): 55 | """ 56 | 登陆视图,用户名与密码匹配返回token 57 | """ 58 | authentication_classes = () 59 | permission_classes = () 60 | 61 | def post(self, request): 62 | """ 63 | 用户名密码一致返回token 64 | { 65 | username: str 66 | password: str 67 | } 68 | """ 69 | try: 70 | username = request.data["username"] 71 | password = request.data["password"] 72 | except KeyError: 73 | return Response(response.KEY_MISS) 74 | 75 | user = models.UserInfo.objects.filter(username=username).first() 76 | 77 | if not user: 78 | return Response(response.USER_NOT_EXISTS) 79 | 80 | if not check_password(password, user.password): 81 | return Response(response.LOGIN_FAILED) 82 | 83 | token = generate_token(username) 84 | 85 | try: 86 | models.UserToken.objects.update_or_create(user=user, defaults={"token": token}) 87 | except ObjectDoesNotExist: 88 | return Response(response.SYSTEM_ERROR) 89 | else: 90 | response.LOGIN_SUCCESS["token"] = token 91 | response.LOGIN_SUCCESS["user"] = username 92 | return Response(response.LOGIN_SUCCESS) 93 | 94 | 95 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 5000; 3 | server_name 127.0.0.1; 4 | charset utf-8; 5 | 6 | client_max_body_size 75M; # adjust to taste 7 | location /media { 8 | alias /opt/workspace/FasterRunner/templates; # your Django project's media files - amend as required 9 | } 10 | 11 | location /static { 12 | alias /opt/workspace/FasterRunner/static; # your Django project's static files - amend as required 13 | } 14 | 15 | location / { 16 | include uwsgi_params; 17 | uwsgi_pass unix:/opt/workspace/FasterRunner/FasterRunner.sock; 18 | } 19 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | uwsgi == 2.0.18 2 | Django == 2.1.3 3 | django-cors-headers == 2.4.0 4 | djangorestframework == 3.8.2 5 | HttpRunner == 1.5.15 6 | mysqlclient == 1.3.13 7 | beautifulsoup4 == 4.6.3 8 | tornado == 5.1.1 9 | django-celery == 3.2.2 -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # start nginx service 3 | service nginx start 4 | # start celery worker 5 | celery multi start w1 -A FasterRunner -l info --logfile=./logs/worker.log 6 | # start celery beat 7 | nohup python3 manage.py celery beat -l info > ./logs/beat.log 2>&1 & 8 | # start fastrunner 9 | uwsgi --ini ./uwsgi.ini -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | # myweb_uwsgi.ini file 2 | [uwsgi] 3 | 4 | # Django-related settings 5 | project = FasterRunner 6 | base = /opt/workspace 7 | 8 | 9 | chdir = %(base)/%(project) 10 | module = %(project).wsgi:application 11 | 12 | 13 | master = true 14 | processes = 4 15 | 16 | 17 | socket = %(base)/%(project)/%(project).sock 18 | chmod-socket = 666 19 | vacuum = true 20 | --------------------------------------------------------------------------------