This commit is contained in:
azhengzz 2021-02-02 18:37:47 +08:00
parent 281b706650
commit 106315b07a
4213 changed files with 904545 additions and 0 deletions

View File

@ -0,0 +1,5 @@
FLASK_APP=webserver.py
FLASK_ENV=production
FLASK_DEBUG=0
#FLASK_ENV=development
#FLASK_DEBUG=1

View File

@ -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)

View File

@ -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超时限制

View File

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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, # 应答包
}
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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 += "&timestamp=" + 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

View File

@ -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())

View File

@ -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()
),)

View File

@ -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

View File

@ -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())

View File

@ -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)

View File

@ -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 '}, {}))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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_

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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事件

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,8 @@
# coding=utf-8
from flask import Blueprint
bp = Blueprint("ajax", __name__)
from app.routes.ajax import routes

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
# coding=utf-8
from flask import Blueprint
bp = Blueprint("auth", __name__)
from app.routes.auth import routes

View File

@ -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)
)

View File

@ -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='确定')

View File

@ -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)

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("case", __name__)
from app.routes.case import routes

View File

@ -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)

View File

@ -0,0 +1,8 @@
# coding=utf-8
from flask import Blueprint
bp = Blueprint("error", __name__)
from app.routes.error import routes

View File

@ -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

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("logic_controller", __name__)
from app.routes.logic_controller import routes

View File

@ -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)

View File

@ -0,0 +1,8 @@
# coding=utf-8
from flask import Blueprint
bp = Blueprint("main", __name__)
from app.routes.main import routes

View File

@ -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')

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("module", __name__)
from app.routes.module import routes

View File

@ -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)

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("project", __name__)
from app.routes.project import routes

View File

@ -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')

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("report", __name__)
from app.routes.report import routes

View File

@ -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)

View File

@ -0,0 +1,7 @@
from flask import Blueprint
bp = Blueprint("scene", __name__)
from app.routes.scene import routes

View File

@ -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)

View File

@ -0,0 +1,8 @@
# coding=utf-8
from flask import Blueprint
bp = Blueprint("setting", __name__)
from app.routes.setting import routes

View File

@ -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('提交')

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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%;
}

View File

@ -0,0 +1,7 @@
.element-case .ace-message-body{
height: 40vh;
}
.element-case .div-case-up{
max-height: 65vh;
}

View File

@ -0,0 +1,3 @@
.element-case .ace-sql{
height: 30vh;
}

View File

@ -0,0 +1,3 @@
.element-case .ace-command{
height: 30vh;
}

View File

@ -0,0 +1,10 @@
.card-body-module-list{
height: 80vh;
overflow-y: auto;
}
.fixed-table-pagination{
position: absolute;
bottom: 1vh;
width: 95%;
}

View File

@ -0,0 +1,10 @@
.card-body-project-list{
height: 80vh;
overflow-y: auto;
}
.fixed-table-pagination{
position: absolute;
bottom: 1vh;
width: 95%;
}

View File

@ -0,0 +1,10 @@
.card-body-report-list{
height: 80vh;
overflow-y: auto;
}
.fixed-table-pagination{
position: absolute;
bottom: 1vh;
width: 95%;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
.element-tool .ace-tool-script{
height: 40vh;
}
.element-tool .ace-tool-log{
height: 20vh;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More