├── .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 |
4 |
5 |
12 |
13 |
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 | [](https://github.com/HttpRunner/FasterRunner/blob/master/LICENSE) [](https://travis-ci.org/HttpRunner/FasterRunner) 
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 |
--------------------------------------------------------------------------------