init
|
@ -0,0 +1,5 @@
|
|||
FLASK_APP=webserver.py
|
||||
FLASK_ENV=production
|
||||
FLASK_DEBUG=0
|
||||
#FLASK_ENV=development
|
||||
#FLASK_DEBUG=1
|
|
@ -0,0 +1,175 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Flask
|
||||
import logging
|
||||
import os
|
||||
from concurrent_log_handler import ConcurrentRotatingFileHandler
|
||||
from queue import Queue
|
||||
from logging.handlers import QueueHandler, QueueListener
|
||||
|
||||
from app.config import Config, proj_dir
|
||||
from app.extensions import (db, migrate, login_manager, mail, csrf, bootstrap, http_cookie_manager, session_id_manager,
|
||||
socketio, dispatcher_scheduler)
|
||||
from app import models
|
||||
from app.template_global import (render_to_json, sort_by_order_in_module, sort_by_order_in_logic_controller,
|
||||
can_show, is_forbidden, get_latest_reports, get_case_from_id, get_tool_from_id)
|
||||
from app.cores.ws import register_all_user_socket
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(import_name=__name__)
|
||||
app.config.from_object(obj=config_class)
|
||||
|
||||
register_logging(app)
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_shell_context(app)
|
||||
register_template_global(app)
|
||||
register_before_first_request_funcs(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_blueprints(app: Flask):
|
||||
from app.routes.auth import bp as auth_bp
|
||||
app.register_blueprint(blueprint=auth_bp, url_prefix='/auth')
|
||||
from app.routes.main import bp as main_bp
|
||||
app.register_blueprint(blueprint=main_bp)
|
||||
from app.routes.case import bp as case_bp
|
||||
app.register_blueprint(blueprint=case_bp, url_prefix='/case')
|
||||
from app.routes.project import bp as project_bp
|
||||
app.register_blueprint(blueprint=project_bp, url_prefix='/project')
|
||||
from app.routes.module import bp as module_bp
|
||||
app.register_blueprint(blueprint=module_bp, url_prefix='/module')
|
||||
from app.routes.logic_controller import bp as logic_controller_bp
|
||||
app.register_blueprint(blueprint=logic_controller_bp, url_prefix='/logic_controller')
|
||||
from app.routes.scene import bp as scene_bp
|
||||
app.register_blueprint(blueprint=scene_bp, url_prefix='/scene')
|
||||
from app.routes.report import bp as report_bp
|
||||
app.register_blueprint(blueprint=report_bp, url_prefix='/report')
|
||||
from app.routes.setting import bp as settings_bp
|
||||
app.register_blueprint(blueprint=settings_bp)
|
||||
from app.routes.error import bp as errors_bp
|
||||
app.register_blueprint(blueprint=errors_bp, url_prefix='/error')
|
||||
from app.routes.ajax import bp as ajax_bp
|
||||
app.register_blueprint(blueprint=ajax_bp, url_prefix='/ajax')
|
||||
from app.utils import bp as util_bp
|
||||
app.register_blueprint(blueprint=util_bp, url_prefix='/util')
|
||||
|
||||
|
||||
def register_extensions(app: Flask):
|
||||
db.init_app(app=app)
|
||||
migrate.init_app(app=app, db=db)
|
||||
login_manager.init_app(app=app)
|
||||
mail.init_app(app=app)
|
||||
csrf.init_app(app=app)
|
||||
bootstrap.init_app(app=app)
|
||||
socketio.init_app(app=app, async_mode='threading')
|
||||
http_cookie_manager.init_app(app=app)
|
||||
session_id_manager.init_app(app=app)
|
||||
dispatcher_scheduler.init_app(app=app)
|
||||
|
||||
|
||||
def register_shell_context(app: Flask):
|
||||
from app.models import (db, User, EmailSetting, Case, HTTPCase, HTTPCaseParameter, HTTPCaseExpectation,
|
||||
HTTPCaseFileUpload, Project, Module, Scene, Dispatcher, DispatcherDetail, Report,
|
||||
ReportCaseData, ReportCaseExpectationData, SubElementInLogicController,
|
||||
ReportToolData, ReportScriptToolData, ReportTimerToolData, ReportVariableDefinitionToolData,
|
||||
ReportVariableDefinitionToolListData)
|
||||
from app.email import send_email
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {
|
||||
'db': db,
|
||||
'User': User,
|
||||
'send_email': send_email,
|
||||
'EmailSetting': EmailSetting,
|
||||
'Case': Case,
|
||||
'HTTPCase': HTTPCase,
|
||||
'HTTPCaseParameter': HTTPCaseParameter,
|
||||
'HTTPCaseExpectation': HTTPCaseExpectation,
|
||||
'HTTPCaseFileUpload': HTTPCaseFileUpload,
|
||||
'Project': Project,
|
||||
'Module': Module,
|
||||
'Scene': Scene,
|
||||
'Dispatcher': Dispatcher,
|
||||
'DispatcherDetail': DispatcherDetail,
|
||||
'SubElementInLogicController': SubElementInLogicController,
|
||||
'ReportToolData': ReportToolData,
|
||||
'ReportScriptToolData': ReportScriptToolData,
|
||||
'ReportTimerToolData': ReportTimerToolData,
|
||||
'ReportVariableDefinitionToolData': ReportVariableDefinitionToolData,
|
||||
'ReportVariableDefinitionToolListData': ReportVariableDefinitionToolListData,
|
||||
}
|
||||
|
||||
|
||||
def register_logging(app: Flask):
|
||||
# 日志记录
|
||||
if not os.path.exists(os.path.join(proj_dir, 'logs')):
|
||||
os.mkdir(os.path.join(proj_dir, 'logs'))
|
||||
# 改用ConcurrentRotatingFileHandler避免出现多进程时出现文件争用情况
|
||||
file_handler = ConcurrentRotatingFileHandler(filename=os.path.join(proj_dir, 'logs', 'webserver.log'),
|
||||
maxBytes=1024 * 1024 * 10,
|
||||
backupCount=10)
|
||||
file_sys_handler = ConcurrentRotatingFileHandler(filename=os.path.join(proj_dir, 'logs', 'webserver_sys.log'),
|
||||
maxBytes=1024 * 1024 * 10,
|
||||
backupCount=10)
|
||||
stream_handler = logging.StreamHandler()
|
||||
formater = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||||
)
|
||||
file_handler.setFormatter(formater)
|
||||
file_sys_handler.setFormatter(formater)
|
||||
stream_handler.setFormatter(formater)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_sys_handler.setLevel(logging.DEBUG)
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
|
||||
# app.logger
|
||||
app_logger_queue = Queue(-1)
|
||||
app_logger_queue_handler = QueueHandler(app_logger_queue)
|
||||
app_logger_queue_listener = QueueListener(app_logger_queue,
|
||||
file_sys_handler, file_handler, stream_handler,
|
||||
respect_handler_level=True)
|
||||
# app.logger.addHandler(file_handler)
|
||||
# app.logger.addHandler(stream_handler)
|
||||
for handler in app.logger.handlers:
|
||||
app.logger.removeHandler(handler)
|
||||
app.logger.addHandler(app_logger_queue_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.propagate = False # 记录消息将不会传递给当前记录器的祖先记录器的处理器
|
||||
app_logger_queue_listener.start()
|
||||
app.logger.info("WebServer 服务已启动")
|
||||
|
||||
# 框架默认logger指定输出handler(包含了http请求日志)
|
||||
logger = logging.getLogger()
|
||||
logger_queue = Queue(-1)
|
||||
logger_queue_handler = QueueHandler(logger_queue)
|
||||
logger_queue_listener = QueueListener(logger_queue,
|
||||
file_sys_handler, file_handler, stream_handler,
|
||||
respect_handler_level=True)
|
||||
# logger.addHandler(file_sys_handler)
|
||||
# logger.addHandler(stream_handler)
|
||||
# logger.addHandler(file_handler)
|
||||
logger.addHandler(logger_queue_handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger_queue_listener.start()
|
||||
|
||||
|
||||
def register_template_global(app: Flask):
|
||||
# 注册jinja全局函数
|
||||
app.add_template_global(render_to_json, 'render_to_json')
|
||||
app.add_template_global(sort_by_order_in_module, 'sort_by_order_in_module')
|
||||
app.add_template_global(can_show, 'can_show')
|
||||
app.add_template_global(sort_by_order_in_logic_controller, 'sort_by_order_in_logic_controller')
|
||||
app.add_template_global(get_latest_reports, 'get_latest_reports')
|
||||
app.add_template_global(is_forbidden, 'is_forbidden')
|
||||
app.add_template_global(get_case_from_id, 'get_case_from_id')
|
||||
app.add_template_global(get_tool_from_id, 'get_tool_from_id')
|
||||
|
||||
|
||||
def register_before_first_request_funcs(app: Flask):
|
||||
# 在app实例拉起后,第一个请求处理前进行一些准备工作
|
||||
# 注册所有用户socket
|
||||
app.before_first_request_funcs.append(register_all_user_socket)
|
|
@ -0,0 +1,33 @@
|
|||
# coding=utf-8
|
||||
|
||||
import os
|
||||
|
||||
app_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
proj_dir = os.path.dirname(app_dir)
|
||||
attachment_dir = os.path.join(proj_dir, 'attachment')
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
# SERVER_NAME = 'WebUiAutoTest:5000' # enable subdomain support
|
||||
# SERVER_NAME = '0.0.0.0:5000' # enable subdomain support
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
|
||||
|
||||
# Flask-SQLAlchemy插件从SQLALCHEMY_DATABASE_URI配置变量中获取应用的数据库的位置
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(app_dir, 'app.db')
|
||||
# SQLALCHEMY_TRACK_MODIFICATIONS配置项用于设置数据发生变更之后是否发送信号给应用,不需要这项功能,因此将其设置为False。
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# flask_mail 配置
|
||||
MAIL_SERVER = 'smtp-mail.outlook.com'
|
||||
MAIL_PORT = 587
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_USE_SSL = False
|
||||
MAIL_USERNAME = '********'
|
||||
MAIL_PASSWORD = '********'
|
||||
MAIL_DEFAULT_SENDER = ('Admin', MAIL_USERNAME)
|
||||
ADMINS = [MAIL_USERNAME]
|
||||
|
||||
# CSRF
|
||||
WTF_CSRF_TIME_LIMIT = 60 * 60 * 24 # 24h超时限制
|
|
@ -0,0 +1,120 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Tuple, Optional, Iterable
|
||||
import re
|
||||
import lxml.etree
|
||||
|
||||
|
||||
class _Assert:
|
||||
def __init__(self, expected: str, actual: str, negater: bool, failure_msg: str=None):
|
||||
"""
|
||||
:param expected: 期望值
|
||||
:param actual: 实际值
|
||||
:param negater: 是否取反
|
||||
:param failure_msg: 错误信息 (暂未支持自定义错误信息)
|
||||
"""
|
||||
self.expected = expected
|
||||
self.actual = actual
|
||||
self.failure_msg = failure_msg
|
||||
self.negater = negater
|
||||
|
||||
def result(self) -> bool:
|
||||
"""断言结果判断"""
|
||||
pass
|
||||
|
||||
def custom_failure_msg(self):
|
||||
"""错误信息"""
|
||||
pass
|
||||
|
||||
def exec(self) -> Tuple[bool, Optional[str]]:
|
||||
if self.result() ^ self.negater: # 异或
|
||||
return True, None
|
||||
else:
|
||||
if hasattr(self, 'custom_failure_msg'):
|
||||
return False, self.custom_failure_msg()
|
||||
else:
|
||||
return False, self.failure_msg
|
||||
|
||||
|
||||
class AssertEqual(_Assert):
|
||||
def result(self):
|
||||
return self.expected == self.actual
|
||||
|
||||
def custom_failure_msg(self):
|
||||
if not self.negater:
|
||||
return '实际值与期望值不相等: [期望值: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
if self.negater:
|
||||
return '实际值与期望值相等: [期望值: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
|
||||
|
||||
class AssertSubString(_Assert):
|
||||
def result(self):
|
||||
return self.expected in self.actual
|
||||
|
||||
def custom_failure_msg(self):
|
||||
if not self.negater:
|
||||
return '实际值未包含指定期望值: [期望值: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
if self.negater:
|
||||
return '实际值包含指定期望值: [期望值: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
|
||||
|
||||
class AssertContains(_Assert):
|
||||
def result(self):
|
||||
res = re.findall(pattern=self.expected, string=self.actual)
|
||||
return len(res) > 0
|
||||
|
||||
def custom_failure_msg(self):
|
||||
if not self.negater:
|
||||
return '实际值中未匹配到正则: [正则: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
if self.negater:
|
||||
return '实际值中匹配到正则: [正则: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
|
||||
|
||||
class AssertMatches(_Assert):
|
||||
def result(self):
|
||||
res = re.fullmatch(pattern=self.expected, string=self.actual)
|
||||
return bool(res)
|
||||
|
||||
def custom_failure_msg(self):
|
||||
if not self.negater:
|
||||
return '实际值完整未匹配到正则: [正则: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
if self.negater:
|
||||
return '实际值完整匹配到正则: [正则: %s][实际值: %s]' % (self.expected, self.actual)
|
||||
|
||||
|
||||
class AssertXPath(_Assert):
|
||||
|
||||
xpath_separator = '|xpathSeparator|'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.xpath = self.expected.split(self.xpath_separator)[0]
|
||||
self.text = self.expected.split(self.xpath_separator)[-1]
|
||||
self.match_element_count = 0 # xpath匹配到元素数量
|
||||
|
||||
def result(self):
|
||||
html = lxml.etree.HTML(self.actual)
|
||||
elements = html.xpath(self.xpath)
|
||||
if not isinstance(elements, Iterable):
|
||||
return False
|
||||
self.match_element_count = len(elements)
|
||||
if len(elements) > 1:
|
||||
return False
|
||||
elif len(elements) == 1:
|
||||
element = elements[0]
|
||||
if isinstance(element, str):
|
||||
return True if self.text == element else False
|
||||
else:
|
||||
return True if self.text == element.text else False
|
||||
else:
|
||||
return False
|
||||
|
||||
def custom_failure_msg(self):
|
||||
if not self.negater:
|
||||
if self.match_element_count > 1:
|
||||
return '指定的XPath路径 %s 匹配到了 %s 个元素,请调整XPath表达式使其只匹配到一个被测元素' % (self.xpath, self.match_element_count)
|
||||
if self.match_element_count == 0:
|
||||
return '指定的XPath路径 %s 未匹配到任何元素' % self.xpath
|
||||
return '未在指定的XPath路径 %s 下找到文本内容 %s' % (self.xpath, self.text)
|
||||
if self.negater:
|
||||
return '在指定的XPath路径 %s 下找到文本内容 %s' % (self.xpath, self.text)
|
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
Apache-2.0 License
|
||||
References Code: https://github.com/httprunner/httprunner
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
|
||||
def gen_random_string(str_len):
|
||||
""" generate random string with specified length
|
||||
"""
|
||||
return "".join(
|
||||
random.choice(string.ascii_letters + string.digits) for _ in range(str_len)
|
||||
)
|
||||
|
||||
|
||||
def get_timestamp(str_len=13):
|
||||
""" get timestamp string, length can only between 0 and 16
|
||||
"""
|
||||
if isinstance(str_len, int) and 0 < str_len < 17:
|
||||
return str(time.time()).replace(".", "")[:str_len]
|
||||
|
||||
raise RuntimeError("timestamp length can only between 0 and 16.")
|
||||
|
||||
|
||||
def get_current_date(fmt="%Y-%m-%d"):
|
||||
""" get current date, default format is %Y-%m-%d
|
||||
"""
|
||||
return datetime.datetime.now().strftime(fmt)
|
||||
|
||||
|
||||
def sleep(n_secs):
|
||||
""" sleep n seconds
|
||||
"""
|
||||
time.sleep(n_secs)
|
|
@ -0,0 +1,84 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Tuple, Optional
|
||||
import json
|
||||
|
||||
from app.cores.dictionaries import TEST_FIELD, MATCHING_RULE, EXPECTATION_LOGIC
|
||||
from app.cores.assertion import AssertEqual, AssertSubString, AssertContains, AssertMatches, AssertXPath
|
||||
|
||||
|
||||
def get_expectations_result(expectations, request, expectation_logic) -> bool:
|
||||
"""
|
||||
获取期望结果
|
||||
:param expectations: 当前请求案例中所有期望/断言数据, 如果没有设置期望则应该是一个空list。会在执行后将结果更新到表中。
|
||||
:param request: 请求和应答信息 <class HTTPRequest>
|
||||
:param expectation_logic: 期望逻辑处理 与-断言全部是True时则返回True;否-断言只要有一个是True时返回True
|
||||
:return: 只要有一个断言失败则返回False,全部成功则返回True
|
||||
"""
|
||||
rets = []
|
||||
if expectations:
|
||||
for expectation in expectations:
|
||||
actual_value = _get_actual_value_from_test_field(test_field=expectation.test_field, request=request)
|
||||
last_result, last_failure_msg = _assert(
|
||||
expected=expectation.value_,
|
||||
actual=actual_value,
|
||||
matching_rule=expectation.matching_rule,
|
||||
negater=expectation.negater,
|
||||
)
|
||||
rets.append(last_result)
|
||||
# 解决HTTP响应文本内容包含html片段导致前台解析问题 begin
|
||||
if last_result is False and expectation.test_field == TEST_FIELD.TEXT_RESPONSE:
|
||||
last_failure_msg = '[期望值: %s] [实际值: 请参考“响应-响应体”中内容]' % expectation.value_
|
||||
# 解决HTTP响应文本内容包含html片段导致前台解析问题 end
|
||||
expectation.update_assert_result(last_result=last_result, last_failure_msg=last_failure_msg)
|
||||
if len(rets) == 0: # 未设置期望断言则结果为False
|
||||
return False
|
||||
if expectation_logic == EXPECTATION_LOGIC.AND:
|
||||
return all(rets)
|
||||
if expectation_logic == EXPECTATION_LOGIC.OR:
|
||||
return any(rets)
|
||||
|
||||
|
||||
def _get_actual_value_from_test_field(test_field, request):
|
||||
"""
|
||||
根据测试字段获取请求应答中的实际值
|
||||
:param test_field: 测试字段
|
||||
:param request: 请求应答信息 <class HTTPRequest>
|
||||
:return:
|
||||
"""
|
||||
if test_field == TEST_FIELD.TEXT_RESPONSE:
|
||||
if isinstance(request.response_body, str): # request.response_body可能是个字符串或字典
|
||||
return request.response_body
|
||||
elif isinstance(request.response_body, dict):
|
||||
return json.dumps(request.response_body, ensure_ascii=False, default=str)
|
||||
if test_field == TEST_FIELD.RESPONSE_HEADERS:
|
||||
return json.dumps(request.response_headers, ensure_ascii=False)
|
||||
if test_field == TEST_FIELD.REQUEST_DATA:
|
||||
return json.dumps(request.request_body, ensure_ascii=False)
|
||||
if test_field == TEST_FIELD.REQUEST_HEADERS:
|
||||
return json.dumps(request.request_headers, ensure_ascii=False)
|
||||
if test_field == TEST_FIELD.RESPONSE_CODE:
|
||||
return request.response_status_code
|
||||
|
||||
|
||||
def _assert(expected, actual, matching_rule, negater) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
根据matching_rule匹配模式类型进行断言
|
||||
:param expected: 期望值
|
||||
:param actual: 实际值
|
||||
:param matching_rule: 匹配模式
|
||||
:param negater: 是否取反
|
||||
:return:
|
||||
"""
|
||||
if matching_rule == MATCHING_RULE.EQUALS:
|
||||
return AssertEqual(expected=expected, actual=actual, negater=negater).exec()
|
||||
if matching_rule == MATCHING_RULE.SUBSTRING:
|
||||
return AssertSubString(expected=expected, actual=actual, negater=negater).exec()
|
||||
if matching_rule == MATCHING_RULE.CONTAINS:
|
||||
return AssertContains(expected=expected, actual=actual, negater=negater).exec()
|
||||
if matching_rule == MATCHING_RULE.MATCHES:
|
||||
return AssertMatches(expected=expected, actual=actual, negater=negater).exec()
|
||||
if matching_rule == MATCHING_RULE.XPATH:
|
||||
return AssertXPath(expected=expected, actual=actual, negater=negater).exec()
|
||||
raise ValueError('暂不支持 %s 匹配模式' % matching_rule)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# coding=utf-8
|
||||
|
||||
# 记录最近一次测试结果
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from app.extensions import session_id_manager
|
||||
|
||||
|
||||
class LastResult:
|
||||
LastResultPool = {}
|
||||
|
||||
@classmethod
|
||||
def get_last_result(cls) -> dict:
|
||||
session_id = session_id_manager.get_session_id()
|
||||
return cls.LastResultPool.get(session_id)
|
||||
|
||||
@classmethod
|
||||
def update_last_result(cls, result, request_headers, request_body, response_headers, response_body):
|
||||
"""更新所有字段"""
|
||||
cls._check_current_session_id_last_result_exist()
|
||||
session_id = session_id_manager.get_session_id()
|
||||
cls.LastResultPool.get(session_id).update({
|
||||
'result': deepcopy(result),
|
||||
'request_headers': deepcopy(request_headers),
|
||||
'request_body': deepcopy(request_body),
|
||||
'response_headers': deepcopy(response_headers),
|
||||
'response_body': deepcopy(response_body),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def update_request_and_response_to_last_result(cls, request_headers, request_body, response_headers, response_body):
|
||||
"""更新请求和应答数据"""
|
||||
cls._check_current_session_id_last_result_exist()
|
||||
session_id = session_id_manager.get_session_id()
|
||||
cls.LastResultPool.get(session_id).update({
|
||||
'request_headers': deepcopy(request_headers),
|
||||
'request_body': deepcopy(request_body),
|
||||
'response_headers': deepcopy(response_headers),
|
||||
'response_body': deepcopy(response_body),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def update_result_to_last_result(cls, result):
|
||||
"""更新最近一次执行结果"""
|
||||
cls._check_current_session_id_last_result_exist()
|
||||
session_id = session_id_manager.get_session_id()
|
||||
cls.LastResultPool.get(session_id).update({
|
||||
'result': deepcopy(result),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _check_current_session_id_last_result_exist(cls):
|
||||
"""检查当前session_id是否在LastResultPool中,如果没有则新增一个空字典"""
|
||||
session_id = session_id_manager.get_session_id()
|
||||
if session_id not in cls.LastResultPool:
|
||||
cls.LastResultPool.update({
|
||||
session_id: {
|
||||
'result': None, # 上次执行结果
|
||||
'request_headers': None, # 请求头
|
||||
'request_body': None, # 请求包
|
||||
'response_headers': None, # 应答头
|
||||
'response_body': None, # 应答包
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# coding=utf-8
|
||||
|
||||
import sys
|
||||
|
||||
from app.cores.variable import Variable
|
||||
from app.cores.exceptions import PreScriptExecException, PostScriptExecException
|
||||
from app.cores.case.base.last_result import LastResult
|
||||
|
||||
|
||||
def exec_script(source, project_id, log, script_type, glb=None, loc=None):
|
||||
"""
|
||||
执行py脚本
|
||||
:param source: py脚本内容
|
||||
:type source: str
|
||||
:param project_id: 当前执行案例所属的项目id,以获取项目级变量
|
||||
:type project_id: int
|
||||
:param log: 日志对象
|
||||
:type log: Logger
|
||||
:param script_type: 脚本类型
|
||||
:type script_type: str
|
||||
:param glb: 全局变量
|
||||
:param loc: 局部变量
|
||||
:return:
|
||||
"""
|
||||
vars = Variable.get_project_variable(project_id=project_id) # TODO 直接从session里面获取project_id??
|
||||
if glb is None:
|
||||
glb = {}
|
||||
if loc is None:
|
||||
loc = {}
|
||||
loc['vars'] = vars
|
||||
loc['log'] = log
|
||||
try:
|
||||
exec(source, glb, loc)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[-1]
|
||||
script_tb = tb.tb_next
|
||||
if script_type == 'PREPROCESSOR_SCRIPT':
|
||||
failure_message = '预处理脚本执行异常: m行号: %s, 错误信息: %s, 错误类型: %s' % (script_tb.tb_lineno, e.args[0], type(e))
|
||||
script_exception = PreScriptExecException(script_tb.tb_lineno, e.args[0], failure_message)
|
||||
log.error('预处理脚本执行异常: 行号: %s, 错误信息: %s' % (script_exception.line_no, script_exception.value))
|
||||
raise script_exception
|
||||
elif script_type == 'POSTPROCESSOR_SCRIPT':
|
||||
loc['failure'] = True # 后处理脚本异常时,将断言结果置为失败
|
||||
failure_message = '后处理脚本执行异常: 行号: %s, 错误信息: %s, 错误类型: %s' % (script_tb.tb_lineno, e.args[0], type(e))
|
||||
script_exception = PostScriptExecException(script_tb.tb_lineno, e.args[0], failure_message)
|
||||
loc['failure_message'] = failure_message
|
||||
log.error(failure_message)
|
||||
raise script_exception
|
||||
else:
|
||||
raise e
|
||||
return glb, loc
|
||||
|
||||
|
||||
def exec_preprocessor_script(source, project_id, log):
|
||||
"""
|
||||
执行预处理脚本
|
||||
:param source: py脚本内容
|
||||
:type source: str
|
||||
:param project_id: 当前执行案例所属的项目id,以获取项目级变量
|
||||
:type project_id: int
|
||||
:param log: 日志对象
|
||||
:type log: Logger
|
||||
"""
|
||||
loc = {}
|
||||
last_result = LastResult.get_last_result()
|
||||
if last_result is None:
|
||||
last_result = {}
|
||||
loc['result'] = last_result.get('result')
|
||||
loc['request_headers'] = last_result.get('request_headers')
|
||||
loc['request_body'] = last_result.get('request_body')
|
||||
loc['response_headers'] = last_result.get('response_headers')
|
||||
loc['response_body'] = last_result.get('response_body')
|
||||
exec_script(source=source, project_id=project_id, log=log, script_type='PREPROCESSOR_SCRIPT', loc=loc)
|
||||
|
||||
|
||||
def exec_postprocessor_script(source, project_id, case, log):
|
||||
"""
|
||||
执行后处理脚本
|
||||
:param source: py脚本内容
|
||||
:type source: str
|
||||
:param project_id: 当前执行案例所属的项目id,以获取项目级变量
|
||||
:type project_id: int
|
||||
:param case: 案例对象
|
||||
:type case: Case
|
||||
:param log: 日志对象
|
||||
:type log: Logger
|
||||
:return: 断言成功或失败
|
||||
:rtype: bool
|
||||
"""
|
||||
loc = {}
|
||||
last_result = LastResult.get_last_result()
|
||||
if last_result is None:
|
||||
last_result = {}
|
||||
loc['result'] = last_result.get('result')
|
||||
loc['request_headers'] = last_result.get('request_headers')
|
||||
loc['request_body'] = last_result.get('request_body')
|
||||
loc['response_headers'] = last_result.get('response_headers')
|
||||
loc['response_body'] = last_result.get('response_body')
|
||||
loc['failure'] = False # 后处理断言失败标志
|
||||
loc['failure_message'] = '' # 后处理失败信息
|
||||
glb, loc = exec_script(source=source, project_id=project_id, log=log, script_type='POSTPROCESSOR_SCRIPT', glb={},
|
||||
loc=loc)
|
||||
if loc.get('failure') is None:
|
||||
postprocessor_failure = False
|
||||
else:
|
||||
postprocessor_failure = bool(loc.get('failure'))
|
||||
if loc.get('failure_message') is None:
|
||||
postprocessor_failure_message = ''
|
||||
else:
|
||||
postprocessor_failure_message = str(loc.get('failure_message'))
|
||||
case.specific_case.update_postprocessor_failure(postprocessor_failure=postprocessor_failure,
|
||||
postprocessor_failure_message=postprocessor_failure_message)
|
||||
return postprocessor_failure, postprocessor_failure_message
|
|
@ -0,0 +1,99 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.cores.dictionaries import DISPATCHER_TYPE, REPORT_RESULT
|
||||
from app.cores.dispatcher import AbstractCaseDispatcher
|
||||
from app.cores.case.debug.request import make_request
|
||||
from app.cores.case.base.script import exec_postprocessor_script, exec_preprocessor_script
|
||||
from app.cores.case.base.last_result import LastResult
|
||||
from app.cores.case.base.expectation import get_expectations_result
|
||||
|
||||
|
||||
class DebugCaseDispatcher(AbstractCaseDispatcher):
|
||||
def __init__(self, case, dispatcher_type=DISPATCHER_TYPE.DEBUG, logger=None, dispatcher=None):
|
||||
"""
|
||||
:param case: 单个SSHCase案例请求数据
|
||||
:type case: Case
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
:param logger: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度日志
|
||||
:type logger: DispatcherLogger
|
||||
:param dispatcher: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度数据
|
||||
:type dispatcher: Dispatcher
|
||||
"""
|
||||
super().__init__(case=case, dispatcher_type=dispatcher_type, logger=logger, dispatcher=dispatcher)
|
||||
self.request_ = None
|
||||
self.postprocessor_failure = ''
|
||||
self.postprocessor_failure_message = ''
|
||||
self.expectations_result = False
|
||||
|
||||
def set_up(self):
|
||||
super().set_up()
|
||||
# 预处理脚本执行
|
||||
preprocessor_script = self.case.specific_case.preprocessor_script_
|
||||
exec_preprocessor_script(
|
||||
source=preprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
|
||||
def _load_data(self): # 在execute执行开始时执行
|
||||
super()._load_data()
|
||||
self.project_variable = self.case.specific_case.project_variable
|
||||
self.expectation_logic = self.case.specific_case.expectation_logic
|
||||
self.postprocessor_script = self.case.specific_case.postprocessor_script_
|
||||
self.expectations = self.case.specific_case.expectations
|
||||
|
||||
def execute(self):
|
||||
super().execute()
|
||||
# 请求发送
|
||||
self.request_ = make_request(project_variable=self.project_variable)
|
||||
self.request_.send()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
# 更新结果数据1
|
||||
LastResult.update_request_and_response_to_last_result(
|
||||
request_headers=self.request_.request_headers,
|
||||
request_body=self.request_.request_body,
|
||||
response_headers=self.request_.response_headers,
|
||||
response_body=self.request_.response_body,
|
||||
)
|
||||
# 后处理脚本执行
|
||||
self.postprocessor_failure, self.postprocessor_failure_message = exec_postprocessor_script(
|
||||
source=self.postprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
case=self.case,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
# 进行期望判断
|
||||
self.expectations_result = get_expectations_result(expectations=self.expectations,
|
||||
request=self.request_,
|
||||
expectation_logic=self.expectation_logic)
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
log_text = self.dispatcher_logger.get_string_buffer()
|
||||
return dict(
|
||||
request_=self.request_,
|
||||
expectations_result=self.expectations_result,
|
||||
expectations=self.expectations,
|
||||
postprocessor_failure=self.postprocessor_failure,
|
||||
postprocessor_failure_message=self.postprocessor_failure_message,
|
||||
log_text=log_text,
|
||||
result=self.get_assert_result(),
|
||||
elapsed_time=self.request_.elapsed_time,
|
||||
)
|
||||
|
||||
def get_assert_result(self):
|
||||
"""
|
||||
案例执行的断言结果
|
||||
:return: REPORT_RESULT
|
||||
"""
|
||||
if self.expectations:
|
||||
result = all([self.expectations_result, not self.postprocessor_failure])
|
||||
else:
|
||||
result = not self.postprocessor_failure
|
||||
if result:
|
||||
return REPORT_RESULT.SUCCESS
|
||||
else:
|
||||
return REPORT_RESULT.FAILURE
|
|
@ -0,0 +1,78 @@
|
|||
# coding=utf-8
|
||||
|
||||
import time
|
||||
from flask import session
|
||||
|
||||
from app.cores.variable import Variable
|
||||
|
||||
|
||||
class DebugRequest:
|
||||
|
||||
def __init__(self, project_variable):
|
||||
"""
|
||||
:param project_variable: 是否展示项目变量
|
||||
:type project_variable: bool
|
||||
"""
|
||||
self.project_variable = project_variable
|
||||
# 请求与响应
|
||||
self.response_headers = None # type: dict
|
||||
self.response_body = None # type: dict
|
||||
self.request_headers = None # type: dict
|
||||
self.request_body = None # type: dict
|
||||
self.vars = []
|
||||
# 请求响应耗时 ms
|
||||
self.elapsed_time = 0
|
||||
|
||||
def send(self):
|
||||
# send request
|
||||
_start_clock = time.clock()
|
||||
if self.project_variable:
|
||||
self.vars = Variable.get_project_variable(project_id=session.get('project_id'))
|
||||
# recv response
|
||||
_end_clock = time.clock()
|
||||
self.elapsed_time = int((_end_clock - _start_clock) * 1000) + 1 # 执行过快导致结果耗时为0,这里特殊处理+1ms进行展示
|
||||
self._handle_response()
|
||||
|
||||
def _handle_response(self):
|
||||
"""处理应答解析拿到 应答体 应答头 请求体 请求头"""
|
||||
self._handle_response_headers()
|
||||
self._handle_request_body()
|
||||
self._handle_request_headers()
|
||||
self._handle_response_body()
|
||||
|
||||
def _handle_response_headers(self):
|
||||
self.response_headers = {
|
||||
'project_variable': self.project_variable,
|
||||
'elapsed_time': self.elapsed_time,
|
||||
}
|
||||
|
||||
def _handle_request_body(self):
|
||||
if self.project_variable:
|
||||
exec_ = "Variable.get_project_variable(project_id=session.get('project_id)')"
|
||||
else:
|
||||
exec_ = ''
|
||||
self.request_body = {
|
||||
'project_variable': self.project_variable,
|
||||
'exec': exec_,
|
||||
}
|
||||
|
||||
def _handle_request_headers(self):
|
||||
self.request_headers = {
|
||||
'project_variable': self.project_variable,
|
||||
}
|
||||
|
||||
def _handle_response_body(self):
|
||||
self.response_body = {
|
||||
'vars': self.vars,
|
||||
}
|
||||
|
||||
|
||||
def make_request(project_variable):
|
||||
"""
|
||||
:param project_variable: 是否展示项目变量
|
||||
"""
|
||||
return DebugRequest(project_variable=project_variable)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
|
@ -0,0 +1,133 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.models import Case, Dispatcher
|
||||
from app.extensions import http_cookie_manager
|
||||
from app.cores.logger import DispatcherLogger
|
||||
from app.cores.dictionaries import DISPATCHER_TYPE, REPORT_RESULT
|
||||
from app.cores.dispatcher import AbstractCaseDispatcher
|
||||
from app.cores.case.http.request_util import handle_url
|
||||
from app.cores.case.base.script import exec_postprocessor_script, exec_preprocessor_script
|
||||
from app.cores.case.base.last_result import LastResult
|
||||
from app.cores.case.base.expectation import get_expectations_result
|
||||
from app.cores.case.http.request import make_request
|
||||
|
||||
|
||||
class HTTPCaseDispatcher(AbstractCaseDispatcher):
|
||||
def __init__(self, case, dispatcher_type=DISPATCHER_TYPE.DEBUG, logger=None, dispatcher=None):
|
||||
"""
|
||||
:param case: 单个HTTPCase案例请求数据
|
||||
:type case: Case
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
:param logger: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度日志
|
||||
:type logger: DispatcherLogger
|
||||
:param dispatcher: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度数据
|
||||
:type dispatcher: Dispatcher
|
||||
"""
|
||||
super().__init__(case=case, dispatcher_type=dispatcher_type, logger=logger, dispatcher=dispatcher)
|
||||
self.rcj = None
|
||||
self.request_ = None
|
||||
self.postprocessor_failure = ''
|
||||
self.postprocessor_failure_message = ''
|
||||
self.expectations_result = False
|
||||
|
||||
def set_up(self):
|
||||
super().set_up()
|
||||
# 获取cookie
|
||||
self.rcj = http_cookie_manager.get_request_cookie_jar(type=self.dispatcher_type)
|
||||
# 预处理脚本执行
|
||||
preprocessor_script = self.case.specific_case.preprocessor_script_
|
||||
exec_preprocessor_script(
|
||||
source=preprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
|
||||
def _load_data(self): # 在execute执行开始时执行
|
||||
super()._load_data()
|
||||
self.protocol = self.case.specific_case.protocol
|
||||
self.domain = self.case.specific_case.domain_
|
||||
self.port = self.case.specific_case.port_
|
||||
self.method = self.case.specific_case.method
|
||||
self.path = self.case.specific_case.path_
|
||||
self.encoding = self.case.specific_case.encoding_
|
||||
self.expectation_logic = self.case.specific_case.expectation_logic
|
||||
self.message_body = self.case.specific_case.message_body_
|
||||
self.content_type = self.case.specific_case.content_type
|
||||
self.postprocessor_script = self.case.specific_case.postprocessor_script_
|
||||
self.parameters = self.case.specific_case.parameters
|
||||
self.expectations = self.case.specific_case.expectations
|
||||
self.file_upload = self.case.specific_case.file_upload
|
||||
|
||||
def execute(self):
|
||||
super().execute()
|
||||
# 请求发送
|
||||
url = handle_url(protocol=self.protocol,
|
||||
domain=self.domain,
|
||||
port=self.port,
|
||||
path=self.path)
|
||||
self.request_ = make_request(method=self.method,
|
||||
url=url,
|
||||
parameters=self.parameters,
|
||||
message_body=self.message_body,
|
||||
cookies=self.rcj,
|
||||
file_upload=self.file_upload,
|
||||
content_type=self.content_type)
|
||||
self.request_.send()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
# 更新结果数据1
|
||||
LastResult.update_request_and_response_to_last_result(
|
||||
request_headers=self.request_.request_headers,
|
||||
request_body=self.request_.request_body,
|
||||
response_headers=self.request_.response_headers,
|
||||
response_body=self.request_.response_body,
|
||||
)
|
||||
# 后处理脚本执行
|
||||
self.postprocessor_failure, self.postprocessor_failure_message = exec_postprocessor_script(
|
||||
source=self.postprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
case=self.case,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
# 进行期望判断
|
||||
self.expectations_result = get_expectations_result(expectations=self.expectations,
|
||||
request=self.request_,
|
||||
expectation_logic=self.expectation_logic)
|
||||
# 更新结果数据2
|
||||
LastResult.update_result_to_last_result(result=self.expectations_result)
|
||||
# cookie处理
|
||||
http_cookie_manager.update_cookie_pool(
|
||||
rcj=self.request_.response.cookies,
|
||||
type=self.dispatcher_type,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
log_text = self.dispatcher_logger.get_string_buffer()
|
||||
# return self.request_, self.expectations_result, self.expectations, self.postprocessor_failure, log_text
|
||||
return dict(
|
||||
request_=self.request_,
|
||||
expectations_result=self.expectations_result,
|
||||
expectations=self.expectations,
|
||||
postprocessor_failure=self.postprocessor_failure,
|
||||
postprocessor_failure_message=self.postprocessor_failure_message,
|
||||
log_text=log_text,
|
||||
result=self.get_assert_result(),
|
||||
elapsed_time=self.request_.elapsed_time,
|
||||
)
|
||||
|
||||
def get_assert_result(self):
|
||||
"""
|
||||
案例执行的断言结果
|
||||
:return: REPORT_RESULT
|
||||
"""
|
||||
if self.expectations:
|
||||
result = all([self.expectations_result, not self.postprocessor_failure])
|
||||
else:
|
||||
result = not self.postprocessor_failure
|
||||
if result:
|
||||
return REPORT_RESULT.SUCCESS
|
||||
else:
|
||||
return REPORT_RESULT.FAILURE
|
|
@ -0,0 +1,134 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request
|
||||
from requests.cookies import RequestsCookieJar
|
||||
from datetime import timedelta, datetime
|
||||
import uuid
|
||||
from typing import Optional, List, Mapping
|
||||
|
||||
from app.cores.dictionaries import DISPATCHER_TYPE
|
||||
|
||||
# requests库会在发起请求时根据请求服务器的domain选择合适的cookie作为请求头中的cookie数据发出
|
||||
# 可参考代码 requests.models.prepare_cookies()
|
||||
|
||||
|
||||
class HTTPCookieManager:
|
||||
"""管理自动化测试中请求头Cookie数据"""
|
||||
|
||||
# cookie池,不同的会话放到不同的字典中,key为区分会话唯一编号放入cookie中
|
||||
CookiePool = {}
|
||||
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
app.http_cookie_manager = self
|
||||
app.after_request(self._update_cookie)
|
||||
|
||||
def update_cookie_pool(self, http_cookie_manager_id: str = None, rcj: RequestsCookieJar = None, type: str = None):
|
||||
"""
|
||||
:param http_cookie_manager_id: 标识当前会话cookie编号
|
||||
:param rcj: RequestsCookieJar对象,一般由应答头获得 response.cookies
|
||||
:param type: 需要更新到的cookie类型 single_case或build_case
|
||||
:return: None
|
||||
"""
|
||||
if http_cookie_manager_id is None:
|
||||
try:
|
||||
http_cookie_manager_id = request.cookies.get(HTTPCookieManagerConfig.COOKIE_NAME)
|
||||
except KeyError:
|
||||
raise KeyError('在请求上下文cookies中未找到名为%s的cookie' % HTTPCookieManagerConfig.COOKIE_NAME)
|
||||
|
||||
if rcj is None or type is None:
|
||||
self._add_empty_to_cookie_pool(http_cookie_manager_id)
|
||||
else:
|
||||
if http_cookie_manager_id not in __class__.CookiePool:
|
||||
self._add_empty_to_cookie_pool(http_cookie_manager_id)
|
||||
if len(rcj) == 0: # 空的RequestsCookieJar
|
||||
return
|
||||
if type in [DISPATCHER_TYPE.DEBUG, DISPATCHER_TYPE.BUILD]:
|
||||
old_rcj = __class__.CookiePool[http_cookie_manager_id][type]
|
||||
if old_rcj is None:
|
||||
__class__.CookiePool[http_cookie_manager_id][type] = rcj
|
||||
else:
|
||||
self._update_request_cookie_jar(old_rcj=old_rcj, new_rcj=rcj)
|
||||
else:
|
||||
raise ValueError('不支持传入type值为%s,只支持type=single_case 或 type=build_case' % type)
|
||||
|
||||
def _update_request_cookie_jar(self, old_rcj: RequestsCookieJar, new_rcj: RequestsCookieJar):
|
||||
"""将新的rcj更新到老的rcj上"""
|
||||
old_rcj.update(other=new_rcj)
|
||||
|
||||
def _add_empty_to_cookie_pool(self, http_cookie_manager_id: str):
|
||||
"""
|
||||
为当前会话在cookie pool中添加一个新的,默认值为RequestsCookieJar()
|
||||
:param http_cookie_manager_id: 标识当前会话cookie编号
|
||||
:return: None
|
||||
"""
|
||||
__class__.CookiePool.update({
|
||||
http_cookie_manager_id: {
|
||||
DISPATCHER_TYPE.DEBUG: RequestsCookieJar(),
|
||||
DISPATCHER_TYPE.BUILD: RequestsCookieJar(),
|
||||
}
|
||||
})
|
||||
|
||||
def get_request_cookie_jar(self, type: str) -> Optional[RequestsCookieJar]:
|
||||
"""
|
||||
获取当前会话中指定类型的cookie数据
|
||||
:param type: cookie类型 single_case或build_case
|
||||
:return: RequestsCookieJar
|
||||
"""
|
||||
http_cookie_manager_id = request.cookies.get(HTTPCookieManagerConfig.COOKIE_NAME)
|
||||
if http_cookie_manager_id not in __class__.CookiePool:
|
||||
return
|
||||
return __class__.CookiePool[http_cookie_manager_id][type]
|
||||
|
||||
def clear_and_reset(self, rcj: RequestsCookieJar, cookies: List[Mapping]):
|
||||
"""
|
||||
清除并重置RequestsCookieJar
|
||||
:param rcj: 待重置的RequestsCookieJar,来自cookie pool中该会话cookie
|
||||
:param cookies: 新的cookie数据,是一个由字典组成的列表,每一个字典表示一个新的cookie
|
||||
:return: None
|
||||
"""
|
||||
if rcj is None:
|
||||
rcj = RequestsCookieJar()
|
||||
rcj.clear()
|
||||
for cookie in cookies:
|
||||
rcj.set(**cookie)
|
||||
|
||||
def _update_cookie(self, response):
|
||||
"""注册到app.after_request, 当浏览器没有该cookie时,将会在应答头中带上该cookie"""
|
||||
# 判断当前浏览器是否保存有HTTPCookieManager ID的Cookie
|
||||
# 如果没有的话则会在该浏览器中新增一个HTTPCookieManager ID的Cookie,并将ID值保存在CookiePool中
|
||||
if HTTPCookieManagerConfig.COOKIE_NAME not in request.cookies:
|
||||
duration = HTTPCookieManagerConfig.COOKIE_DURATION
|
||||
if isinstance(duration, int):
|
||||
duration = timedelta(seconds=duration)
|
||||
try:
|
||||
expires = datetime.utcnow() + duration
|
||||
except TypeError:
|
||||
raise Exception('HTTPCookieManagerConfig.COOKIE_DURATION must be a ' +
|
||||
'datetime.timedelta, instead got: {0}'.format(duration))
|
||||
uuid_ = str(uuid.uuid1())
|
||||
response.set_cookie(HTTPCookieManagerConfig.COOKIE_NAME,
|
||||
value=uuid_, # 唯一标识
|
||||
expires=expires,
|
||||
domain=None,
|
||||
path='/',
|
||||
secure=HTTPCookieManagerConfig.COOKIE_SECURE,
|
||||
httponly=HTTPCookieManagerConfig.COOKIE_HTTPONLY)
|
||||
self._add_empty_to_cookie_pool(uuid_)
|
||||
else:
|
||||
pass
|
||||
return response
|
||||
|
||||
|
||||
class HTTPCookieManagerConfig:
|
||||
# 标识HTTPCookieManager在当前浏览器客户端对应id的cookie
|
||||
COOKIE_NAME = 'http_cookie_manager_id'
|
||||
|
||||
COOKIE_DURATION = timedelta(days=365)
|
||||
|
||||
COOKIE_SECURE = None
|
||||
|
||||
COOKIE_HTTPONLY = False
|
|
@ -0,0 +1,245 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""
|
||||
Author: zhangzheng
|
||||
Description:
|
||||
Version: 0.0.1
|
||||
LastUpdateDate:
|
||||
UpadteURL:
|
||||
LOG:
|
||||
"""
|
||||
|
||||
import requests
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
import copy
|
||||
import json
|
||||
from typing import Union
|
||||
import re
|
||||
import time
|
||||
|
||||
from app.cores.dictionaries import CONTENT_TYPE
|
||||
from app.cores.case.http.request_util import handle_params, handle_data, handle_file_upload, save_binary_to_file
|
||||
|
||||
|
||||
class HTTPRequest:
|
||||
|
||||
_method = None
|
||||
|
||||
def __init__(self, url, **kwargs):
|
||||
"""
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary, list of tuples or bytes to send
|
||||
in the query string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
|
||||
``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
|
||||
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
|
||||
defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
|
||||
to add for the file.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) How many seconds to wait for the server to send data
|
||||
before giving up, as a float, or a :ref:`(connect timeout, read
|
||||
timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
||||
the server's TLS certificate, or a string, in which case it must be a path
|
||||
to a CA bundle to use. Defaults to ``True``.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
"""
|
||||
self.url = url
|
||||
# self.params = kwargs.get('params')
|
||||
# self.data = kwargs.get('data')
|
||||
# self.json = kwargs.get('json')
|
||||
# self.header = kwargs.get('header')
|
||||
self.kwargs = kwargs
|
||||
# 应答
|
||||
self.response = None # type: requests.Response
|
||||
# deepcopy from self.response
|
||||
self.response_headers = None # type: dict
|
||||
self.response_body = None # type: Union[str, dict, list] # 可能是字典/列表(应答是json串)或字符串(应答是html内容)
|
||||
self.request_headers = None # type: dict
|
||||
self.request_body = None # type: dict
|
||||
self.response_status_code = None # type: str
|
||||
# 请求响应耗时 ms
|
||||
self.elapsed_time = 0
|
||||
|
||||
def send(self) -> requests.Response:
|
||||
# send request
|
||||
_start_clock = time.clock()
|
||||
if self._method == 'get':
|
||||
self.response = requests.get(url=self.url, **self.kwargs)
|
||||
if self._method == 'post':
|
||||
self.response = requests.post(url=self.url, **self.kwargs)
|
||||
if self._method == 'delete':
|
||||
self.response = requests.delete(url=self.url, **self.kwargs)
|
||||
if self._method == 'put':
|
||||
self.response = requests.put(url=self.url, **self.kwargs)
|
||||
if self._method == 'patch':
|
||||
self.response = requests.patch(url=self.url, **self.kwargs)
|
||||
if self._method == 'options':
|
||||
self.response = requests.options(url=self.url, **self.kwargs)
|
||||
if self._method == 'head':
|
||||
self.response = requests.head(url=self.url, **self.kwargs)
|
||||
# recv response
|
||||
_end_clock = time.clock()
|
||||
self.elapsed_time = int((_end_clock - _start_clock) * 1000) + 1
|
||||
self._handle_response()
|
||||
return self.response
|
||||
|
||||
def _handle_response(self):
|
||||
"""处理应答解析拿到 应答体 应答头 请求体 请求头"""
|
||||
self._handle_response_status_code()
|
||||
self._handle_response_headers()
|
||||
self._handle_request_body()
|
||||
self._handle_request_headers()
|
||||
self._handle_response_body()
|
||||
|
||||
def _handle_response_status_code(self):
|
||||
self.response_status_code = str(self.response.status_code)
|
||||
|
||||
def _handle_response_body(self):
|
||||
"""处理应答体"""
|
||||
# self.response_body = self.response.content.decode(self.response.apparent_encoding)
|
||||
try:
|
||||
if self.response.content:
|
||||
file_name = self._check_response_header_has_attachment()
|
||||
if file_name: # 响应头标识为附件
|
||||
self.response_body = str(self.response.content)[2:-1] # "b'PK\x03\x04....\x14\x00'" -> "PK\x03\x04....\x14\x00"
|
||||
save_binary_to_file(bytes=self.response.content, file_name=file_name)
|
||||
else:
|
||||
self.response_body = json.loads(self.response.content)
|
||||
else:
|
||||
self.response_body = ''
|
||||
except json.JSONDecodeError:
|
||||
# www.baidu.com 避免应答乱码
|
||||
# try:
|
||||
# self.response_body = self.response.content.decode(self.response.encoding)
|
||||
# except (UnicodeDecodeError, TypeError):
|
||||
# self.response_body = self.response.content.decode(self.response.apparent_encoding)
|
||||
try:
|
||||
self.response_body = self.response.content.decode(self.response.apparent_encoding)
|
||||
except (UnicodeDecodeError, TypeError):
|
||||
self.response_body = self.response.content.decode(self.response.encoding)
|
||||
|
||||
def _handle_response_headers(self):
|
||||
"""处理应答头"""
|
||||
response_headers = copy.deepcopy(self.response.headers) # dict()方法是不是会重新创建一个新的对象 ,实际为浅复制
|
||||
response_headers['status_code'] = self.response_status_code
|
||||
response_headers['elapsed_time'] = self.elapsed_time
|
||||
self.response_headers = dict(response_headers) # CaseInsensitiveDict转为Dict
|
||||
|
||||
def _handle_request_body(self):
|
||||
"""处理请求体"""
|
||||
self.request_body = {
|
||||
'status_code': self.response_status_code,
|
||||
'method': self.response.request.method,
|
||||
'url': self.response.url,
|
||||
'data': str(self.response.request.body), # 当时文件上传请求时response.request.body类型为bytes,需要转化为str
|
||||
}
|
||||
|
||||
def _handle_request_headers(self):
|
||||
"""处理请求头"""
|
||||
request_headers = copy.deepcopy(self.response.request.headers)
|
||||
self.request_headers = dict(request_headers) # CaseInsensitiveDict转为Dict
|
||||
|
||||
def _check_response_header_has_attachment(self):
|
||||
"""确认应答头中是否包含附件信息, 如果有则直接返回文件名"""
|
||||
check_compile = re.compile('.*(attachment);.*filename=(.+)')
|
||||
for key, value in self.response_headers.items():
|
||||
if key.lower() == 'content-disposition':
|
||||
match = check_compile.match(value)
|
||||
if match is not None:
|
||||
file_name = match.group(2)
|
||||
return file_name
|
||||
return False
|
||||
|
||||
|
||||
class Get(HTTPRequest):
|
||||
_method = 'get'
|
||||
|
||||
|
||||
class Post(HTTPRequest):
|
||||
_method = 'post'
|
||||
|
||||
|
||||
class Delete(HTTPRequest):
|
||||
_method = 'delete'
|
||||
|
||||
|
||||
class Put(HTTPRequest):
|
||||
_method = 'put'
|
||||
|
||||
|
||||
class Patch(HTTPRequest):
|
||||
_method = 'patch'
|
||||
|
||||
|
||||
class Options(HTTPRequest):
|
||||
_method = 'options'
|
||||
|
||||
|
||||
class Head(HTTPRequest):
|
||||
_method = 'head'
|
||||
|
||||
|
||||
def make_request(method, url, *, parameters=None, content_type=None, message_body=None, file_upload=None, **kwargs):
|
||||
"""
|
||||
:param method: 方法 GET/POST...
|
||||
:type method: str
|
||||
:param url: url地址 http://127.0.0.1:5000
|
||||
:type url: str
|
||||
:param parameters: 参数数据
|
||||
:type parameters: HTTPCaseParameter
|
||||
:param content_type: 实际发送的数据类型
|
||||
:type content_type: str
|
||||
:param message_body: 消息体数据
|
||||
:type message_body: str
|
||||
:param file_upload: 文件上传参数数据
|
||||
:type file_upload: HTTPCaseFileUpload
|
||||
:param kwargs: 其他参数
|
||||
:type kwargs: dict
|
||||
:param headers: 请求头数据
|
||||
:type headers: dict
|
||||
|
||||
:rtype: HTTPRequest
|
||||
"""
|
||||
headers = kwargs.get('headers', {})
|
||||
# 选择适当的参数数据, 如果parameters没有数据,则取message_body作为入参数据
|
||||
params = []
|
||||
if (parameters is not None) and (len(parameters) > 0):
|
||||
params = parameters
|
||||
elif (message_body is not None) and (message_body.strip() != ''):
|
||||
params = message_body
|
||||
# 如果类型错误是否编译通过? 使用rtype即使类型错误也可以编译通过。但如果使用type hints如果类型标识错误则编译不通过
|
||||
if method.lower() == 'get':
|
||||
return Get(url=url, params=handle_params(params), **kwargs)
|
||||
if method.lower() == 'post':
|
||||
files = handle_file_upload(file_upload) if file_upload else None
|
||||
if content_type == CONTENT_TYPE.FORM_DATA:
|
||||
data = MultipartEncoder(handle_data(params, content_type=CONTENT_TYPE.FORM_DATA))
|
||||
headers['Content-Type'] = data.content_type
|
||||
else:
|
||||
data = handle_data(params)
|
||||
return Post(url=url, data=data, files=files, headers=headers, **kwargs)
|
||||
if method.lower() == 'delete':
|
||||
return Delete(url=url, data=handle_data(params), **kwargs)
|
||||
if method.lower() == 'put':
|
||||
return Put(url=url, data=handle_data(params), **kwargs)
|
||||
if method.lower() == 'patch':
|
||||
return Patch(url=url, data=handle_data(params), **kwargs)
|
||||
if method.lower() == 'options':
|
||||
return Options(url=url, **kwargs)
|
||||
if method.lower() == 'head':
|
||||
return Head(url=url, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
|
@ -0,0 +1,94 @@
|
|||
# coding=utf-8
|
||||
|
||||
"""
|
||||
Author: zhangzheng
|
||||
Description:
|
||||
Version: 0.0.1
|
||||
LastUpdateDate:
|
||||
UpadteURL:
|
||||
LOG:
|
||||
"""
|
||||
|
||||
from flask_login import current_user
|
||||
from typing import Iterable
|
||||
import json
|
||||
import os
|
||||
|
||||
from app.cores.dictionaries import CONTENT_TYPE
|
||||
|
||||
|
||||
def handle_params(params):
|
||||
"""处理请求参数,使其满足get请求查询字符串"""
|
||||
ret = {}
|
||||
if isinstance(params, str):
|
||||
ret = json.loads(params)
|
||||
elif isinstance(params, Iterable):
|
||||
for param in params:
|
||||
if param.name_ in ret.keys():
|
||||
ret[param.name_].append(param.value_)
|
||||
else:
|
||||
ret[param.name_] = [param.value_]
|
||||
return ret
|
||||
|
||||
|
||||
def handle_data(params, content_type=None):
|
||||
"""
|
||||
处理请求参数,使其满足post请求的数据格式
|
||||
:param params: 参数数据
|
||||
:type params: Iterable[HTTPCaseParameter]
|
||||
:param content_type: HTTP请求体数据类型
|
||||
:type content_type: str
|
||||
"""
|
||||
# return [(param.name, param.value) for param in params]
|
||||
if isinstance(params, str):
|
||||
return tuple(json.loads(params).items())
|
||||
elif isinstance(params, Iterable):
|
||||
if content_type == CONTENT_TYPE.FORM_DATA:
|
||||
return tuple((param.name_, (param.name_, param.value_, param.content_type)) for param in params)
|
||||
return tuple((param.name_, param.value_) for param in params)
|
||||
|
||||
|
||||
def handle_url(protocol, domain, port, path):
|
||||
"""预处理并返回url"""
|
||||
protocol = protocol.strip()
|
||||
domain = domain.strip()
|
||||
port = port.strip()
|
||||
path = path.strip()
|
||||
if port:
|
||||
return protocol + "://" + domain + ":" + port + path
|
||||
else:
|
||||
return protocol + "://" + domain + path
|
||||
|
||||
|
||||
def handle_file_upload(file_upload):
|
||||
"""
|
||||
处理文件上传参数
|
||||
:param file_upload: 文件上传参数
|
||||
:type file_upload: Iterable[HTTPCaseFileUpload]
|
||||
"""
|
||||
|
||||
if file_upload:
|
||||
return {row.name_: open(row.value_, 'rb') for row in file_upload}
|
||||
return None
|
||||
|
||||
|
||||
def render_dict_to_html(d: dict):
|
||||
"""将字典解析为html样式内容,供前端展示"""
|
||||
ret = ''
|
||||
for k, v in d.items():
|
||||
ret += str(k) + ':' + str(v) + '<br>'
|
||||
return ret
|
||||
|
||||
|
||||
def save_binary_to_file(bytes, file_name):
|
||||
"""将二进制内容存储到文件"""
|
||||
from app.config import attachment_dir
|
||||
user_id = current_user.id
|
||||
attachment_user_dir = os.path.join(attachment_dir, str(user_id))
|
||||
file_path = os.path.join(attachment_user_dir, file_name)
|
||||
if not os.path.exists(attachment_dir):
|
||||
os.mkdir(attachment_dir)
|
||||
if not os.path.exists(attachment_user_dir):
|
||||
os.mkdir(attachment_user_dir)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(bytes)
|
|
@ -0,0 +1,108 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.cores.dictionaries import DISPATCHER_TYPE, REPORT_RESULT
|
||||
from app.cores.dispatcher import AbstractCaseDispatcher
|
||||
from app.cores.case.sql.request import make_request
|
||||
from app.cores.case.base.script import exec_postprocessor_script, exec_preprocessor_script
|
||||
from app.cores.case.base.last_result import LastResult
|
||||
from app.cores.case.base.expectation import get_expectations_result
|
||||
|
||||
|
||||
class SQLCaseDispatcher(AbstractCaseDispatcher):
|
||||
def __init__(self, case, dispatcher_type=DISPATCHER_TYPE.DEBUG, logger=None, dispatcher=None):
|
||||
"""
|
||||
:param case: 单个SQLCase案例请求数据
|
||||
:type case: Case
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
:param logger: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度日志
|
||||
:type logger: DispatcherLogger
|
||||
:param dispatcher: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度数据
|
||||
:type dispatcher: Dispatcher
|
||||
"""
|
||||
super().__init__(case=case, dispatcher_type=dispatcher_type, logger=logger, dispatcher=dispatcher)
|
||||
self.request_ = None
|
||||
self.postprocessor_failure = ''
|
||||
self.postprocessor_failure_message = ''
|
||||
self.expectations_result = False
|
||||
|
||||
def set_up(self):
|
||||
super().set_up()
|
||||
# 预处理脚本执行
|
||||
preprocessor_script = self.case.specific_case.preprocessor_script_
|
||||
exec_preprocessor_script(
|
||||
source=preprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
|
||||
def _load_data(self): # 在execute执行开始时执行
|
||||
super()._load_data()
|
||||
self.host = self.case.specific_case.host_
|
||||
self.port = self.case.specific_case.port_
|
||||
self.connect_timeout = self.case.specific_case.connect_timeout_
|
||||
self.user = self.case.specific_case.user_
|
||||
self.password = self.case.specific_case.password_
|
||||
self.db_type = self.case.specific_case.db_type
|
||||
self.sql = self.case.specific_case.sql_
|
||||
self.charset = self.case.specific_case.charset_
|
||||
self.expectation_logic = self.case.specific_case.expectation_logic
|
||||
self.postprocessor_script = self.case.specific_case.postprocessor_script_
|
||||
self.expectations = self.case.specific_case.expectations
|
||||
|
||||
def execute(self):
|
||||
super().execute()
|
||||
# 请求发送
|
||||
self.request_ = make_request(host=self.host, port=self.port, user=self.user, password=self.password,
|
||||
connect_timeout=self.connect_timeout, charset=self.charset, sql=self.sql,
|
||||
db_type=self.db_type)
|
||||
self.request_.send()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
# 更新结果数据1
|
||||
LastResult.update_request_and_response_to_last_result(
|
||||
request_headers=self.request_.request_headers,
|
||||
request_body=self.request_.request_body,
|
||||
response_headers=self.request_.response_headers,
|
||||
response_body=self.request_.response_body,
|
||||
)
|
||||
# 后处理脚本执行
|
||||
self.postprocessor_failure, self.postprocessor_failure_message = exec_postprocessor_script(
|
||||
source=self.postprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
case=self.case,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
# 进行期望判断
|
||||
self.expectations_result = get_expectations_result(expectations=self.expectations,
|
||||
request=self.request_,
|
||||
expectation_logic=self.expectation_logic)
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
log_text = self.dispatcher_logger.get_string_buffer()
|
||||
return dict(
|
||||
request_=self.request_,
|
||||
expectations_result=self.expectations_result,
|
||||
expectations=self.expectations,
|
||||
postprocessor_failure=self.postprocessor_failure,
|
||||
postprocessor_failure_message=self.postprocessor_failure_message,
|
||||
log_text=log_text,
|
||||
result=self.get_assert_result(),
|
||||
elapsed_time=self.request_.elapsed_time,
|
||||
)
|
||||
|
||||
def get_assert_result(self):
|
||||
"""
|
||||
案例执行的断言结果
|
||||
:return: REPORT_RESULT
|
||||
"""
|
||||
if self.expectations:
|
||||
result = all([self.expectations_result, not self.postprocessor_failure])
|
||||
else:
|
||||
result = not self.postprocessor_failure
|
||||
if result:
|
||||
return REPORT_RESULT.SUCCESS
|
||||
else:
|
||||
return REPORT_RESULT.FAILURE
|
|
@ -0,0 +1,166 @@
|
|||
# coding=utf-8
|
||||
|
||||
import time
|
||||
import pymysql
|
||||
from pymysql.cursors import DictCursor
|
||||
|
||||
|
||||
class SQLRequest:
|
||||
|
||||
def __init__(self, host, user, password, port, charset, connect_timeout, db_type, sql):
|
||||
"""
|
||||
:param host: 主机名
|
||||
:param user: 用户
|
||||
:param password: 密码
|
||||
:param port: 端口
|
||||
:param charset: 字符集
|
||||
:param connect_timeout: 超时时间 单位s
|
||||
:param db_type: shell命令
|
||||
:param sql: sql脚本
|
||||
"""
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.charset = charset
|
||||
self.connect_timeout = connect_timeout
|
||||
self.db_type = db_type
|
||||
self.sql = sql
|
||||
# 请求与响应
|
||||
self.response_headers = None # type: dict
|
||||
self.response_body = None # type: dict
|
||||
self.request_headers = None # type: dict
|
||||
self.request_body = None # type: dict
|
||||
self.affected_rows = 0 # 影响行数
|
||||
self.all_rows = () # 执行结果
|
||||
# 执行耗时 ms
|
||||
self.elapsed_time = 0
|
||||
|
||||
def send(self):
|
||||
self.execute(query=self.sql)
|
||||
|
||||
def execute(self, query, args=None):
|
||||
# 分割多行sql脚本
|
||||
sql_list = self._sql_split(query=query)
|
||||
# send request
|
||||
_start_clock = time.clock()
|
||||
with self as cursor:
|
||||
for sql in sql_list:
|
||||
self.affected_rows = cursor.execute(query=sql, args=args)
|
||||
self.all_rows = cursor.fetchall()
|
||||
# recv response
|
||||
_end_clock = time.clock()
|
||||
self.elapsed_time = int((_end_clock - _start_clock) * 1000) + 1
|
||||
self._handle_response()
|
||||
|
||||
def _sql_split(self, query):
|
||||
"""
|
||||
将多行sql文本分割为单行sql文本集合
|
||||
:param query: 单行/多行sql文本
|
||||
:type query: str
|
||||
:return: 分割后的sql列表
|
||||
:rtype: list
|
||||
"""
|
||||
sql_list = []
|
||||
lines = []
|
||||
# 删除注释行 空白行
|
||||
for line in query.split('\n'):
|
||||
if str.strip(line).startswith("--"):
|
||||
continue
|
||||
elif str.strip(line) == '':
|
||||
continue
|
||||
else:
|
||||
lines.append(line)
|
||||
for line in '\n'.join(lines).split(';'):
|
||||
if '\n' in line:
|
||||
line = line.replace('\n', ' ')
|
||||
elif str.strip(line) == '':
|
||||
continue
|
||||
sql_list.append(line)
|
||||
return sql_list
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
:return: 游标
|
||||
:rtype: DictCursor
|
||||
"""
|
||||
self.connection = pymysql.connect(
|
||||
host=self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
port=self.port,
|
||||
charset=self.charset,
|
||||
connect_timeout=self.connect_timeout
|
||||
)
|
||||
self.cursor = self.connection.cursor(cursor=DictCursor)
|
||||
return self.cursor
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
self.connection.rollback()
|
||||
else:
|
||||
self.connection.commit()
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
|
||||
def _handle_response(self):
|
||||
"""处理应答解析拿到 应答体 应答头 请求体 请求头"""
|
||||
self._handle_response_headers()
|
||||
self._handle_request_body()
|
||||
self._handle_request_headers()
|
||||
self._handle_response_body()
|
||||
|
||||
def _handle_response_headers(self):
|
||||
self.response_headers = {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
'connect_timeout': self.connect_timeout,
|
||||
'sql': self.sql,
|
||||
'charset': self.charset,
|
||||
'db_type': self.db_type,
|
||||
'elapsed_time': self.elapsed_time,
|
||||
}
|
||||
|
||||
def _handle_request_body(self):
|
||||
self.request_body = {
|
||||
'sql': self.sql,
|
||||
}
|
||||
|
||||
def _handle_request_headers(self):
|
||||
self.request_headers = {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
'connect_timeout': self.connect_timeout,
|
||||
'sql': self.sql,
|
||||
'charset': self.charset,
|
||||
'db_type': self.db_type,
|
||||
}
|
||||
|
||||
def _handle_response_body(self):
|
||||
self.response_body = {
|
||||
'affected_rows': self.affected_rows,
|
||||
'all_rows': self.all_rows,
|
||||
}
|
||||
|
||||
|
||||
def make_request(host, port, user, password, connect_timeout, db_type, charset, sql):
|
||||
"""
|
||||
:param host: 主机名
|
||||
:param port: 端口
|
||||
:param user: 用户
|
||||
:param password: 密码
|
||||
:param connect_timeout: 超时时间
|
||||
:param db_type: shell命令
|
||||
:param charset: 字符集
|
||||
:param sql: sql脚本
|
||||
"""
|
||||
return SQLRequest(host=host, port=int(port), user=user, password=password, connect_timeout=int(connect_timeout),
|
||||
db_type=db_type, charset=charset, sql=sql)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
|
@ -0,0 +1,106 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.cores.dictionaries import DISPATCHER_TYPE, REPORT_RESULT
|
||||
from app.cores.dispatcher import AbstractCaseDispatcher
|
||||
from app.cores.case.ssh.request import make_request
|
||||
from app.cores.case.base.script import exec_postprocessor_script, exec_preprocessor_script
|
||||
from app.cores.case.base.last_result import LastResult
|
||||
from app.cores.case.base.expectation import get_expectations_result
|
||||
|
||||
|
||||
class SSHCaseDispatcher(AbstractCaseDispatcher):
|
||||
def __init__(self, case, dispatcher_type=DISPATCHER_TYPE.DEBUG, logger=None, dispatcher=None):
|
||||
"""
|
||||
:param case: 单个SSHCase案例请求数据
|
||||
:type case: Case
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
:param logger: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度日志
|
||||
:type logger: DispatcherLogger
|
||||
:param dispatcher: 当case_dispatcher_type为DISPATCHER_TYPE.BUILD时,需要传入调度数据
|
||||
:type dispatcher: Dispatcher
|
||||
"""
|
||||
super().__init__(case=case, dispatcher_type=dispatcher_type, logger=logger, dispatcher=dispatcher)
|
||||
self.request_ = None
|
||||
self.postprocessor_failure = ''
|
||||
self.postprocessor_failure_message = ''
|
||||
self.expectations_result = False
|
||||
|
||||
def set_up(self):
|
||||
super().set_up()
|
||||
# 预处理脚本执行
|
||||
preprocessor_script = self.case.specific_case.preprocessor_script_
|
||||
exec_preprocessor_script(
|
||||
source=preprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
|
||||
def _load_data(self): # 在execute执行开始时执行
|
||||
super()._load_data()
|
||||
self.host_name = self.case.specific_case.host_name_
|
||||
self.port = self.case.specific_case.port_
|
||||
self.connection_timeout = self.case.specific_case.connection_timeout_
|
||||
self.user_name = self.case.specific_case.user_name_
|
||||
self.password = self.case.specific_case.password_
|
||||
self.command = self.case.specific_case.command_
|
||||
self.expectation_logic = self.case.specific_case.expectation_logic
|
||||
self.method = self.case.specific_case.method
|
||||
self.postprocessor_script = self.case.specific_case.postprocessor_script_
|
||||
self.expectations = self.case.specific_case.expectations
|
||||
|
||||
def execute(self):
|
||||
super().execute()
|
||||
# 请求发送
|
||||
self.request_ = make_request(hostname=self.host_name, port=self.port, username=self.user_name,
|
||||
password=self.password, timeout=self.connection_timeout, command=self.command)
|
||||
self.request_.send()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
# 更新结果数据1
|
||||
LastResult.update_request_and_response_to_last_result(
|
||||
request_headers=self.request_.request_headers,
|
||||
request_body=self.request_.request_body,
|
||||
response_headers=self.request_.response_headers,
|
||||
response_body=self.request_.response_body,
|
||||
)
|
||||
# 后处理脚本执行
|
||||
self.postprocessor_failure, self.postprocessor_failure_message = exec_postprocessor_script(
|
||||
source=self.postprocessor_script,
|
||||
project_id=self.case.scene.module.project_id,
|
||||
case=self.case,
|
||||
log=self.dispatcher_logger.logger,
|
||||
)
|
||||
# 进行期望判断
|
||||
self.expectations_result = get_expectations_result(expectations=self.expectations,
|
||||
request=self.request_,
|
||||
expectation_logic=self.expectation_logic)
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
log_text = self.dispatcher_logger.get_string_buffer()
|
||||
return dict(
|
||||
request_=self.request_,
|
||||
expectations_result=self.expectations_result,
|
||||
expectations=self.expectations,
|
||||
postprocessor_failure=self.postprocessor_failure,
|
||||
postprocessor_failure_message=self.postprocessor_failure_message,
|
||||
log_text=log_text,
|
||||
result=self.get_assert_result(),
|
||||
elapsed_time=self.request_.elapsed_time,
|
||||
)
|
||||
|
||||
def get_assert_result(self):
|
||||
"""
|
||||
案例执行的断言结果
|
||||
:return: REPORT_RESULT
|
||||
"""
|
||||
if self.expectations:
|
||||
result = all([self.expectations_result, not self.postprocessor_failure])
|
||||
else:
|
||||
result = not self.postprocessor_failure
|
||||
if result:
|
||||
return REPORT_RESULT.SUCCESS
|
||||
else:
|
||||
return REPORT_RESULT.FAILURE
|
|
@ -0,0 +1,124 @@
|
|||
# coding=utf-8
|
||||
|
||||
import paramiko
|
||||
import time
|
||||
|
||||
|
||||
class SSHRequest:
|
||||
|
||||
def __init__(self, hostname, port, username, password, timeout, command):
|
||||
"""
|
||||
:param hostname: 主机名
|
||||
:param port: 端口
|
||||
:param username: 用户
|
||||
:param password: 密码
|
||||
:param timeout: tcp连接超时时间 单位ms
|
||||
:param command: shell命令
|
||||
"""
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.timeout = timeout / 1000 # ms to s
|
||||
self.command = command
|
||||
# 请求与响应
|
||||
self.response_headers = None # type: dict
|
||||
self.response_body = None # type: dict
|
||||
self.request_headers = None # type: dict
|
||||
self.request_body = None # type: dict
|
||||
self.stdout = ''
|
||||
self.stderr = ''
|
||||
# 请求响应耗时 ms
|
||||
self.elapsed_time = 0
|
||||
|
||||
def exec_command(self):
|
||||
"""
|
||||
:return: stdin, stdout, stderr
|
||||
"""
|
||||
# 打开一个Channel并执行命令
|
||||
return self.client.exec_command(self.command) # stdout 为正确输出,stderr为错误输出,同时是有1个变量有值
|
||||
|
||||
def send(self):
|
||||
with self:
|
||||
# send request
|
||||
_start_clock = time.clock()
|
||||
stdin, stdout, stderr = self.exec_command()
|
||||
# recv response
|
||||
_end_clock = time.clock()
|
||||
self.elapsed_time = int((_end_clock - _start_clock) * 1000) + 1
|
||||
self.stdout = stdout.read().decode('utf-8')
|
||||
self.stderr = stderr.read().decode('utf-8')
|
||||
self._handle_response()
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
|
||||
def __enter__(self):
|
||||
# 实例化SSHClient
|
||||
self.client = paramiko.SSHClient()
|
||||
# 自动添加策略,保存服务器的主机名和密钥信息,如果不添加,那么不再本地know_hosts文件中记录的主机将无法连接
|
||||
# 报错:paramiko.ssh_exception.SSHException: Server '127.0.0.1(Server地址)' not found in known_hosts
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
# 连接SSH服务端,以用户名和密码进行认证
|
||||
self.client.connect(hostname=self.hostname, port=self.port, username=self.username, password=self.password,
|
||||
timeout=self.timeout)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def _handle_response(self):
|
||||
"""处理应答解析拿到 应答体 应答头 请求体 请求头"""
|
||||
self._handle_response_headers()
|
||||
self._handle_request_body()
|
||||
self._handle_request_headers()
|
||||
self._handle_response_body()
|
||||
|
||||
def _handle_response_headers(self):
|
||||
self.response_headers = {
|
||||
'hostname': self.hostname,
|
||||
'port': self.port,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'timeout': self.timeout,
|
||||
'command': self.command,
|
||||
'elapsed_time': self.elapsed_time,
|
||||
}
|
||||
|
||||
def _handle_request_body(self):
|
||||
self.request_body = {
|
||||
'command': self.command,
|
||||
}
|
||||
|
||||
def _handle_request_headers(self):
|
||||
self.request_headers = {
|
||||
'hostname': self.hostname,
|
||||
'port': self.port,
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'timeout': self.timeout,
|
||||
'command': self.command,
|
||||
}
|
||||
|
||||
def _handle_response_body(self):
|
||||
self.response_body = {
|
||||
'stdout': self.stdout,
|
||||
'stderr': self.stderr,
|
||||
}
|
||||
|
||||
|
||||
def make_request(hostname, port, username, password, timeout, command):
|
||||
"""
|
||||
:param hostname: 主机名
|
||||
:param port: 端口
|
||||
:param username: 用户
|
||||
:param password: 密码
|
||||
:param timeout: tcp连接超时时间 单位ms
|
||||
:param command: shell命令
|
||||
"""
|
||||
return SSHRequest(hostname=hostname, port=port, username=username, password=password, timeout=int(timeout),
|
||||
command=command)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
|
@ -0,0 +1,109 @@
|
|||
# coding=utf-8
|
||||
|
||||
|
||||
class STATUS:
|
||||
"""状态"""
|
||||
DELETED = '已删除'
|
||||
NORMAL = '正常'
|
||||
FORBIDDEN = '禁止'
|
||||
|
||||
|
||||
class DISPATCHER_STATUS:
|
||||
"""调度状态"""
|
||||
RUNNING = '正在运行'
|
||||
STOPPING = '正在停止'
|
||||
STOPPED = '已停止'
|
||||
FINISHED = '已完成'
|
||||
|
||||
|
||||
class DISPATCHER_TYPE:
|
||||
"""调度触发类型"""
|
||||
DEBUG = 'DEBUG' # 单一案例组件执行
|
||||
BUILD = 'BUILD' # 由模块或项目执行
|
||||
|
||||
|
||||
class DISPATCHER_END_TYPE:
|
||||
"""调度终止类型"""
|
||||
SUCCESS = '成功'
|
||||
ERROR = '错误'
|
||||
ABORT = '中止' # 手动终止测试
|
||||
|
||||
|
||||
class REPORT_RESULT:
|
||||
"""报告结果"""
|
||||
SUCCESS = '成功'
|
||||
FAILURE = '失败'
|
||||
ERROR = '错误'
|
||||
SKIP = '跳过'
|
||||
ABORT = '中止'
|
||||
RUNNING = '运行中'
|
||||
|
||||
|
||||
class TEST_FIELD:
|
||||
"""测试字段"""
|
||||
TEXT_RESPONSE = '响应文本'
|
||||
RESPONSE_CODE = '响应码'
|
||||
RESPONSE_HEADERS = '响应头'
|
||||
REQUEST_DATA = '请求数据'
|
||||
REQUEST_HEADERS = '请求头'
|
||||
|
||||
|
||||
class MATCHING_RULE:
|
||||
"""匹配规则"""
|
||||
CONTAINS = '包括'
|
||||
MATCHES = '匹配'
|
||||
EQUALS = '相等'
|
||||
SUBSTRING = '子字符串'
|
||||
XPATH = 'XPath'
|
||||
|
||||
|
||||
class EXPECTATION_LOGIC:
|
||||
"""期望结果逻辑"""
|
||||
AND = '与'
|
||||
OR = '或'
|
||||
|
||||
|
||||
class CASE_TYPE:
|
||||
"""案例组件类型"""
|
||||
HTTP = 'HTTP'
|
||||
SQL = 'SQL'
|
||||
SSH = 'SSH'
|
||||
DEBUG = 'DEBUG'
|
||||
|
||||
|
||||
class ELEMENT_TYPE:
|
||||
"""元素类型"""
|
||||
PROJECT = 'PROJECT'
|
||||
MODULE = 'MODULE'
|
||||
SCENE = 'SCENE'
|
||||
CASE = 'CASE'
|
||||
LOGIC_CONTROLLER = 'LOGIC_CONTROLLER'
|
||||
TOOL = 'TOOL'
|
||||
|
||||
|
||||
class LOGIC_CONTROLLER_TYPE:
|
||||
"""控制器类型"""
|
||||
SCENE_CONTROLLER = 'SCENE'
|
||||
SIMPLE_CONTROLLER = 'SIMPLE'
|
||||
IF_CONTROLLER = 'IF'
|
||||
LOOP_CONTROLLER = 'LOOP'
|
||||
WHILE_CONTROLLER = 'WHILE'
|
||||
|
||||
|
||||
class DB_TYPE:
|
||||
"""数据库类型"""
|
||||
MYSQL = 'MYSQL'
|
||||
ORACLE = 'ORACLE'
|
||||
|
||||
|
||||
class TOOL_TYPE:
|
||||
"""工具组件类型"""
|
||||
TIMER = 'TIMER'
|
||||
VARIABLE_DEFINITION = 'VARIABLE_DEFINITION'
|
||||
SCRIPT = 'SCRIPT'
|
||||
|
||||
|
||||
class CONTENT_TYPE:
|
||||
"""HTTP请求体数据类型"""
|
||||
X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
FORM_DATA = 'multipart/form-data'
|
|
@ -0,0 +1,37 @@
|
|||
# coding=utf-8
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def _get_timestamp_and_sign(secret):
|
||||
"""获取时间戳和密钥 https://ding-doc.dingtalk.com/document#/org-dev-guide/custom-robot"""
|
||||
timestamp = str(round(time.time() * 1000))
|
||||
secret_enc = secret.encode('utf-8')
|
||||
string_to_sign = '{}\n{}'.format(timestamp, secret)
|
||||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||||
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
|
||||
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||
return timestamp, sign
|
||||
|
||||
|
||||
def send_message(data, access_token, secret, at_all=False, at_mobiles=''):
|
||||
# 请求参数 可以写入配置文件中
|
||||
timestamp, sign = _get_timestamp_and_sign(secret=secret)
|
||||
data['at'] = {
|
||||
'atMobiles': at_mobiles.split(','),
|
||||
"isAtAll": at_all,
|
||||
}
|
||||
# 机器人的webhooK 获取地址参考:https://open-doc.dingtalk.com/microapp/serverapi2/qf2nxq
|
||||
webhook = "https://oapi.dingtalk.com/robot/send?access_token=" + access_token
|
||||
webhook += "×tamp=" + timestamp
|
||||
webhook += "&sign=" + sign
|
||||
headers = {'content-type': 'application/json'} # 请求头
|
||||
r = requests.post(url=webhook, headers=headers, data=json.dumps(data))
|
||||
r.encoding = 'utf-8'
|
||||
return r.content
|
|
@ -0,0 +1,750 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import current_app, _request_ctx_stack
|
||||
from abc import ABC, abstractmethod
|
||||
from flask import session, request
|
||||
from typing import Optional, Union
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import traceback
|
||||
import json
|
||||
from functools import partial
|
||||
|
||||
from app.template_global import sort_by_order_in_module, sort_by_order_in_project, sort_by_order_in_logic_controller
|
||||
from app.cores.dictionaries import (ELEMENT_TYPE, STATUS, CASE_TYPE, DISPATCHER_STATUS, DISPATCHER_TYPE,
|
||||
DISPATCHER_END_TYPE, REPORT_RESULT, TOOL_TYPE)
|
||||
from app.cores.logger import DispatcherLogger
|
||||
from app.models import (Case, Scene, Module, Project, Dispatcher, DispatcherDetail, Report, ReportCaseData,
|
||||
ReportCaseExpectationData, LogicController, DingTalkRobotSetting, Tool, ReportToolData)
|
||||
from app.cores.ws import emit_dispatcher_result, emit_dispatcher_end, emit_dispatcher_start
|
||||
from app.cores.exceptions import *
|
||||
from app.cores.logic_controller.logic_controller import exec_logic_controller
|
||||
from app.cores.tool import Timer, Script, VariableDefinition
|
||||
from app.cores import dingtalk
|
||||
|
||||
|
||||
class AbstractDispatcher(ABC):
|
||||
def __init__(self, element, logger=None, dispatcher=None, dispatcher_type=None):
|
||||
"""
|
||||
:param element: 构建元素
|
||||
:type element: Union[Case, Scene, Module, Project]
|
||||
:param logger: 调度日志
|
||||
:type logger: Optional[DispatcherLogger]
|
||||
:param dispatcher: 调度数据
|
||||
:type dispatcher: Optional[Dispatcher]
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
"""
|
||||
if isinstance(element, Project):
|
||||
element_type = ELEMENT_TYPE.PROJECT
|
||||
project_id = element.id
|
||||
elif isinstance(element, Module):
|
||||
element_type = ELEMENT_TYPE.MODULE
|
||||
project_id = element.project.id
|
||||
elif isinstance(element, Scene):
|
||||
element_type = ELEMENT_TYPE.SCENE
|
||||
project_id = element.module.project.id
|
||||
else:
|
||||
element_type = ELEMENT_TYPE.CASE
|
||||
project_id = element.scene.module.project.id
|
||||
# 标记当前执行案例所属的project_id,执行结束后会删除掉该session
|
||||
# 用于在app.cores.parser.new_parse_data 中方便获取指定项目的变量池
|
||||
# 定时任务可能存在并发执行的情况,而调度任务是在单独的子线程中执行,因此即使多个调度任务并发执行对session进行操作互不影响
|
||||
# 对于debug模式执行时,由于Flask本身对于每次请求都是不同线程且不同session因此也不会相互影响
|
||||
session['project_id'] = project_id
|
||||
self.dispatcher_type = dispatcher_type
|
||||
# 调试模式直接返回退出__init__
|
||||
if self.dispatcher_type == DISPATCHER_TYPE.DEBUG:
|
||||
return
|
||||
# self.dispatcher_type = DISPATCHER_TYPE.BUILD
|
||||
# if dispatcher is None and element_type in [ELEMENT_TYPE.CASE]:
|
||||
# # 对于元素是案例,但无调度数据,表示当前案例是通过案组件调试触发,不再生成任何调度数据。
|
||||
# self.dispatcher_type = DISPATCHER_TYPE.DEBUG
|
||||
# return
|
||||
# 下面是对参与调度的元素进行处理
|
||||
self.element_is_dispatcher_trigger = False # 对于参与调度的元素,默认将触发标志置为False
|
||||
if dispatcher is not None:
|
||||
self.dispatcher = dispatcher
|
||||
# 只有调度触发元素会执行下面if语句块内的代码
|
||||
if (logger is None or dispatcher is None) and element_type in [ELEMENT_TYPE.PROJECT, ELEMENT_TYPE.MODULE]:
|
||||
# 标识触发调度开始的元素
|
||||
self.element_is_dispatcher_trigger = True
|
||||
# 调度开始元素类型 PROJECT or MODULE
|
||||
self.trigger_element_type = element_type
|
||||
# 如果当前element是触发者(通过项目/模块开始构建测试),则创建一条调度数据, 并实例化调度日志dispatcher_logger
|
||||
self.dispatcher = Dispatcher.add(element_type=self.trigger_element_type, element_id=element.id)
|
||||
self.dispatcher.update_status(status=DISPATCHER_STATUS.RUNNING)
|
||||
self.dispatcher.end_type = DISPATCHER_END_TYPE.SUCCESS # 标识调度终止类型
|
||||
# 实例化调度日志dispatcher_logger
|
||||
self.dispatcher_logger = DispatcherLogger(use_memory_string_handler=True,
|
||||
use_dispatcher_log_db_handler=True,
|
||||
dispatcher_id=self.dispatcher.id)
|
||||
# 发送调度执行开始信息
|
||||
emit_dispatcher_start(id=self.dispatcher.id, type=self.trigger_element_type)
|
||||
# 调度报告
|
||||
Report.add(name=element.name, result=REPORT_RESULT.RUNNING, dispatcher_id=self.dispatcher.id)
|
||||
elif logger is not None:
|
||||
self.dispatcher_logger = logger
|
||||
# 创建一条调度子数据
|
||||
if element_type != ELEMENT_TYPE.CASE:
|
||||
# 元素类型为Project、Module、Scene时会在构造函数中直接创建调度子数据。而元素类型Case/Tool/LogicController是在Scene.execute()方法中创建
|
||||
self.dispatcher_detail = DispatcherDetail.add(element_type=element_type, element_id=element.id,
|
||||
element_name=element.name, dispatcher=self.dispatcher)
|
||||
|
||||
@abstractmethod
|
||||
def set_up(self):
|
||||
"""执行前"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute(self):
|
||||
"""执行"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def tear_down(self):
|
||||
"""执行后"""
|
||||
pass
|
||||
|
||||
def clean(self):
|
||||
# 调试任务最后步骤
|
||||
if self.dispatcher_type == DISPATCHER_TYPE.DEBUG:
|
||||
# 清理日志
|
||||
self.dispatcher_logger.close()
|
||||
# 清除当前请求session中project_id
|
||||
session.pop('project_id')
|
||||
# 调度任务最后步骤
|
||||
elif self.dispatcher_type == DISPATCHER_TYPE.BUILD and self.element_is_dispatcher_trigger:
|
||||
# 更新结束日期
|
||||
self.dispatcher.update_end_time()
|
||||
# 报告分析
|
||||
report_data = self._analyse_report_data(report=self.dispatcher.report)
|
||||
# 发送钉钉通知
|
||||
self._ding_talk_send_message(report_data=report_data)
|
||||
# 清理日志
|
||||
self.dispatcher_logger.close()
|
||||
# 发送调度执行结束信息
|
||||
emit_dispatcher_end(id=self.dispatcher.id,
|
||||
type=self.trigger_element_type,
|
||||
end_type=self.dispatcher.end_type)
|
||||
# 清除当前请求session中project_id
|
||||
session.pop('project_id')
|
||||
# 置状态为已完成
|
||||
self.dispatcher.update_status(status=DISPATCHER_STATUS.FINISHED)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.set_up()
|
||||
self.execute()
|
||||
self.tear_down()
|
||||
self.check_dispatcher_status_and_stop()
|
||||
finally:
|
||||
self.clean()
|
||||
|
||||
def exception_handler(self, e):
|
||||
"""
|
||||
异常处理
|
||||
:param e: 异常对象
|
||||
:type e: Exception
|
||||
"""
|
||||
if isinstance(e, ManualStopException):
|
||||
if self.dispatcher_type != DISPATCHER_TYPE.DEBUG:
|
||||
self.dispatcher.end_type = DISPATCHER_END_TYPE.ABORT # 标识调度终止类型
|
||||
self.dispatcher.update_status(status=DISPATCHER_STATUS.STOPPED)
|
||||
raise e
|
||||
else:
|
||||
self.dispatcher_logger.logger.error('执行异常: %s' % e)
|
||||
self.dispatcher_logger.logger.error(traceback.format_exc())
|
||||
if self.dispatcher_type != DISPATCHER_TYPE.DEBUG:
|
||||
self.dispatcher.update_status(status=DISPATCHER_STATUS.STOPPED)
|
||||
self.dispatcher.end_type = DISPATCHER_END_TYPE.ERROR # 标识调度终止类型
|
||||
raise e
|
||||
|
||||
def check_dispatcher_status_and_stop(self):
|
||||
"""检查调度状态,如果是正在停止状态则终止执行"""
|
||||
if self.dispatcher.status == DISPATCHER_STATUS.STOPPING:
|
||||
msg = '检测到调度id:%s的状态为%s, 将终止运行' % (self.dispatcher.id, self.dispatcher.status)
|
||||
self.dispatcher_logger.logger.warning(msg)
|
||||
raise ManualStopException(msg)
|
||||
|
||||
def _analyse_report_data(self, report):
|
||||
"""
|
||||
分析报告数据
|
||||
:param report: 本次构建所报告
|
||||
:type report: Report
|
||||
:return: 案例执行结果
|
||||
:rtype: dict
|
||||
"""
|
||||
self.dispatcher_logger.logger.info('[报告分析][开始]')
|
||||
report_case_data = report.report_case_data
|
||||
case_count = 0
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
error_count = 0
|
||||
skip_count = 0
|
||||
report_result = ''
|
||||
start_time = 0
|
||||
end_time = 0
|
||||
elapsed_time = 0
|
||||
try:
|
||||
for data in report_case_data:
|
||||
case_count += 1
|
||||
if data.result == REPORT_RESULT.SUCCESS:
|
||||
success_count += 1
|
||||
elif data.result == REPORT_RESULT.FAILURE:
|
||||
failure_count += 1
|
||||
elif data.result == REPORT_RESULT.ERROR:
|
||||
error_count += 1
|
||||
elif data.result == REPORT_RESULT.SKIP:
|
||||
skip_count += 1
|
||||
if self.dispatcher.end_type == DISPATCHER_END_TYPE.ERROR:
|
||||
report.update_result(REPORT_RESULT.ERROR)
|
||||
report_result = REPORT_RESULT.ERROR
|
||||
elif self.dispatcher.end_type == DISPATCHER_END_TYPE.ABORT:
|
||||
report.update_result(REPORT_RESULT.ABORT)
|
||||
report_result = REPORT_RESULT.ABORT
|
||||
elif success_count + skip_count == case_count:
|
||||
report.update_result(REPORT_RESULT.SUCCESS)
|
||||
report_result = REPORT_RESULT.SUCCESS
|
||||
else:
|
||||
report.update_result(REPORT_RESULT.FAILURE)
|
||||
report_result = REPORT_RESULT.FAILURE
|
||||
self.dispatcher_logger.logger.info('[报告分析]本次共执行了%s个案例' % case_count)
|
||||
start_time = report.dispatcher.start_time
|
||||
end_time = report.dispatcher.end_time
|
||||
elapsed_time = end_time - start_time
|
||||
self.dispatcher_logger.logger.info('[报告分析]开始时间: %s, 结束时间: %s, 耗时: %s' % (start_time, end_time, elapsed_time))
|
||||
self.dispatcher_logger.logger.info(
|
||||
'[报告分析]成功:%s个, 失败:%s个, 错误%s个, 跳过%s个' % (success_count, failure_count, error_count, skip_count))
|
||||
self.dispatcher_logger.logger.info('[报告分析][结束]')
|
||||
except Exception as e:
|
||||
report_result = REPORT_RESULT.ERROR
|
||||
self.dispatcher_logger.logger.error('[报告分析][异常] %s' % traceback.format_exc())
|
||||
return {
|
||||
'success_count': success_count,
|
||||
'failure_count': failure_count,
|
||||
'error_count': error_count,
|
||||
'skip_count': skip_count,
|
||||
'report_result': report_result,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'elapsed_time': elapsed_time,
|
||||
}
|
||||
|
||||
def _ding_talk_send_message(self, report_data):
|
||||
"""
|
||||
调度结束时发送钉钉通知
|
||||
:param report_data: 案例执行结果数据
|
||||
:type report_data: dict
|
||||
"""
|
||||
self.dispatcher_logger.logger.info('[钉钉通知][开始]')
|
||||
project_id = session.get('project_id')
|
||||
if project_id is None:
|
||||
self.dispatcher_logger.logger.warning('当前调度获取项目编号project_id为None, 无法获取钉钉配置信息')
|
||||
else:
|
||||
text = """# 构建结果
|
||||
调度编号: {dispatcher_id}
|
||||
组件类型: {element_type}
|
||||
执行结果: {report_result}
|
||||
成功: {success_count} 失败: {failure_count} 错误: {error_count} 跳过: {skip_count}
|
||||
开始时间: {start_time}
|
||||
结束时间: {end_time}
|
||||
耗时: {elapsed_time}
|
||||
"""
|
||||
data = {
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"title": "测试调度通知",
|
||||
"content": text.format(
|
||||
dispatcher_id=str(self.dispatcher.id),
|
||||
element_type='模块' if self.dispatcher.element_type == ELEMENT_TYPE.MODULE else '项目',
|
||||
report_result=str(report_data.get('report_result', '')),
|
||||
success_count=str(report_data.get('success_count', '')),
|
||||
failure_count=str(report_data.get('failure_count', '')),
|
||||
error_count=str(report_data.get('error_count', '')),
|
||||
skip_count=str(report_data.get('skip_count', '')),
|
||||
start_time=str(report_data.get('start_time', '')),
|
||||
end_time=str(report_data.get('end_time', '')),
|
||||
elapsed_time=str(report_data.get('elapsed_time', '')),
|
||||
)
|
||||
},
|
||||
}
|
||||
ding_talk_robot_settings = DingTalkRobotSetting.get_project_ding_talk_robot_setting(project_id=project_id)
|
||||
if not ding_talk_robot_settings:
|
||||
self.dispatcher_logger.logger.warning('钉钉通知]当前项目project_id=%s, 未找到相关钉钉配置信息' % project_id)
|
||||
else:
|
||||
for ding_talk_robot_setting in ding_talk_robot_settings:
|
||||
if ding_talk_robot_setting.enable:
|
||||
self.dispatcher_logger.logger.info('[钉钉通知][发送消息]')
|
||||
response_content = dingtalk.send_message(data=data,
|
||||
access_token=ding_talk_robot_setting.access_token,
|
||||
secret=ding_talk_robot_setting.secret,
|
||||
at_all=ding_talk_robot_setting.at_all,
|
||||
at_mobiles=ding_talk_robot_setting.at_mobiles)
|
||||
self.dispatcher_logger.logger.info('[钉钉通知][收到应答]: ' + str(response_content))
|
||||
self.dispatcher_logger.logger.info('[钉钉通知][结束]')
|
||||
|
||||
|
||||
class AbstractCaseDispatcher(AbstractDispatcher, ABC):
|
||||
def __init__(self, case, dispatcher_type=DISPATCHER_TYPE.DEBUG, logger=None, dispatcher=None):
|
||||
"""
|
||||
:param dispatcher_type: 标识构建是通过单独案例调试(DISPATCHER_TYPE.DEBUG)还是通过模块/项目构建测试(DISPATCHER_TYPE.BUILD)
|
||||
:type dispatcher_type: str
|
||||
"""
|
||||
super().__init__(element=case, logger=logger, dispatcher=dispatcher, dispatcher_type=dispatcher_type)
|
||||
# self.dispatcher_type = dispatcher_type
|
||||
# 日志
|
||||
if self.dispatcher_type == DISPATCHER_TYPE.DEBUG:
|
||||
# 对于single_case触发的案例,则单独生成构建日志
|
||||
self.dispatcher_logger = DispatcherLogger(use_memory_string_handler=True, use_queue_handler=False)
|
||||
self.dispatcher_logger.clear_string_buffer()
|
||||
self.dispatcher_logger.logger.info('[执行案例开始] ==> [案例名称:%s][案例类型:%s]' % (case.name, case.case_type))
|
||||
self.case = case
|
||||
|
||||
@abstractmethod
|
||||
def _load_data(self):
|
||||
"""加载并解析本次案例组件执行需要用到的数据"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute(self):
|
||||
"""执行"""
|
||||
self._load_data()
|
||||
|
||||
def clean(self):
|
||||
self.dispatcher_logger.logger.info('[案例执行结束] ==> [案例名称:%s][案例类型:%s]' % (self.case.name, self.case.case_type))
|
||||
super().clean()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.dispatcher_logger.logger.info('[测试案例]执行阶段[set_up]')
|
||||
self.set_up()
|
||||
self.dispatcher_logger.logger.info('[测试案例]执行阶段[execute]')
|
||||
self.execute()
|
||||
self.dispatcher_logger.logger.info('[测试案例]执行阶段[tear_down]')
|
||||
self.tear_down()
|
||||
if self.dispatcher_type == DISPATCHER_TYPE.BUILD:
|
||||
self.check_dispatcher_status_and_stop()
|
||||
except Exception as e:
|
||||
self.exception_handler(e)
|
||||
finally:
|
||||
self.dispatcher_logger.logger.info('[测试案例]执行阶段[clean]')
|
||||
self.clean()
|
||||
|
||||
|
||||
class AbstractSceneDispatcher(AbstractDispatcher, ABC):
|
||||
def run(self):
|
||||
try:
|
||||
self.dispatcher_logger.logger.info('[测试场景]执行阶段[set_up]')
|
||||
self.set_up()
|
||||
self.dispatcher_logger.logger.info('[测试场景]执行阶段[execute]')
|
||||
self.execute()
|
||||
self.dispatcher_logger.logger.info('[测试场景]执行阶段[tear_down]')
|
||||
self.tear_down()
|
||||
self.check_dispatcher_status_and_stop()
|
||||
except Exception as e:
|
||||
self.exception_handler(e)
|
||||
finally:
|
||||
self.dispatcher_logger.logger.info('[测试场景]执行阶段[clean]')
|
||||
self.clean()
|
||||
|
||||
|
||||
class AbstractModuleDispatcher(AbstractDispatcher, ABC):
|
||||
def run(self):
|
||||
try:
|
||||
self.dispatcher_logger.logger.info('[测试模块]执行阶段[set_up]')
|
||||
self.set_up()
|
||||
self.dispatcher_logger.logger.info('[测试模块]执行阶段[execute]')
|
||||
self.execute()
|
||||
self.dispatcher_logger.logger.info('[测试模块]执行阶段[tear_down]')
|
||||
self.tear_down()
|
||||
self.check_dispatcher_status_and_stop()
|
||||
except Exception as e:
|
||||
self.exception_handler(e)
|
||||
finally:
|
||||
self.dispatcher_logger.logger.info('[测试模块]执行阶段[clean]')
|
||||
self.clean()
|
||||
|
||||
|
||||
class AbstractProjectDispatcher(AbstractDispatcher, ABC):
|
||||
def run(self):
|
||||
try:
|
||||
self.dispatcher_logger.logger.info('[测试项目]执行阶段[set_up]')
|
||||
self.set_up()
|
||||
self.dispatcher_logger.logger.info('[测试项目]执行阶段[execute]')
|
||||
self.execute()
|
||||
self.dispatcher_logger.logger.info('[测试项目]执行阶段[tear_down]')
|
||||
self.tear_down()
|
||||
self.check_dispatcher_status_and_stop()
|
||||
except Exception as e:
|
||||
self.exception_handler(e)
|
||||
finally:
|
||||
self.dispatcher_logger.logger.info('[测试项目]执行阶段[clean]')
|
||||
self.clean()
|
||||
|
||||
|
||||
class ProjectDispatcher(AbstractProjectDispatcher):
|
||||
def __init__(self, project_id):
|
||||
project = Project.query.filter_by(id=project_id).first()
|
||||
super().__init__(element=project, logger=None, dispatcher=None, dispatcher_type=DISPATCHER_TYPE.BUILD)
|
||||
self.project = project
|
||||
self.dispatcher_logger.logger.info(('+'*20 + ' [项目测试开始] ==> [项目名称:%s] ' + '+'*20) % self.project.name)
|
||||
|
||||
def set_up(self):
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
# 获取到当前项目下所有模块数据)
|
||||
modules = self.project.modules
|
||||
for module in sort_by_order_in_project(modules):
|
||||
if module.status in [STATUS.NORMAL]:
|
||||
ModuleDispatcher(module_id=module.id, logger=self.dispatcher_logger, dispatcher=self.dispatcher,
|
||||
dispatcher_type=self.dispatcher_type).run()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
|
||||
def clean(self):
|
||||
self.dispatcher_logger.logger.info(('+'*20 + ' [项目测试结束] ==> [项目名称:%s] ' + '+'*20) % self.project.name)
|
||||
super().clean()
|
||||
|
||||
|
||||
class ModuleDispatcher(AbstractModuleDispatcher):
|
||||
def __init__(self, module_id, logger=None, dispatcher=None, dispatcher_type=None):
|
||||
module = Module.query.filter_by(id=module_id).first()
|
||||
super().__init__(element=module, logger=logger, dispatcher=dispatcher, dispatcher_type=dispatcher_type)
|
||||
self.module = module
|
||||
self.dispatcher_logger.logger.info(('='*15 + ' [模块测试开始] ==> [模块名称:%s] ' + '='*15) % self.module.name)
|
||||
|
||||
def set_up(self):
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
# 获取到当前模块下所有场景数据)
|
||||
scenes = self.module.scenes
|
||||
for scene in sort_by_order_in_module(scenes):
|
||||
if scene.status in [STATUS.NORMAL]:
|
||||
SceneDispatcher(scene=scene, logger=self.dispatcher_logger, dispatcher=self.dispatcher,
|
||||
dispatcher_type=self.dispatcher_type).run()
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
|
||||
def clean(self):
|
||||
self.dispatcher_logger.logger.info(('='*15 + ' [模块测试结束] ==> [模块名称:%s] ' + '='*15) % self.module.name)
|
||||
super().clean()
|
||||
|
||||
|
||||
class SceneDispatcher(AbstractSceneDispatcher):
|
||||
def __init__(self, scene, logger=None, dispatcher=None, dispatcher_type=None):
|
||||
super().__init__(element=scene, logger=logger, dispatcher=dispatcher, dispatcher_type=dispatcher_type)
|
||||
self.scene = scene
|
||||
self.dispatcher_logger.logger.info(('#'*5 + ' [场景测试开始] ==> [场景名称:%s] ' + '#'*5) % self.scene.name)
|
||||
# 是否发生异常时终止执行
|
||||
self.stop_on_error = self.scene.module.project.project_advanced_configuration.stop_on_error
|
||||
|
||||
def set_up(self):
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
# 场景控制器直接执行该控制器下所有组件
|
||||
self._recursive_execute_logic_controller(
|
||||
logic_controller_id=self.scene.scene_controller.logic_controller.id,
|
||||
parent_dispatcher_detail_id=self.dispatcher_detail.id
|
||||
)
|
||||
|
||||
def _recursive_execute_logic_controller(self, logic_controller_id, parent_dispatcher_detail_id):
|
||||
"""
|
||||
存在逻辑控制器嵌套,使用递归方式执行
|
||||
:param logic_controller_id: 逻辑控制器id
|
||||
:param parent_dispatcher_detail_id: 当前调度所属父调度id,是为了写入DispatcherDetail.parent_dispatcher_detail_id表中作为层级关系
|
||||
"""
|
||||
for sub_element_in_logic_controller in sort_by_order_in_logic_controller(logic_controller_id):
|
||||
# 执行案例组件
|
||||
if sub_element_in_logic_controller.element_type == ELEMENT_TYPE.CASE:
|
||||
case = Case.query.filter_by(id=sub_element_in_logic_controller.element_id).first()
|
||||
if case is None:
|
||||
raise DispatcherException('未在表 %s 中查到案例, Case.id=%s' %
|
||||
('Case', sub_element_in_logic_controller.element_id))
|
||||
self._execute_case(case=case, parent_dispatcher_detail_id=parent_dispatcher_detail_id)
|
||||
if sub_element_in_logic_controller.element_type == ELEMENT_TYPE.TOOL:
|
||||
tool = Tool.query.filter_by(id=sub_element_in_logic_controller.element_id).first()
|
||||
if tool is None:
|
||||
raise DispatcherException('未在表 %s 中查到逻辑控制器, Tool.id=%s' %
|
||||
('Tool', sub_element_in_logic_controller.element_id))
|
||||
self._execute_tool(tool=tool, parent_dispatcher_detail_id=parent_dispatcher_detail_id)
|
||||
# 执行逻辑控制器组件(递归执行)
|
||||
elif sub_element_in_logic_controller.element_type == ELEMENT_TYPE.LOGIC_CONTROLLER:
|
||||
# 执行逻辑控制器下所有组件
|
||||
try:
|
||||
# 根据逻辑控制对象判断逻辑控制类型
|
||||
logic_controller = LogicController.query.filter_by(id=sub_element_in_logic_controller.element_id).first()
|
||||
if logic_controller is None:
|
||||
raise DispatcherException('未在表 %s 中查到逻辑控制器, LogicController.id=%s' %
|
||||
('LogicController', sub_element_in_logic_controller.element_id))
|
||||
if logic_controller.status == STATUS.NORMAL:
|
||||
# 给逻辑控制器加上调度详细数据,供报告中展示该逻辑控制器节点
|
||||
logic_controller_dispatcher_detail_id = DispatcherDetail.add(
|
||||
element_type=ELEMENT_TYPE.LOGIC_CONTROLLER,
|
||||
element_id=logic_controller.id,
|
||||
element_name=logic_controller.name,
|
||||
dispatcher=self.dispatcher,
|
||||
parent_dispatcher_detail_id=parent_dispatcher_detail_id).id
|
||||
exec_logic_controller(recursive_func=partial(self._recursive_execute_logic_controller,
|
||||
parent_dispatcher_detail_id=logic_controller_dispatcher_detail_id),
|
||||
logic_controller=logic_controller,
|
||||
logger=self.dispatcher_logger.logger)
|
||||
except Exception as e:
|
||||
self.dispatcher_logger.logger.error(traceback.format_exc())
|
||||
# 如果配置了案例执行错误时不终止,则不向上抛出异常
|
||||
if not isinstance(e, ManualStopException) and not self.stop_on_error:
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _execute_case(self, case, parent_dispatcher_detail_id):
|
||||
"""
|
||||
执行一个案例组件
|
||||
:param case: 案例
|
||||
:type case: Case
|
||||
:param parent_dispatcher_detail_id: 当前案例调度所属的父调度id
|
||||
:type parent_dispatcher_detail_id: int
|
||||
"""
|
||||
if case.status in [STATUS.NORMAL] and case.case_type in [CASE_TYPE.HTTP, CASE_TYPE.SSH, CASE_TYPE.SQL,
|
||||
CASE_TYPE.DEBUG]:
|
||||
# ReportCaseData表数据字段
|
||||
report_id = self.dispatcher.report.id
|
||||
case_type = case.case_type
|
||||
case_id = case.id
|
||||
expectation_logic = case.specific_case.expectation_logic
|
||||
postprocessor_script = case.specific_case.postprocessor_script
|
||||
preprocessor_script = case.specific_case.preprocessor_script
|
||||
request_header = ''
|
||||
request_body = ''
|
||||
response_header = ''
|
||||
response_body = ''
|
||||
postprocessor_result = False
|
||||
postprocessor_failure_message = ''
|
||||
case_result = ''
|
||||
expectations = []
|
||||
elapsed_time = 0
|
||||
# 案例调度对象
|
||||
case_dispatcher = None
|
||||
try:
|
||||
if case.case_type == CASE_TYPE.HTTP:
|
||||
from app.cores.case.http.dispatcher import HTTPCaseDispatcher
|
||||
case_dispatcher = HTTPCaseDispatcher(
|
||||
case=case,
|
||||
dispatcher_type=self.dispatcher_type,
|
||||
logger=self.dispatcher_logger,
|
||||
dispatcher=self.dispatcher
|
||||
)
|
||||
elif case.case_type == CASE_TYPE.SSH:
|
||||
from app.cores.case.ssh.dispatcher import SSHCaseDispatcher
|
||||
case_dispatcher = SSHCaseDispatcher(
|
||||
case=case,
|
||||
dispatcher_type=self.dispatcher_type,
|
||||
logger=self.dispatcher_logger,
|
||||
dispatcher=self.dispatcher
|
||||
)
|
||||
elif case.case_type == CASE_TYPE.SQL:
|
||||
from app.cores.case.sql.dispatcher import SQLCaseDispatcher
|
||||
case_dispatcher = SQLCaseDispatcher(
|
||||
case=case,
|
||||
dispatcher_type=self.dispatcher_type,
|
||||
logger=self.dispatcher_logger,
|
||||
dispatcher=self.dispatcher
|
||||
)
|
||||
elif case.case_type == CASE_TYPE.DEBUG:
|
||||
from app.cores.case.debug.dispatcher import DebugCaseDispatcher
|
||||
case_dispatcher = DebugCaseDispatcher(
|
||||
case=case,
|
||||
dispatcher_type=self.dispatcher_type,
|
||||
logger=self.dispatcher_logger,
|
||||
dispatcher=self.dispatcher
|
||||
)
|
||||
result = case_dispatcher.run()
|
||||
# 获取报告需要的数据
|
||||
request_ = result.get('request_')
|
||||
request_header = json.dumps(request_.request_headers, ensure_ascii=False)
|
||||
request_body = json.dumps(request_.request_body, ensure_ascii=False)
|
||||
response_header = json.dumps(request_.response_headers, ensure_ascii=False)
|
||||
response_body = json.dumps(request_.response_body, ensure_ascii=False, default=str) if isinstance(
|
||||
request_.response_body, dict) else request_.response_body
|
||||
postprocessor_result = not bool(result.get('postprocessor_failure'))
|
||||
postprocessor_failure_message = result.get('postprocessor_failure_message')
|
||||
case_result = result.get('result', REPORT_RESULT.FAILURE)
|
||||
expectations = result.get('expectations', [])
|
||||
elapsed_time = result.get('elapsed_time', 0)
|
||||
except Exception as e:
|
||||
self.dispatcher_logger.logger.error(traceback.format_exc())
|
||||
# 如果是手动中止则直接抛出异常
|
||||
if isinstance(e, ManualStopException):
|
||||
case_result = REPORT_RESULT.ABORT
|
||||
raise e
|
||||
else:
|
||||
case_result = REPORT_RESULT.ERROR
|
||||
# 如果配置了案例执行错误时不终止,则不向上抛出异常
|
||||
if self.stop_on_error:
|
||||
raise e
|
||||
else:
|
||||
# 在case_dispatcher.run()中发生异常会将其置为ABORT,因此这里需要重置为SUCCESS
|
||||
# 表示当案例发生错误但继续执行时调度结束类型不更新(初始就是SUCCESS)
|
||||
self.dispatcher.end_type = DISPATCHER_END_TYPE.SUCCESS
|
||||
# 和上面同理,当继续执行时,重置调度状态为RUNNING(初始就是RUNNING)
|
||||
self.dispatcher.update_status(status=DISPATCHER_STATUS.RUNNING)
|
||||
finally: # 即使try块中出现异常也要执行finally块中代码
|
||||
emit_dispatcher_result( # 通知客户端
|
||||
id=case.id,
|
||||
type=ELEMENT_TYPE.CASE,
|
||||
result=case_result,
|
||||
)
|
||||
# 为案例增加调度子数据
|
||||
dispatcher_detail_id = DispatcherDetail.add(element_type=ELEMENT_TYPE.CASE, element_id=case.id,
|
||||
element_name=case.name, dispatcher=self.dispatcher,
|
||||
parent_dispatcher_detail_id=parent_dispatcher_detail_id).id
|
||||
# 案例增加报告数据
|
||||
report_case_data = ReportCaseData.add(
|
||||
report_id=report_id,
|
||||
dispatcher_detail_id=dispatcher_detail_id,
|
||||
case_type=case_type,
|
||||
case_id=case_id,
|
||||
request_header=request_header,
|
||||
request_body=request_body,
|
||||
response_header=response_header,
|
||||
response_body=response_body,
|
||||
preprocessor_script=preprocessor_script,
|
||||
postprocessor_script=postprocessor_script,
|
||||
postprocessor_result=postprocessor_result,
|
||||
postprocessor_failure_message=postprocessor_failure_message,
|
||||
log=case_dispatcher.dispatcher_logger.get_string_buffer() if case_dispatcher is not None else '',
|
||||
expectation_logic=expectation_logic,
|
||||
result=case_result,
|
||||
elapsed_time=elapsed_time,
|
||||
)
|
||||
if expectations:
|
||||
for expectation in expectations:
|
||||
ReportCaseExpectationData.add(
|
||||
report_case_data_id=report_case_data.id,
|
||||
test_field=expectation.test_field,
|
||||
value=expectation.value,
|
||||
matching_rule=expectation.matching_rule,
|
||||
negater=expectation.negater,
|
||||
result=expectation.last_result,
|
||||
failure_msg=expectation.last_failure_msg,
|
||||
)
|
||||
|
||||
def _execute_tool(self, tool, parent_dispatcher_detail_id):
|
||||
"""
|
||||
执行工具
|
||||
:param tool: 工具
|
||||
:type tool: Tool
|
||||
:param parent_dispatcher_detail_id: 当前工具所属的父调度id
|
||||
:type parent_dispatcher_detail_id: int
|
||||
"""
|
||||
if tool.status in [STATUS.NORMAL] and tool.tool_type in [TOOL_TYPE.TIMER,
|
||||
TOOL_TYPE.SCRIPT,
|
||||
TOOL_TYPE.VARIABLE_DEFINITION]:
|
||||
# ReportToolData表数据字段
|
||||
report_id = self.dispatcher.report.id
|
||||
try:
|
||||
if tool.tool_type == TOOL_TYPE.TIMER:
|
||||
Timer(tool=tool, dispatcher_logger=self.dispatcher_logger).exec_tool()
|
||||
elif tool.tool_type == TOOL_TYPE.SCRIPT:
|
||||
Script(tool=tool, dispatcher_logger=self.dispatcher_logger).exec_tool()
|
||||
elif tool.tool_type == TOOL_TYPE.VARIABLE_DEFINITION:
|
||||
VariableDefinition(tool=tool, dispatcher_logger=self.dispatcher_logger).exec_tool()
|
||||
except Exception as e:
|
||||
self.dispatcher_logger.logger.error('执行异常: \n' + traceback.format_exc())
|
||||
finally:
|
||||
# 工具增加调度子数据
|
||||
dispatcher_detail_id = DispatcherDetail.add(element_type=ELEMENT_TYPE.TOOL, element_id=tool.id,
|
||||
element_name=tool.name, dispatcher=self.dispatcher,
|
||||
parent_dispatcher_detail_id=parent_dispatcher_detail_id).id
|
||||
# 工具增加报告数据
|
||||
ReportToolData.add(tool=tool, report_id=report_id, dispatcher_detail_id=dispatcher_detail_id)
|
||||
|
||||
def tear_down(self):
|
||||
super().tear_down()
|
||||
|
||||
def clean(self):
|
||||
self.dispatcher_logger.logger.info(('#'*5 + ' [场景测试结束] ==> [场景名称:%s] ' + '#'*5) % self.scene.name)
|
||||
super().clean()
|
||||
|
||||
|
||||
def async_module_run(module_id):
|
||||
"""
|
||||
异步执行模块测试
|
||||
"""
|
||||
def run(app, request, session):
|
||||
try:
|
||||
with app.test_request_context(): # 在线程中创建请求上下文,当栈中没有应用上下文时同时也会创建应用上下文
|
||||
# 将主线程请求上下文栈中的request和session放入子线程的请求上下文栈顶
|
||||
_request_ctx_stack.top.request = request
|
||||
_request_ctx_stack.top.session = session
|
||||
ModuleDispatcher(module_id=module_id, dispatcher_type=DISPATCHER_TYPE.BUILD).run()
|
||||
except Exception as e:
|
||||
app.logger.error(traceback.format_exc())
|
||||
executor = ThreadPoolExecutor(1)
|
||||
executor.submit(run, current_app._get_current_object(), request._get_current_object(), session._get_current_object())
|
||||
# d = DispatcherThread()
|
||||
# from threading import Thread
|
||||
# t = Thread(target=d.run, args=(module_id,))
|
||||
# t.start()
|
||||
|
||||
|
||||
def apscheduler_async_module_run(module_id, app, request, session):
|
||||
"""定时任务触发"""
|
||||
try:
|
||||
with app.test_request_context(): # 在线程中创建请求上下文,当栈中没有应用上下文时同时也会创建应用上下文
|
||||
# 将主线程请求上下文栈中的request和session放入子线程的请求上下文栈顶
|
||||
_request_ctx_stack.top.request = request
|
||||
_request_ctx_stack.top.session = session
|
||||
ModuleDispatcher(module_id=module_id, dispatcher_type=DISPATCHER_TYPE.BUILD).run()
|
||||
except Exception as e:
|
||||
app.logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# class DispatcherThread:
|
||||
# def __init__(self):
|
||||
# self.app = current_app._get_current_object()
|
||||
# self.request = request._get_current_object()
|
||||
# self.session = session._get_current_object()
|
||||
#
|
||||
# def run(self, module_id):
|
||||
# try:
|
||||
# with self.app.test_request_context(): # 在线程中创建请求上下文,当栈中没有应用上下文时同时也会创建应用上下文
|
||||
# # 将主线程请求上下文栈中的request和session放入子线程的请求上下文栈顶
|
||||
# _request_ctx_stack.top.request = self.request
|
||||
# _request_ctx_stack.top.session = self.session
|
||||
# ModuleDispatcher(module_id=module_id).run()
|
||||
# except Exception as e:
|
||||
# print(traceback.format_exc())
|
||||
#
|
||||
|
||||
def async_project_run(project_id):
|
||||
"""
|
||||
异步执行项目测试
|
||||
"""
|
||||
def run(app, request, session):
|
||||
try:
|
||||
with app.test_request_context(): # 在线程中创建请求上下文,当栈中没有应用上下文时同时也会创建应用上下文
|
||||
# 将主线程请求上下文栈中的request和session放入子线程的请求上下文栈顶
|
||||
_request_ctx_stack.top.request = request
|
||||
_request_ctx_stack.top.session = session
|
||||
ProjectDispatcher(project_id=project_id).run()
|
||||
except Exception as e:
|
||||
app.logger.error(traceback.format_exc())
|
||||
executor = ThreadPoolExecutor(1)
|
||||
executor.submit(run, current_app._get_current_object(), request._get_current_object(), session._get_current_object())
|
||||
|
||||
|
||||
def apscheduler_async_project_run(project_id, app, request, session):
|
||||
"""定时任务触发"""
|
||||
try:
|
||||
with app.test_request_context(): # 在线程中创建请求上下文,当栈中没有应用上下文时同时也会创建应用上下文
|
||||
# 将主线程请求上下文栈中的request和session放入子线程的请求上下文栈顶
|
||||
_request_ctx_stack.top.request = request
|
||||
_request_ctx_stack.top.session = session
|
||||
ProjectDispatcher(project_id=project_id).run()
|
||||
except Exception as e:
|
||||
app.logger.error(traceback.format_exc())
|
|
@ -0,0 +1,37 @@
|
|||
# coding=utf-8
|
||||
from flask import Flask
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
|
||||
class DispatcherScheduler(BackgroundScheduler):
|
||||
# 调度计划定时任务
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app=app)
|
||||
|
||||
def init_app(self, app: Flask):
|
||||
super().__init__()
|
||||
super().start()
|
||||
# 每次在拉起Flask服务后,第一个请求处理前执行
|
||||
app.before_first_request(self.run_all_scheduler)
|
||||
|
||||
def run_all_scheduler(self):
|
||||
# 拉起所有执行计划
|
||||
from app.cores.dispatcher import apscheduler_async_project_run
|
||||
from app.models import Scheduler
|
||||
from flask import current_app, request, session
|
||||
|
||||
schedulers = Scheduler.query.all()
|
||||
for scheduler in schedulers:
|
||||
if scheduler.enable:
|
||||
job_id = 'project-' + str(scheduler.element_id)
|
||||
super().add_job(func=apscheduler_async_project_run,
|
||||
id=job_id,
|
||||
trigger=CronTrigger.from_crontab(scheduler.cron),
|
||||
kwargs=dict(
|
||||
project_id=scheduler.element_id,
|
||||
app=current_app._get_current_object(),
|
||||
request=request._get_current_object(),
|
||||
session=session._get_current_object()
|
||||
),)
|
|
@ -0,0 +1,36 @@
|
|||
# coding=utf-8
|
||||
|
||||
|
||||
class WebApiAutoTestException(Exception): pass
|
||||
|
||||
|
||||
class ParseException(WebApiAutoTestException): pass
|
||||
|
||||
|
||||
class VariableNotFound(ParseException): pass
|
||||
|
||||
|
||||
class FunctionNotFound(ParseException): pass
|
||||
|
||||
|
||||
class FunctionCallError(ParseException): pass
|
||||
|
||||
|
||||
class DispatcherException(WebApiAutoTestException): pass
|
||||
|
||||
|
||||
class ManualStopException(DispatcherException): pass
|
||||
|
||||
|
||||
class ScriptExecException(DispatcherException):
|
||||
def __init__(self, line_no, value, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.line_no = line_no
|
||||
self.value = value
|
||||
|
||||
|
||||
class PostScriptExecException(ScriptExecException): pass
|
||||
|
||||
|
||||
class PreScriptExecException(ScriptExecException): pass
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# coding=utf-8
|
||||
|
||||
import queue
|
||||
import logging
|
||||
from logging.handlers import QueueHandler, QueueListener
|
||||
from logging import Logger
|
||||
import uuid
|
||||
from flask import current_app
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import Dispatcher
|
||||
|
||||
|
||||
class MemoryStringHandler(logging.Handler):
|
||||
"""将日志信息保存在内存中"""
|
||||
def __init__(self, level=logging.NOTSET):
|
||||
super().__init__(level=level)
|
||||
self.string_buffer = ''
|
||||
|
||||
def emit(self, record):
|
||||
self.string_buffer += self.format(record=record) + '\n'
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
self.string_buffer = ''
|
||||
|
||||
|
||||
class DispatcherLogDBHandler(logging.Handler):
|
||||
"""将日志信息保存在Dispatcher表中"""
|
||||
def __init__(self, dispatcher_id, level=logging.NOTSET):
|
||||
super().__init__(level=level)
|
||||
self.dispatcher_id = dispatcher_id
|
||||
self.app = current_app._get_current_object() # 获取被代理的真实对象,
|
||||
# self.app 用于在队列线程中使用app实例提供的上下文(当该handler结合QueueHandler使用时)
|
||||
|
||||
def emit(self, record):
|
||||
# 强制读取最新结果(从__init__到emit,这期间同一条数据被修改,如果这里不使用db.session.commit(),
|
||||
# 则取self.dispatcher.log数值将不会从数据库更新,获取依然是__init__中查询到的数据)
|
||||
with self.app.app_context():
|
||||
db.session.commit()
|
||||
dispatcher = Dispatcher.query.filter_by(id=self.dispatcher_id).first()
|
||||
dispatcher.log += self.format(record=record) + '\n'
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# class CaseDispatcherLogger:
|
||||
class DispatcherLogger:
|
||||
def __init__(self,
|
||||
use_memory_string_handler=True,
|
||||
use_dispatcher_log_db_handler=False,
|
||||
use_queue_handler=True,
|
||||
dispatcher_id=None):
|
||||
"""
|
||||
创建调度日志
|
||||
:param use_memory_string_handler: 仅使用MemoryStringHandler(), 一般用于单独执行case
|
||||
:param use_dispatcher_log_db_handler: 仅使用DispatcherLogDBHandler(), 将调度日志写入数据库中
|
||||
:param use_queue_handler: 是否使用QueueHandler QueueListener将handler放入异步队列完成日志记录
|
||||
:param dispatcher_id: 当use_dispatcher_log_db_handler为True时需要指定调度编号,调度日志将写入该条调度数据
|
||||
"""
|
||||
if not any([use_memory_string_handler, use_dispatcher_log_db_handler]):
|
||||
raise ValueError('参数use_memory_string_handler与参数use_dispatcher_log_db_handler不能同时为False')
|
||||
self.use_memory_string_handler = use_memory_string_handler
|
||||
self.use_queue_handler = use_queue_handler
|
||||
|
||||
formatter = logging.Formatter('[%(levelname)s %(asctime)s]:%(message)s')
|
||||
self.name = str(uuid.uuid1())
|
||||
self.logger = logging.getLogger(self.name)
|
||||
self.logger.propagate = False # 记录消息将不会传递给当前记录器的祖先记录器的处理器
|
||||
|
||||
handlers = []
|
||||
if use_memory_string_handler:
|
||||
self.memory_string_handler = MemoryStringHandler()
|
||||
self.memory_string_handler.setFormatter(formatter)
|
||||
handlers.append(self.memory_string_handler)
|
||||
if use_dispatcher_log_db_handler:
|
||||
if dispatcher_id is None:
|
||||
raise ValueError('参数use_dispatcher_log_db_handler为True时必须指定dispatcher_id, 当前为None')
|
||||
self.dispatcher_log_db_handler = DispatcherLogDBHandler(dispatcher_id)
|
||||
self.dispatcher_log_db_handler.setFormatter(formatter)
|
||||
handlers.append(self.dispatcher_log_db_handler)
|
||||
|
||||
if self.use_queue_handler:
|
||||
self.queue = queue.Queue(-1)
|
||||
self.queue_handler = QueueHandler(self.queue)
|
||||
self.logger.addHandler(self.queue_handler)
|
||||
self.listener = QueueListener(self.queue, *handlers, respect_handler_level=True)
|
||||
self.listener.start()
|
||||
else:
|
||||
for handler in handlers:
|
||||
self.logger.addHandler(handler)
|
||||
|
||||
def clear_string_buffer(self):
|
||||
if self.use_memory_string_handler:
|
||||
self.memory_string_handler.string_buffer = ''
|
||||
|
||||
def get_string_buffer(self):
|
||||
if self.use_memory_string_handler:
|
||||
return self.memory_string_handler.string_buffer
|
||||
|
||||
def close(self):
|
||||
if self.use_queue_handler:
|
||||
self.listener.stop()
|
||||
# 清理LoggerDict池中临时logger对象
|
||||
Logger.manager.loggerDict.pop(self.name)
|
||||
|
||||
|
||||
# class DispatcherLogger:
|
||||
# def __init__(self, dispatcher_id):
|
||||
# self.queue = queue.Queue(-1)
|
||||
# self.queue_handler = QueueHandler(self.queue)
|
||||
#
|
||||
# self.name = str(uuid.uuid1())
|
||||
# self.logger = logging.getLogger(self.name)
|
||||
# self.logger.propagate = False # 记录消息将不会传递给当前记录器的祖先记录器的处理器
|
||||
#
|
||||
# formatter = logging.Formatter('[%(levelname)s %(asctime)s]:%(message)s')
|
||||
# self.dispatcher_log_db_handler = DispatcherLogDBHandler(dispatcher_id)
|
||||
# self.dispatcher_log_db_handler.setFormatter(formatter)
|
||||
#
|
||||
# self.logger.addHandler(self.queue_handler)
|
||||
# self.listener = QueueListener(self.queue, self.dispatcher_log_db_handler)
|
||||
# self.listener.start()
|
||||
#
|
||||
# def close(self):
|
||||
# self.listener.stop()
|
||||
# # 清理LoggerDict池中临时logger对象
|
||||
# Logger.manager.loggerDict.pop(self.name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger = DispatcherLogger()
|
||||
logger.logger.warning('这是CaseDispatcherLogger打印日志')
|
||||
logger.close()
|
||||
print(logger.get_string_buffer())
|
|
@ -0,0 +1,141 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import session
|
||||
from typing import Callable
|
||||
from logging import Logger
|
||||
|
||||
from app.cores.variable import Variable
|
||||
from app.cores.parser import new_parse_data as _p
|
||||
from app.cores.dictionaries import ELEMENT_TYPE, LOGIC_CONTROLLER_TYPE
|
||||
from app.models import LogicController, IfController, WhileController, LoopController, SimpleController
|
||||
|
||||
|
||||
def exec_logic_controller(recursive_func, logic_controller, logger):
|
||||
"""
|
||||
执行逻辑控制器
|
||||
:param recursive_func: 递归执行函数
|
||||
:type recursive_func: Callable[[int], None]
|
||||
:param logic_controller: 逻辑控制器
|
||||
:type logic_controller: LogicController
|
||||
:param logger: 日志
|
||||
:type logger: Logger
|
||||
"""
|
||||
|
||||
if logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.IF_CONTROLLER:
|
||||
logger.info('[IF控制器][表达式内容:%s]' % logic_controller.specific_controller.expression)
|
||||
exec_if_controller(recursive_func=recursive_func, if_controller=logic_controller.specific_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.WHILE_CONTROLLER:
|
||||
logger.info('[WHILE控制器][表达式内容:%s]' % logic_controller.specific_controller.expression)
|
||||
exec_while_controller(recursive_func=recursive_func, while_controller=logic_controller.specific_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.LOOP_CONTROLLER:
|
||||
logger.info('[LOOP控制器][循环次数:%s]' % logic_controller.specific_controller.expression)
|
||||
exec_loop_controller(recursive_func=recursive_func, loop_controller=logic_controller.specific_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.SIMPLE_CONTROLLER:
|
||||
logger.info('[SIMPLE控制器]')
|
||||
exec_simple_controller(recursive_func=recursive_func, simple_controller=logic_controller.specific_controller)
|
||||
|
||||
|
||||
def exec_simple_controller(recursive_func, simple_controller):
|
||||
"""
|
||||
执行简单控制器
|
||||
:param recursive_func: 递归执行函数
|
||||
:type recursive_func: Callable[[int], None]
|
||||
:param simple_controller: 简单控制器
|
||||
:type simple_controller: SimpleController
|
||||
"""
|
||||
# 直接执行该简单控制器下的所有组件
|
||||
recursive_func(logic_controller_id=simple_controller.logic_controller.id)
|
||||
|
||||
|
||||
def exec_if_controller(recursive_func, if_controller):
|
||||
"""
|
||||
执行if控制器判断
|
||||
:param recursive_func: 递归执行函数
|
||||
:type recursive_func: Callable[[int], None]
|
||||
:param if_controller: if控制器
|
||||
:type if_controller: IfController
|
||||
"""
|
||||
|
||||
def _eval_if_controller_expression(expression):
|
||||
"""
|
||||
执行if控制的表达式
|
||||
:param expression: IF控制器表达式
|
||||
:type expression: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
if expression.strip() == '':
|
||||
return False
|
||||
else:
|
||||
glb = {}
|
||||
loc = {}
|
||||
loc['vars'] = Variable.get_project_variable(session.get('project_id'))
|
||||
return bool(eval(_p(expression), glb, loc))
|
||||
|
||||
# 获取if控制数据
|
||||
if_controller_expression = if_controller.expression
|
||||
if _eval_if_controller_expression(if_controller_expression):
|
||||
# 执行逻辑控制器下所有组件
|
||||
recursive_func(logic_controller_id=if_controller.logic_controller.id)
|
||||
|
||||
|
||||
def exec_while_controller(recursive_func, while_controller):
|
||||
"""
|
||||
执行While控制器判断
|
||||
:param recursive_func: 递归执行函数
|
||||
:type recursive_func: Callable[[int], None]
|
||||
:param while_controller: While控制器
|
||||
:type while_controller: WhileController
|
||||
"""
|
||||
|
||||
def _eval_while_controller_expression(expression):
|
||||
"""
|
||||
执行While控制的表达式
|
||||
:param expression: While控制器表达式
|
||||
:type expression: str
|
||||
:return: True or False
|
||||
:rtype: bool
|
||||
"""
|
||||
if expression.strip() == '':
|
||||
return False
|
||||
else:
|
||||
glb = {}
|
||||
loc = {}
|
||||
loc['vars'] = Variable.get_project_variable(session.get('project_id'))
|
||||
return bool(eval(_p(expression), glb, loc))
|
||||
|
||||
# 获取While控制数据
|
||||
while_controller_expression = while_controller.expression
|
||||
while _eval_while_controller_expression(while_controller_expression):
|
||||
# 执行逻辑控制器下所有组件
|
||||
recursive_func(logic_controller_id=while_controller.logic_controller.id)
|
||||
|
||||
|
||||
def exec_loop_controller(recursive_func, loop_controller):
|
||||
"""
|
||||
执行Loop控制器判断
|
||||
:param recursive_func: 递归执行函数
|
||||
:type recursive_func: Callable[[int], None]
|
||||
:param loop_controller: Loop控制器
|
||||
:type loop_controller: LoopController
|
||||
"""
|
||||
|
||||
def _eval_loop_controller_expression(expression):
|
||||
"""
|
||||
获取Loop控制器循环次数
|
||||
:param expression: Loop控制器表达式
|
||||
:type expression: str
|
||||
:return: 循环次数
|
||||
:rtype: int
|
||||
"""
|
||||
if expression.strip() == '':
|
||||
return 0
|
||||
else:
|
||||
glb = {}
|
||||
loc = {}
|
||||
loc['vars'] = Variable.get_project_variable(session.get('project_id'))
|
||||
return int(eval(_p(expression), glb, loc))
|
||||
|
||||
for _ in range(_eval_loop_controller_expression(loop_controller.expression)):
|
||||
# 执行逻辑控制器下所有组件
|
||||
recursive_func(logic_controller_id=loop_controller.logic_controller.id)
|
|
@ -0,0 +1,370 @@
|
|||
"""
|
||||
Apache-2.0 License
|
||||
References Code: https://github.com/httprunner/httprunner
|
||||
"""
|
||||
|
||||
import re
|
||||
import ast
|
||||
import builtins
|
||||
import types
|
||||
from typing import Any, Text, Callable, Dict
|
||||
from flask import session
|
||||
|
||||
from app.cores import builtin
|
||||
from app.cores.variable import Variable
|
||||
from app.cores.exceptions import FunctionCallError, VariableNotFound, FunctionNotFound
|
||||
|
||||
VariablesMapping = Dict[Text, Any]
|
||||
FunctionsMapping = Dict[Text, Callable]
|
||||
|
||||
dolloar_regex_compile = re.compile(r"\$\$")
|
||||
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
|
||||
variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
|
||||
|
||||
|
||||
def load_module_functions(module) -> Dict[Text, Callable]:
|
||||
""" load python module functions.
|
||||
|
||||
Args:
|
||||
module: python module
|
||||
|
||||
Returns:
|
||||
dict: functions mapping for specified python module
|
||||
|
||||
{
|
||||
"func1_name": func1,
|
||||
"func2_name": func2
|
||||
}
|
||||
|
||||
"""
|
||||
module_functions = {}
|
||||
|
||||
for name, item in vars(module).items():
|
||||
if isinstance(item, types.FunctionType):
|
||||
module_functions[name] = item
|
||||
|
||||
return module_functions
|
||||
|
||||
|
||||
def load_builtin_functions() -> Dict[Text, Callable]:
|
||||
""" load builtin module functions
|
||||
"""
|
||||
return load_module_functions(builtin)
|
||||
|
||||
|
||||
def get_mapping_function(function_name: Text, functions_mapping: FunctionsMapping) -> Callable:
|
||||
""" get function from functions_mapping,
|
||||
if not found, then try to check if builtin function.
|
||||
|
||||
Args:
|
||||
function_name (str): function name
|
||||
functions_mapping (dict): functions mapping
|
||||
|
||||
Returns:
|
||||
mapping function object.
|
||||
|
||||
Raises:
|
||||
exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
|
||||
|
||||
"""
|
||||
if function_name in functions_mapping:
|
||||
return functions_mapping[function_name]
|
||||
|
||||
# modify start: 注释掉当前项目不用的条件
|
||||
# elif function_name in ["parameterize", "P"]:
|
||||
# return loader.load_csv_file
|
||||
#
|
||||
# elif function_name in ["environ", "ENV"]:
|
||||
# return utils.get_os_environ
|
||||
#
|
||||
# elif function_name in ["multipart_encoder", "multipart_content_type"]:
|
||||
# # extension for upload test
|
||||
# from httprunner.ext import uploader
|
||||
#
|
||||
# return getattr(uploader, function_name)
|
||||
# modify end
|
||||
|
||||
try:
|
||||
# check if HttpRunner builtin functions
|
||||
built_in_functions = load_builtin_functions()
|
||||
return built_in_functions[function_name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# check if Python builtin functions
|
||||
return getattr(builtins, function_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
# modify start: 替换成当前项目中定义的异常
|
||||
raise FunctionNotFound(f"{function_name} is not found.")
|
||||
# modify end
|
||||
|
||||
|
||||
def parse_string_value(str_value: Text) -> Any:
|
||||
""" parse string to number if possible
|
||||
e.g. "123" => 123
|
||||
"12.2" => 12.3
|
||||
"abc" => "abc"
|
||||
"$var" => "$var"
|
||||
"""
|
||||
try:
|
||||
return ast.literal_eval(str_value)
|
||||
except ValueError:
|
||||
return str_value
|
||||
except SyntaxError:
|
||||
# e.g. $var, ${func}
|
||||
return str_value
|
||||
|
||||
|
||||
def parse_function_params(params: Text) -> Dict:
|
||||
""" parse function params to args and kwargs.
|
||||
|
||||
Args:
|
||||
params (str): function param in string
|
||||
|
||||
Returns:
|
||||
dict: function meta dict
|
||||
|
||||
{
|
||||
"args": [],
|
||||
"kwargs": {}
|
||||
}
|
||||
|
||||
Examples:
|
||||
>>> parse_function_params("")
|
||||
{'args': [], 'kwargs': {}}
|
||||
|
||||
>>> parse_function_params("5")
|
||||
{'args': [5], 'kwargs': {}}
|
||||
|
||||
>>> parse_function_params("1, 2")
|
||||
{'args': [1, 2], 'kwargs': {}}
|
||||
|
||||
>>> parse_function_params("a=1, b=2")
|
||||
{'args': [], 'kwargs': {'a': 1, 'b': 2}}
|
||||
|
||||
>>> parse_function_params("1, 2, a=3, b=4")
|
||||
{'args': [1, 2], 'kwargs': {'a': 3, 'b': 4}}
|
||||
|
||||
"""
|
||||
function_meta = {"args": [], "kwargs": {}}
|
||||
|
||||
params_str = params.strip()
|
||||
if params_str == "":
|
||||
return function_meta
|
||||
|
||||
args_list = params_str.split(",")
|
||||
for arg in args_list:
|
||||
arg = arg.strip()
|
||||
if "=" in arg:
|
||||
key, value = arg.split("=")
|
||||
function_meta["kwargs"][key.strip()] = parse_string_value(value.strip())
|
||||
else:
|
||||
function_meta["args"].append(parse_string_value(arg))
|
||||
|
||||
return function_meta
|
||||
|
||||
|
||||
def get_mapping_variable(
|
||||
variable_name: Text, variables_mapping: VariablesMapping
|
||||
) -> Any:
|
||||
""" get variable from variables_mapping.
|
||||
|
||||
Args:
|
||||
variable_name (str): variable name
|
||||
variables_mapping (dict): variables mapping
|
||||
|
||||
Returns:
|
||||
mapping variable value.
|
||||
|
||||
Raises:
|
||||
exceptions.VariableNotFound: variable is not found.
|
||||
|
||||
"""
|
||||
try:
|
||||
return variables_mapping[variable_name]
|
||||
except KeyError:
|
||||
# modify start: 替换成当前项目中定义的异常
|
||||
raise VariableNotFound(
|
||||
f"{variable_name} not found in {variables_mapping}"
|
||||
)
|
||||
# modify end
|
||||
|
||||
|
||||
def parse_string(
|
||||
raw_string: Text,
|
||||
variables_mapping: VariablesMapping,
|
||||
functions_mapping: FunctionsMapping,
|
||||
) -> Any:
|
||||
""" parse string content with variables and functions mapping.
|
||||
|
||||
Args:
|
||||
raw_string: raw string content to be parsed.
|
||||
variables_mapping: variables mapping.
|
||||
functions_mapping: functions mapping.
|
||||
|
||||
Returns:
|
||||
str: parsed string content.
|
||||
|
||||
Examples:
|
||||
>>> raw_string = "abc${add_one($num)}def"
|
||||
>>> variables_mapping = {"num": 3}
|
||||
>>> functions_mapping = {"add_one": lambda x: x + 1}
|
||||
>>> parse_string(raw_string, variables_mapping, functions_mapping)
|
||||
"abc4def"
|
||||
|
||||
"""
|
||||
try:
|
||||
match_start_position = raw_string.index("$", 0)
|
||||
parsed_string = raw_string[0:match_start_position]
|
||||
except ValueError:
|
||||
parsed_string = raw_string
|
||||
return parsed_string
|
||||
|
||||
while match_start_position < len(raw_string):
|
||||
|
||||
# Notice: notation priority
|
||||
# $$ > ${func($a, $b)} > $var
|
||||
|
||||
# search $$
|
||||
dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
|
||||
if dollar_match:
|
||||
match_start_position = dollar_match.end()
|
||||
parsed_string += "$"
|
||||
continue
|
||||
|
||||
# search function like ${func($a, $b)}
|
||||
func_match = function_regex_compile.match(raw_string, match_start_position)
|
||||
if func_match:
|
||||
func_name = func_match.group(1)
|
||||
func = get_mapping_function(func_name, functions_mapping)
|
||||
|
||||
func_params_str = func_match.group(2)
|
||||
function_meta = parse_function_params(func_params_str)
|
||||
args = function_meta["args"]
|
||||
kwargs = function_meta["kwargs"]
|
||||
parsed_args = parse_data(args, variables_mapping, functions_mapping)
|
||||
parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
|
||||
|
||||
try:
|
||||
func_eval_value = func(*parsed_args, **parsed_kwargs)
|
||||
except Exception as ex:
|
||||
# modify start: 替换成当前项目中定义的异常
|
||||
raise FunctionCallError(
|
||||
f"call function error:\n"
|
||||
f"func_name: {func_name}\n"
|
||||
f"args: {parsed_args}\n"
|
||||
f"kwargs: {parsed_kwargs}\n"
|
||||
f"{type(ex).__name__}: {ex}"
|
||||
)
|
||||
# modify end
|
||||
|
||||
func_raw_str = "${" + func_name + f"({func_params_str})" + "}"
|
||||
if func_raw_str == raw_string:
|
||||
# raw_string is a function, e.g. "${add_one(3)}", return its eval value directly
|
||||
return func_eval_value
|
||||
|
||||
# raw_string contains one or many functions, e.g. "abc${add_one(3)}def"
|
||||
parsed_string += str(func_eval_value)
|
||||
match_start_position = func_match.end()
|
||||
continue
|
||||
|
||||
# search variable like ${var} or $var
|
||||
var_match = variable_regex_compile.match(raw_string, match_start_position)
|
||||
if var_match:
|
||||
var_name = var_match.group(1) or var_match.group(2)
|
||||
# modify start: 如果未匹配到变量则按照raw文本返回
|
||||
# var_value = get_mapping_variable(var_name, variables_mapping)
|
||||
try:
|
||||
var_value = get_mapping_variable(var_name, variables_mapping)
|
||||
except VariableNotFound:
|
||||
var_value = var_match.group(0) # 比如匹配到了${num}, 当num变量未找到时,依然返回${num}
|
||||
# modify end
|
||||
if f"${var_name}" == raw_string or "${" + var_name + "}" == raw_string:
|
||||
# raw_string is a variable, $var or ${var}, return its value directly
|
||||
return var_value
|
||||
|
||||
# raw_string contains one or many variables, e.g. "abc${var}def"
|
||||
parsed_string += str(var_value)
|
||||
match_start_position = var_match.end()
|
||||
continue
|
||||
|
||||
curr_position = match_start_position
|
||||
try:
|
||||
# find next $ location
|
||||
match_start_position = raw_string.index("$", curr_position + 1)
|
||||
remain_string = raw_string[curr_position:match_start_position]
|
||||
except ValueError:
|
||||
remain_string = raw_string[curr_position:]
|
||||
# break while loop
|
||||
match_start_position = len(raw_string)
|
||||
|
||||
parsed_string += remain_string
|
||||
|
||||
return parsed_string
|
||||
|
||||
|
||||
def parse_data(
|
||||
raw_data: Any,
|
||||
variables_mapping: VariablesMapping = None,
|
||||
functions_mapping: FunctionsMapping = None,
|
||||
) -> Any:
|
||||
""" parse raw data with evaluated variables mapping.
|
||||
Notice: variables_mapping should not contain any variable or function.
|
||||
"""
|
||||
if isinstance(raw_data, str):
|
||||
# content in string format may contains variables and functions
|
||||
variables_mapping = variables_mapping or {}
|
||||
functions_mapping = functions_mapping or {}
|
||||
# only strip whitespaces and tabs, \n\r is left because they maybe used in changeset
|
||||
raw_data = raw_data.strip(" \t")
|
||||
return parse_string(raw_data, variables_mapping, functions_mapping)
|
||||
|
||||
elif isinstance(raw_data, (list, set, tuple)):
|
||||
return [
|
||||
parse_data(item, variables_mapping, functions_mapping) for item in raw_data
|
||||
]
|
||||
|
||||
elif isinstance(raw_data, dict):
|
||||
parsed_data = {}
|
||||
for key, value in raw_data.items():
|
||||
parsed_key = parse_data(key, variables_mapping, functions_mapping)
|
||||
parsed_value = parse_data(value, variables_mapping, functions_mapping)
|
||||
parsed_data[parsed_key] = parsed_value
|
||||
|
||||
return parsed_data
|
||||
|
||||
else:
|
||||
# other types, e.g. None, int, float, bool
|
||||
return raw_data
|
||||
|
||||
|
||||
# modify start: 定义新的入口函数
|
||||
def new_parse_data(raw_data: str) -> str:
|
||||
if session.get('project_id') is None:
|
||||
return raw_data
|
||||
project_vars = Variable.get_project_variable(session.get('project_id'))
|
||||
return parse_data(raw_data=raw_data, variables_mapping=project_vars)
|
||||
# modify end
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
# print(parse_data('aaa${num}bbb', {'num': 3}, {}))
|
||||
# print(parse_data('aaa${num} ${hhh} bbb', {'num': 3, 'hhh': 'gan'}, {}))
|
||||
# print(parse_data('aaa${num}$hhh bbb', {'num': 3, 'hhh': 'gan'}, {}))
|
||||
# print(parse_data('aaa${add_one($num)}bbb', {'num': 3, 'hhh': 'gan'}, {"add_one": lambda x: x + 1}))
|
||||
# def is_true():
|
||||
# return True
|
||||
# print(parse_data('aaa${is_true()}bbb', {'num': 3, 'hhh': 'gan'}, {"is_true": is_true}))
|
||||
# print(parse_data('aaa${add($num, $hhh)}bbb', {'num': 3, 'hhh': 4}, {"add": lambda x, y: x + y}))
|
||||
# print(parse_data('aaa${get_current_date()}bbb', {}, {}))
|
||||
# muilt_line_str = """aaa
|
||||
# ${get_current_date()}
|
||||
# ${get_timestamp()}
|
||||
# bbb
|
||||
# """
|
||||
# print(parse_data(muilt_line_str, {}, {}))
|
||||
# print(parse_data(muilt_line_str))
|
||||
# print(parse_data('aaa${num}bbb$hello', {'hello': ' this is hello '}, {}))
|
|
@ -0,0 +1,53 @@
|
|||
# coding=utf-8
|
||||
|
||||
import datetime
|
||||
from datetime import timedelta, datetime
|
||||
from flask import request
|
||||
import uuid
|
||||
|
||||
|
||||
class SessionIDManager:
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app=app)
|
||||
|
||||
def init_app(self, app):
|
||||
app.session_id_manager = self
|
||||
app.after_request(self._update_cookie)
|
||||
|
||||
def _update_cookie(self, response):
|
||||
"""注册到app.after_request, 当浏览器没有该cookie时,将会在应答头中带上该cookie"""
|
||||
if SessionIDManagerConfig.COOKIE_NAME not in request.cookies:
|
||||
duration = SessionIDManagerConfig.COOKIE_DURATION
|
||||
if isinstance(duration, int):
|
||||
duration = timedelta(seconds=duration)
|
||||
try:
|
||||
expires = datetime.utcnow() + duration
|
||||
except TypeError:
|
||||
raise Exception('SessionIDManagerConfig.COOKIE_DURATION must be a ' +
|
||||
'datetime.timedelta, instead got: {0}'.format(duration))
|
||||
uuid_ = str(uuid.uuid1())
|
||||
response.set_cookie(SessionIDManagerConfig.COOKIE_NAME,
|
||||
value=uuid_, # 唯一标识
|
||||
expires=expires,
|
||||
domain=None,
|
||||
path='/',
|
||||
secure=SessionIDManagerConfig.COOKIE_SECURE,
|
||||
httponly=SessionIDManagerConfig.COOKIE_HTTPONLY)
|
||||
else:
|
||||
pass
|
||||
return response
|
||||
|
||||
def get_session_id(self) -> str:
|
||||
"""获取当前会话的session_id"""
|
||||
return request.cookies.get(SessionIDManagerConfig.COOKIE_NAME)
|
||||
|
||||
|
||||
class SessionIDManagerConfig:
|
||||
COOKIE_NAME = 'session_id'
|
||||
|
||||
COOKIE_DURATION = timedelta(days=365)
|
||||
|
||||
COOKIE_SECURE = None
|
||||
|
||||
COOKIE_HTTPONLY = False
|
|
@ -0,0 +1,5 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.cores.tool.timer import Timer
|
||||
from app.cores.tool.script import Script
|
||||
from app.cores.tool.variable_definition import VariableDefinition
|
|
@ -0,0 +1,52 @@
|
|||
# coding=utf-8
|
||||
|
||||
from app.cores.logger import DispatcherLogger
|
||||
from app.cores.variable import Variable
|
||||
|
||||
import sys
|
||||
from flask import session
|
||||
|
||||
|
||||
class Script:
|
||||
|
||||
def __init__(self, tool, project_id=None, dispatcher_logger=None):
|
||||
"""
|
||||
初始化脚本工具对象
|
||||
:param tool: 工具
|
||||
:type tool: Tool
|
||||
:param project_id: 工具所在项目id
|
||||
:type project_id: int
|
||||
:param dispatcher_logger: 日志
|
||||
:type dispatcher_logger: DispatcherLogger
|
||||
"""
|
||||
self.script_tool = tool.specific_tool
|
||||
if project_id is None:
|
||||
self.project_id = session.get('project_id')
|
||||
else:
|
||||
self.project_id = project_id
|
||||
if dispatcher_logger is None:
|
||||
self.dispatcher_logger = DispatcherLogger(use_memory_string_handler=True, use_queue_handler=False)
|
||||
else:
|
||||
self.dispatcher_logger = dispatcher_logger
|
||||
|
||||
def exec_tool(self):
|
||||
error_msg = ''
|
||||
self.dispatcher_logger.logger.info('[脚本][执行]')
|
||||
if self.project_id is None:
|
||||
self.dispatcher_logger.logger.warn('[脚本]获取项目编号project_id为None, 不再执行该脚本')
|
||||
else:
|
||||
vars = Variable.get_project_variable(project_id=self.project_id)
|
||||
glb = {}
|
||||
loc = {}
|
||||
glb['vars'] = vars
|
||||
glb['log'] = self.dispatcher_logger.logger
|
||||
try:
|
||||
exec(self.script_tool.script_, glb, loc)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[-1]
|
||||
while tb.tb_next:
|
||||
tb = tb.tb_next
|
||||
error_msg = '\n脚本执行异常, 行号: %s, 错误信息: %s' % (tb.tb_lineno, e.args[0])
|
||||
log_text = self.dispatcher_logger.get_string_buffer()
|
||||
log_text += error_msg
|
||||
return log_text
|
|
@ -0,0 +1,21 @@
|
|||
# coding=utf-8
|
||||
|
||||
from time import sleep
|
||||
|
||||
|
||||
class Timer:
|
||||
|
||||
def __init__(self, tool, dispatcher_logger):
|
||||
"""
|
||||
初始化定时器对象
|
||||
:param tool: 定时器数据库对象
|
||||
:type tool: Tool
|
||||
:param dispatcher_logger: 调度日志
|
||||
:type dispatcher_logger: DispatcherLogger
|
||||
"""
|
||||
self.timer_tool = tool.specific_tool
|
||||
self.dispatcher_logger = dispatcher_logger
|
||||
|
||||
def exec_tool(self):
|
||||
self.dispatcher_logger.logger.info('[定时器][暂停%sms]' % self.timer_tool.delay)
|
||||
sleep(int(self.timer_tool.delay) / 1000)
|
|
@ -0,0 +1,30 @@
|
|||
# coding=utf-8
|
||||
from app.cores.variable import Variable
|
||||
|
||||
from flask import session
|
||||
|
||||
|
||||
class VariableDefinition:
|
||||
|
||||
def __init__(self, tool, dispatcher_logger, project_id=None):
|
||||
"""
|
||||
初始变量定义工具对象
|
||||
:param tool: 工具
|
||||
:type tool: Tool
|
||||
:param dispatcher_logger: 日志
|
||||
:type dispatcher_logger: DispatcherLogger
|
||||
:param project_id: 工具所在项目id
|
||||
:type project_id: int
|
||||
"""
|
||||
self.variable_definition_tool = tool.specific_tool
|
||||
self.dispatcher_logger = dispatcher_logger
|
||||
if project_id is None:
|
||||
self.project_id = session.get('project_id')
|
||||
else:
|
||||
self.project_id = project_id
|
||||
|
||||
def exec_tool(self):
|
||||
self.dispatcher_logger.logger.info('[脚本][执行]')
|
||||
variables = Variable.get_project_variable(project_id=self.project_id)
|
||||
for row in self.variable_definition_tool.variable_definition_list:
|
||||
variables[row.name_] = row.value_
|
|
@ -0,0 +1,77 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from app.extensions import session_id_manager
|
||||
|
||||
|
||||
class Variable:
|
||||
|
||||
"""
|
||||
VariablePool = {
|
||||
session_id: {
|
||||
project_id: {
|
||||
key: value,
|
||||
key1: value1,
|
||||
...
|
||||
},
|
||||
project_id2: {
|
||||
...
|
||||
}
|
||||
},
|
||||
session_id2:{
|
||||
...
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
VariablePool = {}
|
||||
|
||||
@classmethod
|
||||
def get_project_variable(cls, project_id):
|
||||
"""
|
||||
获取项目中所有定义的变量
|
||||
:param id: 项目id
|
||||
:type id: int
|
||||
:return: 项目级所有变量
|
||||
:rtype: Dict
|
||||
"""
|
||||
session_id = session_id_manager.get_session_id()
|
||||
cls._set_default_project_variable(project_id=project_id)
|
||||
return cls.VariablePool.get(session_id).get(project_id)
|
||||
|
||||
@classmethod
|
||||
def set_project_variable(cls, project_id, key, value):
|
||||
"""
|
||||
设置项目级别变量
|
||||
:param project_id: 项目id
|
||||
:type project_id: int
|
||||
:param key: 变量名
|
||||
:type key: str
|
||||
:param value: 变量值
|
||||
:type value: str
|
||||
"""
|
||||
session_id = session_id_manager.get_session_id()
|
||||
cls._set_default_project_variable(project_id=project_id)
|
||||
cls.VariablePool.get(session_id).get(project_id).update({
|
||||
key: value
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _set_default_project_variable(cls, project_id):
|
||||
"""当VariablePool为空时,为其设置默认值,避免取出值为None的项目级变量池"""
|
||||
session_id = session_id_manager.get_session_id()
|
||||
session_var = cls.VariablePool.get(session_id)
|
||||
if session_var is None:
|
||||
cls.VariablePool.update({
|
||||
session_id: {
|
||||
project_id: {}
|
||||
}
|
||||
})
|
||||
return
|
||||
project_var = cls.VariablePool.get(session_id).get(project_id)
|
||||
if project_var is None:
|
||||
cls.VariablePool.get(session_id).update({
|
||||
project_id: {}
|
||||
})
|
||||
return
|
|
@ -0,0 +1,97 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from app.extensions import socketio
|
||||
from app.models import User
|
||||
|
||||
|
||||
class SocketIOEvent:
|
||||
DISPATCHER_RESULT = 'DISPATCHER_RESULT' # 测试执行结果
|
||||
DISPATCHER_BEGIN = 'DISPATCHER_BEGIN' # 调度执行开始
|
||||
DISPATCHER_END = 'DISPATCHER_END' # 调度执行结束
|
||||
|
||||
|
||||
def emit_dispatcher_result(id, type, result):
|
||||
"""
|
||||
测试执行结果
|
||||
:param id: 组件id
|
||||
:param type: 组件类型
|
||||
:param result: 结果
|
||||
:return:
|
||||
"""
|
||||
socketio.emit(
|
||||
SocketIOEvent.DISPATCHER_RESULT,
|
||||
dict(
|
||||
id=str(id),
|
||||
type=str(type),
|
||||
result=str(result),
|
||||
),
|
||||
namespace='/user/' + str(current_user.id),
|
||||
)
|
||||
|
||||
|
||||
def emit_dispatcher_start(id, type):
|
||||
"""
|
||||
调度执行开始
|
||||
:param id:调度id
|
||||
:param type:调度类型 project/module
|
||||
"""
|
||||
socketio.emit(
|
||||
SocketIOEvent.DISPATCHER_BEGIN,
|
||||
dict(
|
||||
id=str(id),
|
||||
type=str(type).lower(),
|
||||
),
|
||||
namespace='/user/' + str(current_user.id),
|
||||
)
|
||||
|
||||
|
||||
def emit_dispatcher_end(id, type, end_type):
|
||||
"""
|
||||
调度执行结束
|
||||
:param id:调度id
|
||||
:param type:调度类型 project/module
|
||||
:param end_type: 结束类型 DISPATCHER_END_TYPE
|
||||
"""
|
||||
socketio.emit(
|
||||
SocketIOEvent.DISPATCHER_END,
|
||||
dict(
|
||||
id=str(id),
|
||||
type=str(type).lower(),
|
||||
end_type=end_type,
|
||||
),
|
||||
namespace='/user/' + str(current_user.id),
|
||||
)
|
||||
|
||||
# def test_message(message):
|
||||
# print('收到:%s' % message)
|
||||
# socketio.emit(
|
||||
# 'new response',
|
||||
# '服务端的消息推送给用户: %s' % current_user.id,
|
||||
# namespace='/user/' + str(current_user.id)
|
||||
# )
|
||||
#
|
||||
#
|
||||
# def register_user_socket(user_id):
|
||||
# socketio.on(
|
||||
# message='new message',
|
||||
# namespace='/user/' + str(user_id),
|
||||
# )(test_message)
|
||||
|
||||
|
||||
def register_all_user_socket():
|
||||
users = User.query.all()
|
||||
user_id_list = [user.id for user in users]
|
||||
for user_id in user_id_list:
|
||||
# register_user_socket(user_id=user_id) # 目前没有注册服务端socket事件监听的需求
|
||||
pass
|
||||
|
||||
# @socketio.on('connect')
|
||||
# def test_connect():
|
||||
# print('当前用户%s已连接' % current_user.id)
|
||||
#
|
||||
#
|
||||
# @socketio.on('disconnect')
|
||||
# def test_disconnect():
|
||||
# print('用户%s断开连接' % current_user.id)
|
|
@ -0,0 +1,18 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask_mail import Message, current_app
|
||||
from threading import Thread
|
||||
|
||||
from app.extensions import mail
|
||||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def send_email(subject, sender, recipients, text_body=None, html_body=None):
|
||||
msg = Message(subject=subject, sender=sender, recipients=recipients, body=text_body, html=html_body)
|
||||
app = current_app._get_current_object() # 获取被代理的真实对象
|
||||
t = Thread(target=send_async_email, args=(app, msg))
|
||||
t.start()
|
|
@ -0,0 +1,37 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from app.cores.case.http.http_cookie_manager import HTTPCookieManager
|
||||
from app.cores.session_id_manager import SessionIDManager
|
||||
from app.cores.dispatcher_scheduler import DispatcherScheduler
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
# render_as_batch=True在修改sqlite表结构后进行flask db migrate/upgrade升级时自动进行copy-and-move
|
||||
migrate = Migrate(render_as_batch=True)
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = "auth.login" # 注册登录视图
|
||||
login_manager.login_message_category = 'warning'
|
||||
login_manager.login_message = "请先登录"
|
||||
mail = Mail()
|
||||
csrf = CSRFProtect()
|
||||
bootstrap = Bootstrap()
|
||||
socketio = SocketIO()
|
||||
http_cookie_manager = HTTPCookieManager()
|
||||
session_id_manager = SessionIDManager()
|
||||
dispatcher_scheduler = DispatcherScheduler()
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
from app.models import User
|
||||
return User.query.filter_by(id=int(id)).first()
|
||||
|
||||
# 注册websocket事件
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("ajax", __name__)
|
||||
|
||||
from app.routes.ajax import routes
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("auth", __name__)
|
||||
|
||||
from app.routes.auth import routes
|
|
@ -0,0 +1,28 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import current_app, render_template
|
||||
|
||||
from app.models import User
|
||||
from app.email import send_email
|
||||
|
||||
|
||||
def send_email_authentication(email: str):
|
||||
token = User.get_email_token(email=email)
|
||||
send_email(
|
||||
subject="[WebApiAutoTest] 邮箱注册认证",
|
||||
sender=current_app.config['ADMINS'][0],
|
||||
recipients=[email],
|
||||
text_body=render_template("email/email_authentication.txt", token=token),
|
||||
html_body=render_template("email/email_authentication.html", token=token)
|
||||
)
|
||||
|
||||
|
||||
def send_email_reset_password(email: str):
|
||||
token = User.get_email_token(email=email)
|
||||
send_email(
|
||||
subject="[WebApiAutoTest] 密码重置",
|
||||
sender=current_app.config['ADMINS'][0],
|
||||
recipients=[email],
|
||||
text_body=render_template("email/reset_password.txt", token=token),
|
||||
html_body=render_template("email/reset_password.html", token=token)
|
||||
)
|
|
@ -0,0 +1,50 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, SelectMultipleField
|
||||
from wtforms.validators import DataRequired, Email, EqualTo, Length
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(label="用户名", validators=[DataRequired(message="用户名不能为空"), Length(5, message='长度过短')])
|
||||
password = PasswordField(label="密码", validators=[DataRequired(message="密码不能为空")])
|
||||
submit = SubmitField(label="登录")
|
||||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
username = StringField(label='用户名', validators=[DataRequired(message="用户名不能为空")])
|
||||
email = StringField(label='邮箱',
|
||||
validators=[DataRequired(message="邮箱不能为空"), Email()],
|
||||
render_kw={})
|
||||
password = PasswordField(label='密码', validators=[DataRequired(message="密码不能为空")])
|
||||
password2 = PasswordField(label='重复输入密码',
|
||||
validators=[DataRequired(message="密码不能为空"),
|
||||
EqualTo(fieldname='password', message="两次密码输入不一致")])
|
||||
submit = SubmitField(label='注册')
|
||||
|
||||
|
||||
class EmailAuthenticationForm(FlaskForm):
|
||||
email = StringField(label="请输入有效邮箱",
|
||||
validators=[DataRequired(message="邮箱不能为空"), Email(message="请输入正确格式邮箱地址")])
|
||||
submit = SubmitField(label="点击认证")
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
email = StringField(label='请输入注册邮箱, 将向此邮箱发送密码重置邮件',
|
||||
validators=[DataRequired(message='邮箱不能为空')])
|
||||
submit = SubmitField(label='确定')
|
||||
|
||||
|
||||
class ResetForm(FlaskForm):
|
||||
email = StringField(label='邮箱',
|
||||
validators=[DataRequired(message='邮箱不能为空')],
|
||||
render_kw={})
|
||||
usernames = SelectMultipleField(label='可选一个或多个用户重置',
|
||||
validators=[DataRequired('用户名不能为空')],
|
||||
choices=[])
|
||||
password = PasswordField(label='请输入新的密码',
|
||||
validators=[DataRequired(message='密码不能为空')])
|
||||
password2 = PasswordField(label='重复输入密码',
|
||||
validators=[DataRequired(message='密码不能为空'),
|
||||
EqualTo(fieldname='password', message="两次密码输入不一致")])
|
||||
submit = SubmitField(label='确定')
|
|
@ -0,0 +1,151 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, flash, redirect, url_for, session, Markup
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
|
||||
from app.routes.auth import bp
|
||||
from app.routes.auth.forms import LoginForm, RegistrationForm, ResetForm, ResetPasswordForm, EmailAuthenticationForm
|
||||
from app.models import User
|
||||
from app.routes.auth.email import send_email_reset_password, send_email_authentication
|
||||
from app.utils.util import redirect_back
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
username = form.username.data
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.check_password(passowrd=form.password.data):
|
||||
login_user(user=user, remember=True)
|
||||
flash("{} 登录成功".format(user.username))
|
||||
return redirect_back()
|
||||
else:
|
||||
flash("用户不存在或密码错误", category="warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/login.html", title="登录", form=form)
|
||||
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
if current_user.is_anonymous:
|
||||
flash('当前没有用户登录', category="error")
|
||||
logout_user()
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@bp.route("/email_authentication", methods=['GET', 'POST'])
|
||||
def email_authentication():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
form = EmailAuthenticationForm()
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
send_email_authentication(email=email)
|
||||
session['email_authentication'] = email
|
||||
message = Markup('已向邮箱 {} 发送认证邮件,请前往邮箱认证 '
|
||||
'<br>'
|
||||
'未收到?<a class="alert-link" href="{}">重新发送邮件</a>'.format(
|
||||
email,
|
||||
url_for('auth.resend_authentication', email=email),)
|
||||
)
|
||||
# flash("已向邮箱 {} 发送认证邮件,请前往邮箱认证".format(email))
|
||||
flash(message)
|
||||
return redirect(url_for("auth.email_authentication"))
|
||||
return render_template("auth/email_authentication.html", title="邮箱认证", form=form)
|
||||
|
||||
|
||||
@bp.route("/register/<token>", methods=["GET", "POST"])
|
||||
def register(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
form = RegistrationForm()
|
||||
email_token = User.get_email_from_token(token=token)
|
||||
email_session = session.get('email_authentication', None)
|
||||
if email_token != email_session:
|
||||
flash("跳转链接中的邮箱地址{}与会话中记录的请求认证邮箱地址{}不一致,可能原因如下:\n"
|
||||
"1). 认证邮件已超时失效\n"
|
||||
"2). 发起认证的浏览器与当前使用的必须为同一浏览器".format(email_token, email_session), category="error")
|
||||
return redirect(url_for("auth.email_authentication"))
|
||||
form.email.data = email_token
|
||||
form.email.render_kw.update(dict(disabled="disabled"))
|
||||
if form.validate_on_submit():
|
||||
if User.check_existence_buy_username_email(username=form.username.data,
|
||||
email=form.email.data):
|
||||
flash('用户{}和邮箱{}之前已经注册,可直接登录'.format(form.username.data, form.email.data))
|
||||
return redirect(url_for('auth.login'))
|
||||
User.add(username=form.username.data,
|
||||
email=form.email.data,
|
||||
password=form.password.data)
|
||||
flash('{} 用户注册成功'.format(form.username.data), category='success')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template("auth/register.html", title="用户注册", form=form, token=token)
|
||||
|
||||
|
||||
@bp.route('/resend_authentication/<email>', methods=['GET'])
|
||||
def resend_authentication(email):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
send_email_authentication(email=email)
|
||||
flash('已重新向邮箱 {} 发送认证邮件,请前往邮箱认证'.format(email))
|
||||
return redirect(url_for('auth.email_authentication'))
|
||||
|
||||
|
||||
@bp.route('/reset_password', methods=['GET', 'POST'])
|
||||
def reset_password():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
form = ResetPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
session['email_reset_password'] = email
|
||||
send_email_reset_password(email=email)
|
||||
# flash("已向邮箱 {} 发送密码重置邮件,请前往邮箱确认".format(email))
|
||||
message = Markup('已向邮箱 {} 发送认证邮件,请前往邮箱认证 '
|
||||
'<br>'
|
||||
'未收到?<a class="alert-link" href="{}">重新发送邮件</a>'.format(
|
||||
email,
|
||||
url_for('auth.resend_reset_password', email=email), )
|
||||
)
|
||||
flash(message)
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('auth/reset_password.html', title='密码重置', form=form)
|
||||
|
||||
|
||||
@bp.route('/resend_reset_password/<email>', methods=['GET'])
|
||||
def resend_reset_password(email):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
send_email_reset_password(email=email)
|
||||
flash('已重新向邮箱 {} 发送重置邮件,请前往邮箱认证'.format(email))
|
||||
return redirect(url_for('auth.reset_password'))
|
||||
|
||||
|
||||
@bp.route('/reset/<token>', methods=['GET', 'POST'])
|
||||
def reset(token):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('main.index'))
|
||||
form = ResetForm()
|
||||
email_token = User.get_email_from_token(token=token)
|
||||
email_session = session.get('email_reset_password', None)
|
||||
if email_token != email_session:
|
||||
flash("跳转链接中的邮箱地址{}与会话中记录的请求重置邮箱地址{}不一致,可能原因如下:\n"
|
||||
"1). 重置邮件已超时失效\n"
|
||||
"2). 发起密码重置的浏览器与当前使用的必须为同一浏览器".format(email_token, email_session), category="error")
|
||||
return redirect(url_for("auth.reset_password_request"))
|
||||
form.email.data = email_token
|
||||
form.email.render_kw.update(dict(disabled='disabled'))
|
||||
users = User.query.filter_by(email=email_token).all()
|
||||
choices = [(user.username, user.username) for user in users]
|
||||
form.usernames.choices = choices
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
usernames = form.usernames.data
|
||||
password = form.password.data
|
||||
for username in usernames:
|
||||
User.update_password(username=username, email=email, password=password)
|
||||
flash('密码重置成功', category='success')
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('/auth/reset.html', title='密码重置', form=form, token=token)
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("case", __name__)
|
||||
|
||||
from app.routes.case import routes
|
|
@ -0,0 +1,30 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, abort
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.case import bp
|
||||
from app.models import Case
|
||||
from app.cores.dictionaries import STATUS, CASE_TYPE
|
||||
|
||||
|
||||
@bp.route('/<string:id>') # TODO string 改为 int
|
||||
@login_required
|
||||
def case(id):
|
||||
if int(id) > 0:
|
||||
# 从数据库匹配案例
|
||||
case = Case.query.filter_by(id=id).first()
|
||||
if (case is None) or (case.status != STATUS.NORMAL):
|
||||
abort(404)
|
||||
if case.case_type == CASE_TYPE.HTTP:
|
||||
return render_template('case/http/http.html', case=case)
|
||||
elif case.case_type == CASE_TYPE.SSH:
|
||||
return render_template('case/ssh.html', case=case)
|
||||
elif case.case_type == CASE_TYPE.SQL:
|
||||
return render_template('case/sql.html', case=case)
|
||||
elif case.case_type == CASE_TYPE.DEBUG:
|
||||
return render_template('case/debug.html', case=case)
|
||||
else:
|
||||
abort(404)
|
||||
else:
|
||||
abort(404)
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("error", __name__)
|
||||
|
||||
from app.routes.error import routes
|
|
@ -0,0 +1,32 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template
|
||||
from flask_wtf.csrf import CSRFError
|
||||
|
||||
from app.extensions import db
|
||||
from app.routes.error import bp
|
||||
from app.utils.util import save_request_session_info
|
||||
|
||||
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found_error(error):
|
||||
return render_template('/error/404.html'), 404
|
||||
|
||||
|
||||
@bp.app_errorhandler(500)
|
||||
def internal_error(error):
|
||||
db.session.rollback()
|
||||
save_request_session_info()
|
||||
return render_template('/error/500.html'), 500
|
||||
|
||||
|
||||
@bp.app_errorhandler(CSRFError)
|
||||
def csrf_error(error):
|
||||
save_request_session_info()
|
||||
return render_template('/error/error.html', msg='csrf_token异常: %s' % error.description), 400
|
||||
|
||||
|
||||
@bp.app_errorhandler(400)
|
||||
def bad_request_error(error):
|
||||
save_request_session_info()
|
||||
return render_template('/error/error.html', msg='错误请求: %s' % error.description), 400
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("logic_controller", __name__)
|
||||
|
||||
from app.routes.logic_controller import routes
|
|
@ -0,0 +1,32 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, abort
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.logic_controller import bp
|
||||
from app.models import LogicController
|
||||
from app.cores.dictionaries import LOGIC_CONTROLLER_TYPE
|
||||
|
||||
|
||||
@bp.route('/<int:logic_controller_id>')
|
||||
@login_required
|
||||
def logic_controller(logic_controller_id):
|
||||
if logic_controller_id is None:
|
||||
abort(404)
|
||||
logic_controller = LogicController.query.filter_by(id=logic_controller_id).first()
|
||||
if logic_controller is None:
|
||||
abort(404)
|
||||
if logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.IF_CONTROLLER:
|
||||
if_controller = logic_controller.specific_controller
|
||||
return render_template('logic_controller/if.html', if_controller=if_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.LOOP_CONTROLLER:
|
||||
loop_controller = logic_controller.specific_controller
|
||||
return render_template('logic_controller/loop.html', loop_controller=loop_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.WHILE_CONTROLLER:
|
||||
while_controller = logic_controller.specific_controller
|
||||
return render_template('logic_controller/while.html', while_controller=while_controller)
|
||||
elif logic_controller.logic_controller_type == LOGIC_CONTROLLER_TYPE.SIMPLE_CONTROLLER:
|
||||
simple_controller = logic_controller.specific_controller
|
||||
return render_template('logic_controller/simple.html', simple_controller=simple_controller)
|
||||
else:
|
||||
abort(404)
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
from app.routes.main import routes
|
|
@ -0,0 +1,84 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.routes.main import bp
|
||||
from app.models import Dispatcher, Scheduler, Case
|
||||
from app.cores.dictionaries import REPORT_RESULT
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route('/index')
|
||||
@login_required
|
||||
def index():
|
||||
# 获取历史构建次数
|
||||
total_dispatcher_count = len(Dispatcher.query.all())
|
||||
# 获取当前用户构建次数
|
||||
total_dispatcher_count_current_user = len(Dispatcher.query.filter_by(user_id=current_user.id).all())
|
||||
# 获取最近2周构建结果数据/最近2周当前用户构建结果数据
|
||||
current_time = datetime.now()
|
||||
current_date = datetime(year=current_time.year, month=current_time.month, day=current_time.day, hour=23, minute=59,
|
||||
second=59)
|
||||
current_date_14_days_ago = current_date - timedelta(days=14)
|
||||
dispatchers = Dispatcher.query.filter(Dispatcher.start_time > current_date_14_days_ago,
|
||||
Dispatcher.start_time < current_date).all()
|
||||
echart_line_last_dispatch_dataset_source = []
|
||||
echart_line_last_dispatch_current_user_dataset_source = []
|
||||
for i in range(14):
|
||||
echart_line_last_dispatch_dataset_source.append({
|
||||
'date': (current_date_14_days_ago + timedelta(days=i+1)).strftime("%Y%m%d"),
|
||||
'构建': 0, '成功': 0, '失败': 0, '错误': 0, '中止': 0,
|
||||
})
|
||||
echart_line_last_dispatch_current_user_dataset_source.append({
|
||||
'date': (current_date_14_days_ago + timedelta(days=i+1)).strftime("%Y%m%d"),
|
||||
'构建': 0, '成功': 0, '失败': 0, '错误': 0, '中止': 0,
|
||||
})
|
||||
for dispatcher in dispatchers:
|
||||
date = dispatcher.start_time.strftime("%Y%m%d")
|
||||
for source in echart_line_last_dispatch_dataset_source:
|
||||
if date == source['date']:
|
||||
source['构建'] += 1
|
||||
if dispatcher.report.result == REPORT_RESULT.SUCCESS:
|
||||
source['成功'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.ERROR:
|
||||
source['错误'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.FAILURE:
|
||||
source['失败'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.ABORT:
|
||||
source['中止'] += 1
|
||||
for source in echart_line_last_dispatch_current_user_dataset_source:
|
||||
if dispatcher.user_id == current_user.id and date == source['date']:
|
||||
source['构建'] += 1
|
||||
if dispatcher.report.result == REPORT_RESULT.SUCCESS:
|
||||
source['成功'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.ERROR:
|
||||
source['错误'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.FAILURE:
|
||||
source['失败'] += 1
|
||||
elif dispatcher.report.result == REPORT_RESULT.ABORT:
|
||||
source['中止'] += 1
|
||||
# echart_line_last_dispatch_dataset_source = [
|
||||
# {'date': '20201231', '构建': 20, '成功': 13, '失败': 5, '错误': 2, '中止': 0},
|
||||
# {'date': '20201230', '构建': 10, '成功': 3, '失败': 5, '错误': 2, '中止': 0},
|
||||
# {'date': '20201229', '构建': 15, '成功': 8, '失败': 2, '错误': 5, '中止': 0},
|
||||
# {'date': '20201228', '构建': 25, '成功': 13, '失败': 10, '错误': 2, '中止': 0},
|
||||
# ]
|
||||
# 获取正在运行的定时任务数
|
||||
scheduler_enable_count = len(Scheduler.query.filter_by(enable=True).all())
|
||||
# 获取总案例个数
|
||||
total_case_count = len(Case.query.all())
|
||||
return render_template('main/index.html',
|
||||
scheduler_enable_count=scheduler_enable_count,
|
||||
total_dispatcher_count=total_dispatcher_count,
|
||||
total_dispatcher_count_current_user=total_dispatcher_count_current_user,
|
||||
total_case_count=total_case_count,
|
||||
echart_line_last_dispatch_dataset_source=echart_line_last_dispatch_dataset_source,
|
||||
echart_line_last_dispatch_current_user_dataset_source=echart_line_last_dispatch_current_user_dataset_source)
|
||||
|
||||
|
||||
@bp.route('/blank')
|
||||
@login_required
|
||||
def blank():
|
||||
return render_template('main/blank.html')
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("module", __name__)
|
||||
|
||||
from app.routes.module import routes
|
|
@ -0,0 +1,19 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.module import bp
|
||||
from app.models import Project
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def module():
|
||||
project_id = request.args.get('project_id')
|
||||
if project_id is None:
|
||||
abort(404)
|
||||
project = Project.query.filter_by(id=project_id).first()
|
||||
if project is None:
|
||||
abort(404)
|
||||
return render_template('module/module.html', project=project)
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("project", __name__)
|
||||
|
||||
from app.routes.project import routes
|
|
@ -0,0 +1,12 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.project import bp
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def project():
|
||||
return render_template('project/project.html')
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("report", __name__)
|
||||
|
||||
from app.routes.report import routes
|
|
@ -0,0 +1,49 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, abort
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.report import bp
|
||||
from app.cores.dictionaries import ELEMENT_TYPE, REPORT_RESULT
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def report():
|
||||
return render_template('report/report.html')
|
||||
|
||||
|
||||
@bp.route('/detail/<int:report_id>')
|
||||
@login_required
|
||||
def report_detail(report_id):
|
||||
from app.models import Report
|
||||
report = Report.query.filter_by(id=report_id).first()
|
||||
if report is None:
|
||||
abort(404)
|
||||
dispatcher = report.dispatcher
|
||||
report_case_data = report.report_case_data
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
error_count = 0
|
||||
skip_count = 0
|
||||
case_count = 0
|
||||
for data in report_case_data:
|
||||
case_count += 1
|
||||
if data.result == REPORT_RESULT.SUCCESS:
|
||||
success_count += 1
|
||||
elif data.result == REPORT_RESULT.FAILURE:
|
||||
failure_count += 1
|
||||
elif data.result == REPORT_RESULT.ERROR:
|
||||
error_count += 1
|
||||
elif data.result == REPORT_RESULT.SKIP:
|
||||
skip_count += 1
|
||||
echart_pie_result_data = [
|
||||
{'name': REPORT_RESULT.SUCCESS, 'value': success_count},
|
||||
{'name': REPORT_RESULT.FAILURE, 'value': failure_count},
|
||||
{'name': REPORT_RESULT.ERROR, 'value': error_count},
|
||||
{'name': REPORT_RESULT.SKIP, 'value': skip_count},
|
||||
]
|
||||
return render_template('report/report_detail.html', dispatcher=dispatcher, report=report, ELEMENT_TYPE=ELEMENT_TYPE,
|
||||
echart_pie_result_data=echart_pie_result_data, case_count=case_count,
|
||||
failure_count=failure_count, error_count=error_count, success_count=success_count,
|
||||
skip_count=skip_count)
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("scene", __name__)
|
||||
|
||||
from app.routes.scene import routes
|
|
@ -0,0 +1,32 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.scene import bp
|
||||
from app.models import Module, Scene, Project
|
||||
from app.cores.dictionaries import STATUS, ELEMENT_TYPE, LOGIC_CONTROLLER_TYPE
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def scene():
|
||||
module_id = request.args.get('module_id')
|
||||
if module_id is None:
|
||||
abort(404)
|
||||
module = Module.query.filter_by(id=module_id).first()
|
||||
if module is None:
|
||||
abort(404)
|
||||
projects = Project.query.filter_by(status=STATUS.NORMAL).all()
|
||||
return render_template('scene/scene.html', scenes=module.scenes, module=module, project=module.project,
|
||||
projects=projects, ELEMENT_TYPE=ELEMENT_TYPE, LOGIC_CONTROLLER_TYPE=LOGIC_CONTROLLER_TYPE)
|
||||
|
||||
|
||||
@bp.route('/setting/<int:id>')
|
||||
@login_required
|
||||
def scene_setting(id):
|
||||
scene = Scene.query.filter_by(id=id).first()
|
||||
if scene is None:
|
||||
abort(404)
|
||||
return render_template('scene/scene_setting.html', scene=scene)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
bp = Blueprint("setting", __name__)
|
||||
|
||||
from app.routes.setting import routes
|
|
@ -0,0 +1,30 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, SelectMultipleField, SelectField
|
||||
from wtforms.validators import DataRequired, Email, EqualTo, Length
|
||||
from wtforms.validators import DataRequired, Regexp, ValidationError
|
||||
|
||||
|
||||
def required_when_send_mail(form, field):
|
||||
if (form.whether_send_email.data == 'true') and (not field.data):
|
||||
raise ValidationError('当发送邮件时,此项不能为空')
|
||||
|
||||
|
||||
class EmailSettingForm(FlaskForm):
|
||||
whether_send_email = SelectField(label='是否发送邮件',
|
||||
choices=[('false', '否'), ('true', '是')],
|
||||
render_kw={})
|
||||
whether_gen_report = SelectField(label='是否发送生成报告',
|
||||
choices=[('false', '否'), ('true', '是')],
|
||||
render_kw={})
|
||||
email_title = StringField(label='邮件主题',
|
||||
validators=[required_when_send_mail],
|
||||
render_kw={})
|
||||
email_text = StringField(label='正文内容',
|
||||
validators=[required_when_send_mail],
|
||||
render_kw={})
|
||||
receiver_address = StringField(label='收件地址',
|
||||
validators=[required_when_send_mail],
|
||||
render_kw={})
|
||||
submit = SubmitField('提交')
|
|
@ -0,0 +1,55 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import render_template, redirect, url_for, flash, current_app
|
||||
from flask_login import login_required
|
||||
|
||||
from app.routes.setting import bp
|
||||
from app.routes.setting.forms import EmailSettingForm
|
||||
from app.models import EmailSetting, Project
|
||||
from app.utils.util import exception_handle
|
||||
from app.cores.dictionaries import STATUS
|
||||
|
||||
|
||||
@bp.route('/basic_setting', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def basic_setting():
|
||||
form = EmailSettingForm()
|
||||
email_setting = EmailSetting.get_current_email_setting()
|
||||
projects = Project.query.filter_by(status=STATUS.NORMAL).all()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
if email_setting:
|
||||
email_setting.update(
|
||||
whether_send_email=True if form.whether_send_email.data.lower() == 'true' else False,
|
||||
whether_gen_report=True if form.whether_gen_report.data.lower() == 'true' else False,
|
||||
email_title=form.email_title.data,
|
||||
email_text=form.email_text.data,
|
||||
receiver_address=form.receiver_address.data,
|
||||
)
|
||||
else:
|
||||
EmailSetting.add(
|
||||
whether_send_email=True if form.whether_send_email.data.lower() == 'true' else False,
|
||||
whether_gen_report=True if form.whether_gen_report.data.lower() == 'true' else False,
|
||||
email_title=form.email_title.data,
|
||||
email_text=form.email_text.data,
|
||||
receiver_address=form.receiver_address.data,
|
||||
)
|
||||
except Exception as e:
|
||||
exception_handle(current_app, e)
|
||||
flash('更新失败,请查看日志', category='error')
|
||||
else:
|
||||
flash('更新成功')
|
||||
return redirect(url_for('setting.basic_setting')) # TODO util增加一个方法可以自动刷新当前页面
|
||||
if email_setting:
|
||||
form.whether_send_email.data = str(email_setting.whether_send_email).lower()
|
||||
form.whether_gen_report.data = str(email_setting.whether_gen_report).lower()
|
||||
form.email_title.data = email_setting.email_title
|
||||
form.email_text.data = email_setting.email_text
|
||||
form.receiver_address.data = email_setting.receiver_address
|
||||
else:
|
||||
form.whether_send_email.data = None
|
||||
form.whether_gen_report.data = None
|
||||
form.email_title.data = None
|
||||
form.email_text.data = None
|
||||
form.receiver_address.data = None
|
||||
return render_template('setting/basic_setting.html', form=form, projects=projects)
|
|
@ -0,0 +1,52 @@
|
|||
.bg-auth-login-image {
|
||||
/*background: url(https://source.unsplash.com/K4mSJ7kc0As/600x800);*/
|
||||
background: url(/static/img/photo1.jpg);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bg-auth-email-authentication-image {
|
||||
background: url(/static/img/photo2.jpg);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bg-auth-rest-password-image{
|
||||
background: url(/static/img/photo2.jpg);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bg-auth-register-image{
|
||||
background: url(/static/img/photo3.jpg);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bg-auth-reset-image{
|
||||
background: url(/static/img/photo4.jpg);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
color: #3a3b45!important;
|
||||
}
|
||||
|
||||
form .form-control-auth {
|
||||
font-size: .8rem;
|
||||
border-radius: 10rem;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
form .form-control-auth-multiple-select {
|
||||
font-size: .8rem;
|
||||
border-radius: 2rem;
|
||||
/*padding: 1.5rem 1rem;*/
|
||||
}
|
||||
|
||||
form .btn-auth {
|
||||
font-size: .8rem;
|
||||
border-radius: 10rem;
|
||||
padding: .75rem 1rem;
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
html {
|
||||
font-size:0.9rem;
|
||||
/*background-color: #f3f3f3;*/
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
/*background-color: #f3f3f3;*/
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bg-gradient-grey {
|
||||
background-color: #f4f4f4;
|
||||
background-image: linear-gradient(180deg, #f4f4f4 10%, #bcc0c1 100%);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
/*background-color: white;*/
|
||||
}
|
||||
|
||||
.div-forbidden{ /* 禁用某个div */
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.cursor-pointer{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-refresh{
|
||||
background-image: url(/static/icon/bootstrap-icons/arrow-repeat.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.btn-refresh:hover{
|
||||
background-image: url(/static/icon/bootstrap-icons/arrow-repeat-blue.svg);
|
||||
}
|
||||
|
||||
.div-split-gutter-vertical{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-vertical.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
.div-split-gutter-vertical:hover{
|
||||
background-color: #409dfe;
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-vertical-white.svg);
|
||||
}
|
||||
|
||||
.div-split-gutter{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
.div-split-gutter:hover{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-white.svg);
|
||||
background-color: #409dfe;
|
||||
}
|
||||
|
||||
.fancytree-container {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.fancytree-treefocus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.icon-project{
|
||||
background-image: url(/static/icon/p.svg);
|
||||
}
|
||||
|
||||
.icon-module{
|
||||
background-image: url(/static/icon/m.svg);
|
||||
}
|
||||
|
||||
.icon-scene{
|
||||
background-image: url(/static/icon/s.svg);
|
||||
}
|
||||
|
||||
.icon-case{
|
||||
background-image: url(/static/icon/c.svg);
|
||||
}
|
||||
|
||||
.icon-case-http{
|
||||
background-image: url(/static/icon/http.svg);
|
||||
}
|
||||
.icon-case-http-success{
|
||||
background-image: url(/static/icon/http-success.svg);
|
||||
}
|
||||
.icon-case-http-failure{
|
||||
background-image: url(/static/icon/http-failure.svg);
|
||||
}
|
||||
.icon-case-http-error{
|
||||
background-image: url(/static/icon/http-error.svg);
|
||||
}
|
||||
.icon-case-http-skip{
|
||||
background-image: url(/static/icon/http-skip.svg);
|
||||
}
|
||||
|
||||
.icon-case-sql{
|
||||
background-image: url(/static/icon/sql.svg);
|
||||
}
|
||||
.icon-case-sql-success{
|
||||
background-image: url(/static/icon/sql-success.svg);
|
||||
}
|
||||
.icon-case-sql-failure{
|
||||
background-image: url(/static/icon/sql-failure.svg);
|
||||
}
|
||||
.icon-case-sql-error{
|
||||
background-image: url(/static/icon/sql-error.svg);
|
||||
}
|
||||
.icon-case-sql-skip{
|
||||
background-image: url(/static/icon/sql-skip.svg);
|
||||
}
|
||||
|
||||
.icon-case-debug{
|
||||
background-image: url(/static/icon/debug.svg);
|
||||
}
|
||||
.icon-case-debug-success{
|
||||
background-image: url(/static/icon/debug-success.svg);
|
||||
}
|
||||
.icon-case-debug-failure{
|
||||
background-image: url(/static/icon/debug-failure.svg);
|
||||
}
|
||||
.icon-case-debug-error{
|
||||
background-image: url(/static/icon/debug-error.svg);
|
||||
}
|
||||
.icon-case-debug-skip{
|
||||
background-image: url(/static/icon/debug-skip.svg);
|
||||
}
|
||||
|
||||
.icon-case-ssh{
|
||||
background-image: url(/static/icon/ssh.svg);
|
||||
}
|
||||
.icon-case-ssh-success{
|
||||
background-image: url(/static/icon/ssh-success.svg);
|
||||
}
|
||||
.icon-case-ssh-failure{
|
||||
background-image: url(/static/icon/ssh-failure.svg);
|
||||
}
|
||||
.icon-case-ssh-error{
|
||||
background-image: url(/static/icon/ssh-error.svg);
|
||||
}
|
||||
.icon-case-ssh-skip{
|
||||
background-image: url(/static/icon/ssh-skip.svg);
|
||||
}
|
||||
|
||||
.icon-if-controller{
|
||||
background-image: url(/static/icon/if-controller.svg);
|
||||
}
|
||||
.icon-while-controller{
|
||||
background-image: url(/static/icon/while-controller.svg);
|
||||
}
|
||||
.icon-loop-controller{
|
||||
background-image: url(/static/icon/loop-controller.svg);
|
||||
}
|
||||
.icon-simple-controller{
|
||||
background-image: url(/static/icon/simple-controller.svg);
|
||||
}
|
||||
|
||||
.icon-tool-timer{
|
||||
background-image: url(/static/icon/timer.svg);
|
||||
}
|
||||
.icon-tool-script{
|
||||
background-image: url(/static/icon/python.svg);
|
||||
}
|
||||
.icon-tool-variable-definition{
|
||||
background-image: url(/static/icon/variable.svg);
|
||||
}
|
||||
|
||||
/*div边框样式*/
|
||||
.border-left-primary {
|
||||
border-left: .25rem solid #4e73df!important;
|
||||
}
|
||||
.border-left-success {
|
||||
border-left: .25rem solid #28a645 !important;
|
||||
}
|
||||
.border-left-info {
|
||||
border-left: .25rem solid #53a7bb !important;
|
||||
}
|
||||
.border-left-warning {
|
||||
border-left: .25rem solid #fdc007 !important;
|
||||
}
|
||||
|
||||
/*badge自定义样式*/
|
||||
.badge-error{
|
||||
color:#fff;
|
||||
background-color:#cd00cc;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
.element-case .nav-pills .nav-link.failure {
|
||||
color: #fff;
|
||||
background-color: #b90000;
|
||||
}
|
||||
|
||||
.element-case .nav-pills .nav-link.success {
|
||||
color: #fff;
|
||||
background-color: #00b90d;
|
||||
}
|
||||
|
||||
.element-case .div-case-up{
|
||||
max-height: 55vh;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.element-case .div-case-bottom{
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.element-case .div-case-container{
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.element-case .ace-request-header{
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.element-case .ace-request-body{
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.element-case .ace-response-header{
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.element-case .ace-response-body{
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.element-case .ace-preprocessor-script{
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.element-case .ace-postprocessor-script{
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.element-case .ace-log{
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.element-case .postprocessor-result-card{
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.element-case .lastResult.false {
|
||||
/*background-color: red;*/
|
||||
background-image: url(/static/icon/bootstrap-icons/emoji-frown-failure.svg);
|
||||
}
|
||||
|
||||
.element-case .lastResult.true {
|
||||
/*background-color: green;*/
|
||||
background-image: url(/static/icon/bootstrap-icons/emoji-laughing-success.svg);
|
||||
}
|
||||
|
||||
.element-case .lastResult {
|
||||
/*background-size: 33px 22px;*/
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.element-case .ace-message-body{
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.element-case .div-case-up{
|
||||
max-height: 65vh;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.element-case .ace-sql{
|
||||
height: 30vh;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.element-case .ace-command{
|
||||
height: 30vh;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.card-body-module-list{
|
||||
height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fixed-table-pagination{
|
||||
position: absolute;
|
||||
bottom: 1vh;
|
||||
width: 95%;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.card-body-project-list{
|
||||
height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fixed-table-pagination{
|
||||
position: absolute;
|
||||
bottom: 1vh;
|
||||
width: 95%;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.card-body-report-list{
|
||||
height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fixed-table-pagination{
|
||||
position: absolute;
|
||||
bottom: 1vh;
|
||||
width: 95%;
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
.div-container-report-summary{
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.div-container-report-detail{
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.container-fluid.pre-scrollable {
|
||||
max-height: 100%;
|
||||
overflow: auto; /* 内容超过指定高度会出现滚动条 */
|
||||
}
|
||||
|
||||
.case-method{
|
||||
/*padding-top: 0px;*/
|
||||
/*padding-right: 5px;*/
|
||||
/*padding-bottom: 0px;*/
|
||||
/*padding-left: 5px;*/
|
||||
color: #1e90ff;
|
||||
}
|
||||
|
||||
.lastResult.false {
|
||||
/*background-color: red;*/
|
||||
background-image: url(/static/icon/bootstrap-icons/emoji-frown-failure.svg);
|
||||
}
|
||||
|
||||
.lastResult.true {
|
||||
/*background-color: green;*/
|
||||
background-image: url(/static/icon/bootstrap-icons/emoji-laughing-success.svg);
|
||||
}
|
||||
|
||||
.lastResult {
|
||||
/*background-size: 33px 22px;*/
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.failure {
|
||||
color: #fff;
|
||||
background-color: #b90000;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.success {
|
||||
color: #fff;
|
||||
background-color: #00b90d;
|
||||
}
|
||||
|
||||
.ace-report-log{
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.ace-request-header {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.ace-request-body{
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.ace-response-header {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.ace-response-body{
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.ace-preprocessor-script{
|
||||
height: 40vh;
|
||||
}
|
||||
.ace-postprocessor-script{
|
||||
height: 40vh;
|
||||
}
|
||||
.ace-post-processor-script-info{
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.ace-case-log{
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.ace-tool-script{
|
||||
height: 55vh;
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
.div-container-scene{
|
||||
height: 86vh;
|
||||
}
|
||||
|
||||
.container-fluid.pre-scrollable {
|
||||
max-height: 83vh;
|
||||
overflow: auto; /* 内容超过指定高度会出现滚动条 */
|
||||
}
|
||||
|
||||
.container-fluid.element-copy-tree.pre-scrollable {
|
||||
height: 60vh;
|
||||
max-height: 100%;
|
||||
overflow: auto; /* 内容超过指定高度会出现滚动条 */
|
||||
}
|
||||
/*左侧组件导航*/
|
||||
.element-navigation{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*各个类型组件容器*/
|
||||
.div-container-element{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*.div-embed-responsive-iframe-case{*/
|
||||
/*width: 100%;*/
|
||||
/*height: 100%*/
|
||||
/*}*/
|
||||
|
||||
.element-navigation .nested-sortable{
|
||||
min-height: 4vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.element-navigation .scene-name{
|
||||
height: 5vh;
|
||||
line-height: 5vh; /*行文本高度与div高度相同,实现div文本垂直居中效果*/
|
||||
width: 100%;
|
||||
/*当文本超过宽度时使用省略号(white-space overflow text-overflow)*/
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.element-navigation .item-scene-header{
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
.element-navigation .item-scene-header.active{
|
||||
background-color: #c3c3c3;
|
||||
}
|
||||
|
||||
.element-navigation .item-case{
|
||||
border-left-width: 5px;
|
||||
border-left-color: #1e90ff;
|
||||
border-left-style: solid;
|
||||
}
|
||||
.element-navigation .item-case:hover{
|
||||
background-color: #ebf4fe;
|
||||
}
|
||||
.element-navigation .item-case.active{
|
||||
background-color: #ebf4fe;
|
||||
color: #000;
|
||||
z-index: auto;
|
||||
}
|
||||
.element-navigation .item-case.marked{
|
||||
border-top-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-top-color: #ff4000;
|
||||
border-right-color: #ff4000;
|
||||
border-bottom-color: #ff4000;
|
||||
}
|
||||
.element-navigation .item-case-dispatcher-result-success{
|
||||
border-left-color: #00cd00;
|
||||
}
|
||||
.element-navigation .item-case-dispatcher-result-failure{
|
||||
border-left-color: #ff4000;
|
||||
}
|
||||
.element-navigation .item-case-dispatcher-result-error{
|
||||
border-left-color: #cd00cc;
|
||||
}
|
||||
.element-navigation .item-case-dispatcher-result-abort{
|
||||
border-left-color: #6c757d;
|
||||
}
|
||||
.element-navigation .case-type{
|
||||
/*height: 2vh;*/
|
||||
background-color: #409eff;
|
||||
}
|
||||
.element-navigation .case-method{
|
||||
/*padding-top: 0px;*/
|
||||
/*padding-right: 5px;*/
|
||||
/*padding-bottom: 0px;*/
|
||||
/*padding-left: 5px;*/
|
||||
color: #1e90ff;
|
||||
}
|
||||
.element-navigation .case-info{
|
||||
height: 5vh;
|
||||
width:100%;
|
||||
/*当超过宽度时隐藏*/
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
}
|
||||
.element-navigation .case-script{
|
||||
height: 1.5vh;
|
||||
}
|
||||
.element-navigation .case-script .badge-script{
|
||||
color: #fff;
|
||||
background-color: #7bbdfe;
|
||||
font-size: 30%;
|
||||
}
|
||||
.element-navigation .case-name{
|
||||
height: 2vh;
|
||||
line-height: 2vh;
|
||||
/*当文本超过宽度时使用省略号(white-space overflow text-overflow)*/
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
/*工具组件*/
|
||||
.element-navigation .item-tool{
|
||||
border-left-width: 5px;
|
||||
border-left-color: #88929b;
|
||||
border-left-style: solid;
|
||||
height: 3vh;
|
||||
line-height: 3vh;
|
||||
}
|
||||
.element-navigation .item-tool:hover{
|
||||
background-color: #e8ecf5;
|
||||
}
|
||||
.element-navigation .item-tool.active{
|
||||
background-color: #e8ecf5;
|
||||
border-color: #88929b;
|
||||
color: #000;
|
||||
z-index: auto;
|
||||
}
|
||||
.element-navigation .item-tool.marked{
|
||||
border-top-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-top-color: #ff4000;
|
||||
border-right-color: #ff4000;
|
||||
border-bottom-color: #ff4000;
|
||||
}
|
||||
.element-navigation .tool-type{
|
||||
background-color: #88929b;
|
||||
}
|
||||
.element-navigation .tool-name{
|
||||
width: 100%;
|
||||
/*当文本超过宽度时使用省略号(white-space overflow text-overflow)*/
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
/*逻辑控制器*/
|
||||
.element-navigation .logic-controller{
|
||||
border-left-width: 5px;
|
||||
border-left-color: #88929b;
|
||||
border-left-style: solid;
|
||||
}
|
||||
.element-navigation .logic-controller .logic-controller-content{
|
||||
/*当文本超过宽度时使用省略号(white-space overflow text-overflow)*/
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
width: 100%;
|
||||
height: 3vh;
|
||||
line-height: 3vh;
|
||||
}
|
||||
.element-navigation .logic-controller .specific-logic-controller{
|
||||
background-color: #dfdfe0;
|
||||
}
|
||||
.element-navigation .logic-controller .specific-logic-controller.active{
|
||||
background-color: #c3c3c3;
|
||||
}
|
||||
.element-navigation .logic-controller .specific-logic-controller.marked{
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #ff4000;
|
||||
}
|
||||
.element-navigation .logic-controller .logic-controller-type{
|
||||
background-color: #88929b;
|
||||
}
|
||||
|
||||
/* 组件设置按钮样式 “...” [START] */
|
||||
.element-navigation .btn-case{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-case:hover{
|
||||
background-color: #007bff;
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-white.svg);
|
||||
}
|
||||
|
||||
.element-navigation .btn-scene{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-scene:hover{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-white.svg);
|
||||
background-color: #9b9b9b;
|
||||
}
|
||||
|
||||
.element-navigation .btn-logic-controller{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-logic-controller:hover{
|
||||
background-color: #9b9b9b;
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-white.svg);
|
||||
}
|
||||
|
||||
.element-navigation .btn-tool{
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-tool:hover{
|
||||
background-color: #9b9b9b;
|
||||
background-image: url(/static/icon/bootstrap-icons/three-dots-white.svg);
|
||||
}
|
||||
/* 组件设置按钮样式 “...” [END] */
|
||||
|
||||
/* 案例场景添加按钮样式 “+” [START] */
|
||||
.element-navigation .btn-add-case{
|
||||
background-image: url(/static/icon/bootstrap-icons/plus.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: #ebf4fe;
|
||||
height: 2vh;
|
||||
}
|
||||
.element-navigation .btn-add-case:hover{
|
||||
color: #fff;
|
||||
background-image: url(/static/icon/bootstrap-icons/plus-white.svg);
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.element-navigation .btn-add-scene{
|
||||
background-image: url(/static/icon/bootstrap-icons/plus.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: #ebf4fe;
|
||||
height: 2vh;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
.element-navigation .btn-add-scene:hover{
|
||||
color: #fff;
|
||||
background-image: url(/static/icon/bootstrap-icons/plus-white.svg);
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.element-navigation .btn-fold{
|
||||
background-image: url(/static/icon/bootstrap-icons/chevron-down.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-fold:hover{
|
||||
background-image: url(/static/icon/bootstrap-icons/chevron-down-white.svg);
|
||||
background-color: #9b9b9b;
|
||||
}
|
||||
|
||||
.element-navigation .btn-unfold{
|
||||
background-image: url(/static/icon/bootstrap-icons/chevron-right.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.element-navigation .btn-unfold:hover{
|
||||
background-image: url(/static/icon/bootstrap-icons/chevron-right-white.svg);
|
||||
background-color: #9b9b9b;
|
||||
}
|
||||
/* 案例场景添加按钮样式 “+” [END] */
|
||||
|
||||
.element-navigation .tippy-content{
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.btn-add-case-tippy-content{
|
||||
width: 30vw;
|
||||
}
|
||||
.btn-add-case-tippy-content .btn{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.element-navigation.bg-sortable-ghost{
|
||||
background-color: #ebf4fe;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*如下两个css共同作用,限制bootstrap-table表宽,同时提供超长内容省略号显示*/
|
||||
/*表格列宽在js中限制*/
|
||||
.table {
|
||||
/*固定表格宽度*/
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
|
||||
.table tbody tr td{
|
||||
/*超过宽度使用省略号*/
|
||||
overflow: hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.element-tool .ace-tool-script{
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.element-tool .ace-tool-log{
|
||||
height: 20vh;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-alarm-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M6 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H9v1.07a7.001 7.001 0 0 1 3.274 12.474l.601.602a.5.5 0 0 1-.707.708l-.746-.746A6.97 6.97 0 0 1 8 16a6.97 6.97 0 0 1-3.422-.892l-.746.746a.5.5 0 0 1-.707-.708l.602-.602A7.001 7.001 0 0 1 7 2.07V1h-.5A.5.5 0 0 1 6 .5zM.86 5.387A2.5 2.5 0 1 1 4.387 1.86 8.035 8.035 0 0 0 .86 5.387zM11.613 1.86a2.5 2.5 0 1 1 3.527 3.527 8.035 8.035 0 0 0-3.527-3.527zM8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 650 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-alarm" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 718 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-bottom" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V2z"/>
|
||||
<path fill-rule="evenodd" d="M1 14.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 323 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-center" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1a.5.5 0 0 1 .5.5V6h-1V1.5A.5.5 0 0 1 8 1zm0 14a.5.5 0 0 1-.5-.5V10h1v4.5a.5.5 0 0 1-.5.5zM2 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 317 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-end" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M14.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 1 0v-13a.5.5 0 0 0-.5-.5z"/>
|
||||
<path d="M13 7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 320 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-middle" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 13a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v10zM1 8a.5.5 0 0 0 .5.5H6v-1H1.5A.5.5 0 0 0 1 8zm14 0a.5.5 0 0 1-.5.5H10v-1h4.5a.5.5 0 0 1 .5.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 318 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-start" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M1.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M3 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 320 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-align-top" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 14a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v10z"/>
|
||||
<path fill-rule="evenodd" d="M1 1.5a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 0-1h-13a.5.5 0 0 0-.5.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 318 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-alt" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5zm10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 348 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-app-indicator" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M5.5 2A3.5 3.5 0 0 0 2 5.5v5A3.5 3.5 0 0 0 5.5 14h5a3.5 3.5 0 0 0 3.5-3.5V8a.5.5 0 0 1 1 0v2.5a4.5 4.5 0 0 1-4.5 4.5h-5A4.5 4.5 0 0 1 1 10.5v-5A4.5 4.5 0 0 1 5.5 1H8a.5.5 0 0 1 0 1H5.5z"/>
|
||||
<path d="M16 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 409 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-app" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M11 2H5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4H5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 304 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-archive-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M12.643 15C13.979 15 15 13.845 15 12.5V5H1v7.5C1 13.845 2.021 15 3.357 15h9.286zM5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM.8 1a.8.8 0 0 0-.8.8V3a.8.8 0 0 0 .8.8h14.4A.8.8 0 0 0 16 3V1.8a.8.8 0 0 0-.8-.8H.8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 381 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-archive" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 423 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-arrow-90deg-down" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.854 14.854a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V3.5A2.5 2.5 0 0 1 6.5 1h8a.5.5 0 0 1 0 1h-8A1.5 1.5 0 0 0 5 3.5v9.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 352 B |