提交所有代码到 qiaoxinjiu 分支
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
53
app/api/controller/automationController.py
Normal file
53
app/api/controller/automationController.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# encoding: UTF-8
|
||||
from flask import g
|
||||
|
||||
from const import AUTOMATION_CALLBACK_SECRET
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..service.automationService import AutomationService
|
||||
|
||||
|
||||
class AutomationController(BaseCrudController):
|
||||
def validate_callback_secret(self):
|
||||
callback_secret = self.req_data.get('_callback_secret')
|
||||
if AUTOMATION_CALLBACK_SECRET and callback_secret != AUTOMATION_CALLBACK_SECRET:
|
||||
return False, '回调鉴权失败'
|
||||
return True, ''
|
||||
|
||||
def case_run(self):
|
||||
return AutomationService.create_case_execution(self.session, self.req_data, getattr(g, 'current_user_id', None))
|
||||
|
||||
def plan_run(self):
|
||||
return AutomationService.create_plan_execution(self.session, self.req_data, getattr(g, 'current_user_id', None))
|
||||
|
||||
def execution_list(self):
|
||||
return AutomationService.list_executions(self.session, self.req_data)
|
||||
|
||||
def execution_detail(self):
|
||||
execution_id = self._get(self.req_data, 'executionId', 'id')
|
||||
if not execution_id:
|
||||
return {}, 'executionId 为必传参数'
|
||||
return AutomationService.get_execution_detail(self.session, execution_id)
|
||||
|
||||
def execution_case_list(self):
|
||||
return AutomationService.list_execution_cases(self.session, self.req_data)
|
||||
|
||||
def execution_case_pull(self):
|
||||
execution_id = self._get(self.req_data, 'executionId', 'execution_id')
|
||||
if not execution_id:
|
||||
return {}, 'executionId 为必传参数'
|
||||
return AutomationService.pull_execution_cases(self.session, execution_id, self.req_data.get('_callback_token'))
|
||||
|
||||
def execution_queued(self):
|
||||
return AutomationService.mark_execution_queued(self.session, self.req_data)
|
||||
|
||||
def execution_start(self):
|
||||
return AutomationService.mark_execution_started(self.session, self.req_data)
|
||||
|
||||
def execution_case_result(self):
|
||||
return AutomationService.save_case_result(self.session, self.req_data)
|
||||
|
||||
def execution_finish(self):
|
||||
return AutomationService.finish_execution(self.session, self.req_data)
|
||||
|
||||
def execution_abort(self):
|
||||
return AutomationService.abort_execution(self.session, self.req_data)
|
||||
@@ -169,10 +169,12 @@ class CaseController(BaseCrudController):
|
||||
steps_value = self._get(self.req_data, 'steps', default='')
|
||||
if isinstance(steps_value, (list, dict)):
|
||||
steps_value = ''
|
||||
product_id = self._get(self.req_data, 'productId')
|
||||
module_id = self._get(self.req_data, 'moduleId')
|
||||
add_info = {
|
||||
'project_id': project_id,
|
||||
'module_id': self._get(self.req_data, 'moduleId'),
|
||||
'case_key': self._get(self.req_data, 'caseKey') or CaseService.next_case_key(self.session, project_id),
|
||||
'module_id': module_id,
|
||||
'case_key': self._get(self.req_data, 'caseKey') or CaseService.next_case_key(self.session, project_id, module_id, product_id),
|
||||
'title': title,
|
||||
'preconditions': self._get(self.req_data, 'preconditions'),
|
||||
'steps': steps_value,
|
||||
@@ -355,7 +357,7 @@ class CaseController(BaseCrudController):
|
||||
|
||||
retry_count = 0
|
||||
max_retries = 5
|
||||
case_key = CaseService.next_case_key(self.session, project_id)
|
||||
case_key = CaseService.next_case_key(self.session, project_id, module_id)
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
|
||||
@@ -56,7 +56,7 @@ class PlanController(BaseCrudController):
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not project_id or not name:
|
||||
return 0, 'projectId、name 为必传参数'
|
||||
add_info = {'project_id': project_id, 'name': name, 'version': self._get(self.req_data, 'version'), 'description': self._get(self.req_data, 'description'), 'start_date': self._get(self.req_data, 'startDate', 'start_time'), 'end_date': self._get(self.req_data, 'endDate', 'end_time'), 'owner_id': self._get(self.req_data, 'ownerId', 'owner_id'), 'status': int(self._get(self.req_data, 'status', default=0)), 'environment_id': self._get(self.req_data, 'environmentId', 'environment_id'), 'is_delete': 0}
|
||||
add_info = {'project_id': project_id, 'name': name, 'version': self._get(self.req_data, 'version'), 'description': self._get(self.req_data, 'description'), 'start_date': self._get(self.req_data, 'startDate', 'start_time'), 'end_date': self._get(self.req_data, 'endDate', 'end_time'), 'owner_id': self._get(self.req_data, 'ownerId', 'owner_id'), 'status': int(self._get(self.req_data, 'status', default=0)), 'environment_id': self._get(self.req_data, 'environmentId', 'environment_id'), 'jenkins_url': self._get(self.req_data, 'jenkinsUrl', 'jenkins_url'), 'is_auto': int(self._get(self.req_data, 'isAuto', 'is_auto', default=0)), 'is_delete': 0}
|
||||
return PlanService.create(self.session, TestPlan, add_info)
|
||||
|
||||
def plan_update(self):
|
||||
@@ -65,7 +65,7 @@ class PlanController(BaseCrudController):
|
||||
if not plan_id:
|
||||
return 0, 'planId 为必传参数'
|
||||
update_info = {}
|
||||
for req_keys, column_key in [(('name', 'name'), 'name'), (('version', 'version'), 'version'), (('description', 'description'), 'description'), (('startDate', 'start_time', 'start_date'), 'start_date'), (('endDate', 'end_time', 'end_date'), 'end_date'), (('ownerId', 'owner_id'), 'owner_id'), (('status', 'status'), 'status'), (('environmentId', 'environment_id'), 'environment_id')]:
|
||||
for req_keys, column_key in [(('name', 'name'), 'name'), (('version', 'version'), 'version'), (('description', 'description'), 'description'), (('startDate', 'start_time', 'start_date'), 'start_date'), (('endDate', 'end_time', 'end_date'), 'end_date'), (('ownerId', 'owner_id'), 'owner_id'), (('status', 'status'), 'status'), (('environmentId', 'environment_id'), 'environment_id'), (('jenkinsUrl', 'jenkins_url'), 'jenkins_url'), (('isAuto', 'is_auto'), 'is_auto')]:
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
@@ -99,7 +99,7 @@ class PlanController(BaseCrudController):
|
||||
return PlanService.batch_create(self.session, PlanCase, batch_info_list)
|
||||
|
||||
def plan_case_list(self):
|
||||
plan_id = self._get(self.req_data, 'planId')
|
||||
plan_id = self._get(self.req_data, 'planId', 'plan_id')
|
||||
filters = [PlanCase.plan_id == int(plan_id)] if plan_id else []
|
||||
round_no = self._get(self.req_data, 'roundNo')
|
||||
if round_no not in (None, ''):
|
||||
@@ -151,38 +151,9 @@ class PlanController(BaseCrudController):
|
||||
update_info = {'status': int(self._get(self.req_data, 'status', default=0)), 'actual_result': self._get(self.req_data, 'actualResult'), 'defect_links': self._get(self.req_data, 'defectLinks', default=[]), 'attachments': self._get(self.req_data, 'attachments', default=[]), 'executed_time': datetime.now(), 'execution_duration': self._get(self.req_data, 'executionDuration')}
|
||||
result = PlanService.update_by_id(self.session, PlanCase, plan_case_id, update_info, soft_delete=False)
|
||||
|
||||
self._update_plan_status(plan_id)
|
||||
PlanService.refresh_plan_status(self.session, plan_id)
|
||||
|
||||
return result
|
||||
|
||||
def _update_plan_status(self, plan_id):
|
||||
total = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id).count()
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
unexecuted_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 0).count()
|
||||
passed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 1).count()
|
||||
failed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status.in_([2, 3])).count()
|
||||
|
||||
plan = PlanService.get_by_id(self.session, TestPlan, plan_id)
|
||||
if not plan:
|
||||
return
|
||||
|
||||
if plan.status == 3:
|
||||
return
|
||||
|
||||
if unexecuted_count == 0:
|
||||
if failed_count == 0:
|
||||
new_status = 4
|
||||
else:
|
||||
new_status = 2
|
||||
elif unexecuted_count < total:
|
||||
new_status = 1
|
||||
else:
|
||||
new_status = plan.status
|
||||
|
||||
if new_status != plan.status:
|
||||
PlanService.update_by_id(self.session, TestPlan, plan_id, {'status': new_status})
|
||||
|
||||
def progress(self):
|
||||
"""查询计划进度统计。"""
|
||||
|
||||
@@ -208,9 +208,6 @@ class RbacController(BaseCrudController):
|
||||
(('name',), 'name'),
|
||||
(('code',), 'code'),
|
||||
(('type',), 'type'),
|
||||
(('path',), 'path'),
|
||||
(('component',), 'component'),
|
||||
(('icon',), 'icon'),
|
||||
(('permissionCode', 'permission_code'), 'permission_code'),
|
||||
(('sort',), 'sort'),
|
||||
(('visible',), 'visible'),
|
||||
@@ -220,12 +217,30 @@ class RbacController(BaseCrudController):
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
|
||||
for key in ['path', 'component', 'icon']:
|
||||
if key in self.req_data:
|
||||
update_info[key] = self.req_data[key]
|
||||
|
||||
return RbacService.update_by_id(self.session, Menu, menu_id, update_info)
|
||||
|
||||
def menu_delete(self):
|
||||
menu_id = self._get(self.req_data, 'menuId', 'id')
|
||||
if not menu_id:
|
||||
return 0, 'menuId 为必传参数'
|
||||
|
||||
menu = RbacService.get_by_id(self.session, Menu, menu_id)
|
||||
if menu and menu.permission_code:
|
||||
permission = self.session.query(Permission).filter(
|
||||
Permission.code == menu.permission_code,
|
||||
Permission.is_delete == 0
|
||||
).first()
|
||||
if permission:
|
||||
self.session.query(RolePermission).filter(
|
||||
RolePermission.permission_id == permission.id,
|
||||
RolePermission.is_delete == 0
|
||||
).update({'is_delete': 1})
|
||||
|
||||
return RbacService.delete_by_id(self.session, Menu, menu_id)
|
||||
|
||||
def role_permission_list(self):
|
||||
@@ -254,4 +269,44 @@ class RbacController(BaseCrudController):
|
||||
menu_ids = self._get(self.req_data, 'menuIds', default=[])
|
||||
if not role_id:
|
||||
return 0, 'roleId 为必传参数'
|
||||
|
||||
if isinstance(menu_ids, str):
|
||||
import json
|
||||
menu_ids = json.loads(menu_ids)
|
||||
if not isinstance(menu_ids, list):
|
||||
menu_ids = []
|
||||
|
||||
menu_permission_codes = RbacService.get_menu_permission_codes(self.session, menu_ids)
|
||||
|
||||
permission_ids = RbacService.get_permission_ids_by_codes(self.session, menu_permission_codes)
|
||||
|
||||
existing_permission_ids = RbacService.get_role_permission_ids(self.session, role_id)
|
||||
|
||||
deleted_permission_ids = [pid for pid in existing_permission_ids if pid not in permission_ids]
|
||||
if deleted_permission_ids:
|
||||
self.session.query(RolePermission).filter(
|
||||
RolePermission.role_id == int(role_id),
|
||||
RolePermission.permission_id.in_(deleted_permission_ids),
|
||||
RolePermission.is_delete == 0
|
||||
).update({'is_delete': 1})
|
||||
|
||||
new_permission_ids = [pid for pid in permission_ids if pid not in existing_permission_ids]
|
||||
if new_permission_ids:
|
||||
existing_deleted = self.session.query(RolePermission).filter(
|
||||
RolePermission.role_id == int(role_id),
|
||||
RolePermission.permission_id.in_(new_permission_ids),
|
||||
RolePermission.is_delete == 1
|
||||
).all()
|
||||
existing_deleted_map = {rp.permission_id: rp for rp in existing_deleted}
|
||||
|
||||
for permission_id in new_permission_ids:
|
||||
if permission_id in existing_deleted_map:
|
||||
existing_deleted_map[permission_id].is_delete = 0
|
||||
else:
|
||||
self.session.add(RolePermission(
|
||||
role_id=int(role_id),
|
||||
permission_id=permission_id,
|
||||
is_delete=0
|
||||
))
|
||||
|
||||
return RbacService.assign_menus(self.session, role_id, menu_ids)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.userModel import User
|
||||
from ..service.userService import UserService
|
||||
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token
|
||||
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token, create_refresh_token
|
||||
|
||||
|
||||
class UserController(BaseCrudController):
|
||||
@@ -118,11 +118,14 @@ class UserController(BaseCrudController):
|
||||
return {}, '用户已禁用!'
|
||||
UserService.update_last_login_time(self.session, user.id)
|
||||
token, expire_seconds = create_token(user.id)
|
||||
refresh_token, refresh_expire_seconds = create_refresh_token(user.id)
|
||||
ret = self.serialize(user, ['is_delete', 'password_hash'])
|
||||
ret['role_ids'] = UserService.get_user_role_ids(self.session, user.id)
|
||||
ret['token'] = token
|
||||
ret['token_type'] = 'Bearer'
|
||||
ret['expires_in'] = expire_seconds
|
||||
ret['refresh_token'] = refresh_token
|
||||
ret['refresh_expires_in'] = refresh_expire_seconds
|
||||
ret['refresh_threshold_seconds'] = TOKEN_REFRESH_THRESHOLD_SECONDS
|
||||
ret['refresh_mechanism'] = '请求任意已登录接口时,若token剩余有效期小于阈值则自动续期到完整有效期'
|
||||
return ret, ''
|
||||
|
||||
BIN
app/api/dao/__pycache__/automationDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/automationDao.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
129
app/api/dao/automationDao.py
Normal file
129
app/api/dao/automationDao.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# encoding: UTF-8
|
||||
from sqlalchemy import func
|
||||
|
||||
from logger import logger
|
||||
from ..model.automationModel import AutoExecution, AutoExecutionCase
|
||||
from ..model.caseModel import TestCase
|
||||
from ..model.planModel import PlanCase, TestPlan
|
||||
|
||||
|
||||
class AutomationDao(object):
|
||||
@staticmethod
|
||||
def create_execution(session, add_info):
|
||||
obj = AutoExecution(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'AutoExecution新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj, ''
|
||||
|
||||
@staticmethod
|
||||
def batch_create_execution_cases(session, batch_info_list):
|
||||
if not batch_info_list:
|
||||
return [], ''
|
||||
objs = [AutoExecutionCase(**info) for info in batch_info_list]
|
||||
session.add_all(objs)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'AutoExecutionCase批量新增失败!{err}')
|
||||
return [], f'批量新增失败!{err}'
|
||||
return objs, ''
|
||||
|
||||
@staticmethod
|
||||
def update_execution_by_id(session, execution_id, update_info):
|
||||
update_res = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'AutoExecution更新失败!id: {execution_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应执行记录!'
|
||||
return int(execution_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_execution_by_id(session, execution_id):
|
||||
return session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
|
||||
|
||||
@staticmethod
|
||||
def list_execution_by_filters(session, filters, page=1, limit=20):
|
||||
query = session.query(AutoExecution).filter(*filters)
|
||||
total = query.count()
|
||||
items = query.order_by(AutoExecution.created_time.desc()).offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_execution_case_by_id(session, execution_case_id):
|
||||
return session.query(AutoExecutionCase).filter(AutoExecutionCase.id == int(execution_case_id)).first()
|
||||
|
||||
@staticmethod
|
||||
def get_execution_case_by_unique(session, execution_id, case_id, plan_case_id=None):
|
||||
filters = [AutoExecutionCase.execution_id == int(execution_id), AutoExecutionCase.case_id == int(case_id)]
|
||||
if plan_case_id:
|
||||
filters.append(AutoExecutionCase.plan_case_id == int(plan_case_id))
|
||||
return session.query(AutoExecutionCase).filter(*filters).order_by(AutoExecutionCase.id.asc()).first()
|
||||
|
||||
@staticmethod
|
||||
def update_execution_case_by_id(session, execution_case_id, update_info):
|
||||
update_res = session.query(AutoExecutionCase).filter(AutoExecutionCase.id == int(execution_case_id)).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'AutoExecutionCase更新失败!id: {execution_case_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应执行明细!'
|
||||
return int(execution_case_id), ''
|
||||
|
||||
@staticmethod
|
||||
def list_execution_case_by_filters(session, filters, page=1, limit=20):
|
||||
query = session.query(AutoExecutionCase).filter(*filters)
|
||||
total = query.count()
|
||||
items = query.order_by(AutoExecutionCase.id.asc()).offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def count_execution_case_summary(session, execution_id):
|
||||
rows = session.query(AutoExecutionCase.status, func.count(AutoExecutionCase.id)).filter(
|
||||
AutoExecutionCase.execution_id == int(execution_id)
|
||||
).group_by(AutoExecutionCase.status).all()
|
||||
summary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
|
||||
for status, count in rows:
|
||||
summary[int(status)] = int(count)
|
||||
summary['total'] = sum(summary.values())
|
||||
return summary
|
||||
|
||||
@staticmethod
|
||||
def query_case_auto_item(session, case_id):
|
||||
return session.query(TestCase).filter(
|
||||
TestCase.id == int(case_id), TestCase.is_delete == 0, TestCase.is_auto == 1
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def query_plan_auto_cases(session, plan_id, round_no=None, case_ids=None):
|
||||
query = session.query(PlanCase, TestCase).join(
|
||||
TestCase, PlanCase.case_id == TestCase.id
|
||||
).filter(
|
||||
PlanCase.plan_id == int(plan_id),
|
||||
TestCase.is_delete == 0,
|
||||
TestCase.is_auto == 1
|
||||
)
|
||||
if round_no not in (None, ''):
|
||||
query = query.filter(PlanCase.round_no == int(round_no))
|
||||
if case_ids:
|
||||
query = query.filter(PlanCase.case_id.in_([int(case_id) for case_id in case_ids]))
|
||||
return query.order_by(PlanCase.id.asc()).all()
|
||||
|
||||
@staticmethod
|
||||
def update_plan_case_result(session, plan_case_id, update_info):
|
||||
update_res = session.query(PlanCase).filter(PlanCase.id == int(plan_case_id)).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'PlanCase更新失败!id: {plan_case_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应计划用例!'
|
||||
return int(plan_case_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_plan_by_id(session, plan_id):
|
||||
return session.query(TestPlan).filter(TestPlan.id == int(plan_id), TestPlan.is_delete == 0).first()
|
||||
@@ -56,9 +56,166 @@ class CaseDao(object):
|
||||
return CaseDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def next_case_key(session, project_id):
|
||||
count_num = session.query(func.count(TestCase.id)).filter(TestCase.project_id == int(project_id)).scalar() or 0
|
||||
return 'TC-{:03d}'.format(count_num + 1)
|
||||
def next_case_key(session, project_id, module_id=None, product_id=None):
|
||||
from ..model.productModel import Product
|
||||
from ..model.projectModel import Project
|
||||
from ..model.caseModel import Module
|
||||
|
||||
product_abbr = ''
|
||||
if product_id:
|
||||
product = session.query(Product).filter(Product.id == int(product_id), Product.is_delete == 0).first()
|
||||
if product and product.name:
|
||||
product_abbr = CaseDao._generate_abbreviation(product.name)
|
||||
|
||||
project_abbr = ''
|
||||
project = session.query(Project).filter(Project.id == int(project_id), Project.is_delete == 0).first()
|
||||
if project and project.name:
|
||||
project_abbr = CaseDao._generate_abbreviation(project.name)
|
||||
|
||||
module_abbr = ''
|
||||
if module_id:
|
||||
module = session.query(Module).filter(Module.id == int(module_id), Module.is_delete == 0).first()
|
||||
if module and module.name:
|
||||
module_abbr = CaseDao._generate_abbreviation(module.name)
|
||||
|
||||
parts = ['TC']
|
||||
if product_abbr:
|
||||
parts.append(product_abbr)
|
||||
if project_abbr:
|
||||
parts.append(project_abbr)
|
||||
if module_abbr:
|
||||
parts.append(module_abbr)
|
||||
|
||||
prefix = '-'.join(parts)
|
||||
|
||||
count_num = session.query(func.count(TestCase.id)).filter(
|
||||
TestCase.project_id == int(project_id),
|
||||
TestCase.is_delete == 0,
|
||||
TestCase.case_key.like(f'{prefix}-%')
|
||||
).scalar() or 0
|
||||
|
||||
return '{}-{:03d}'.format(prefix, count_num + 1)
|
||||
|
||||
@staticmethod
|
||||
def _generate_abbreviation(name):
|
||||
import re
|
||||
chinese_pattern = re.compile(r'[\u4e00-\u9fff]+')
|
||||
english_pattern = re.compile(r'[a-zA-Z]+')
|
||||
|
||||
chinese_chars = chinese_pattern.findall(name)
|
||||
if chinese_chars:
|
||||
full_chinese = ''.join(chinese_chars)
|
||||
abbr = ''.join([CaseDao._get_pinyin_first_char(c) for c in full_chinese[:4]])
|
||||
abbr = abbr.lower()
|
||||
if len(abbr) < 2:
|
||||
abbr = abbr.ljust(2, 'n')
|
||||
return abbr
|
||||
|
||||
english_words = english_pattern.findall(name)
|
||||
if english_words:
|
||||
abbr = english_words[0].lower()[:4]
|
||||
if len(abbr) < 2:
|
||||
abbr = abbr.ljust(2, 'x')
|
||||
return abbr
|
||||
|
||||
abbr = name.lower()[:4]
|
||||
if len(abbr) < 2:
|
||||
abbr = abbr.ljust(2, 'x')
|
||||
return abbr
|
||||
|
||||
@staticmethod
|
||||
def _get_pinyin_first_char(char):
|
||||
pinyin_map = {
|
||||
'智': 'Z', '慧': 'H', '运': 'Y', '营': 'Y',
|
||||
'报': 'B', '关': 'G', '工': 'G', '作': 'Z', '台': 'T',
|
||||
'测': 'C', '试': 'S', '用': 'Y', '例': 'L',
|
||||
'产': 'C', '品': 'P', '项': 'X', '目': 'M',
|
||||
'模': 'M', '块': 'K', '管': 'G', '理': 'L',
|
||||
'系': 'X', '统': 'T', '功': 'G', '能': 'N',
|
||||
'页': 'Y', '面': 'M', '查': 'C', '询': 'X',
|
||||
'添': 'T', '加': 'J', '编': 'B', '辑': 'J',
|
||||
'删': 'S', '除': 'C', '导': 'D', '入': 'R',
|
||||
'导': 'D', '出': 'C', '批': 'P', '量': 'L',
|
||||
'设': 'S', '置': 'Z', '配': 'P', '置': 'Z',
|
||||
'权': 'Q', '限': 'X', '角': 'J', '色': 'S',
|
||||
'用': 'Y', '户': 'H', '组': 'Z', '织': 'Z',
|
||||
'计': 'J', '划': 'H', '执': 'Z', '行': 'X',
|
||||
'报': 'B', '告': 'G', '统': 'T', '计': 'J',
|
||||
'首': 'S', '页': 'Y', '仪': 'Y', '表': 'B',
|
||||
'烟': 'Y', '冒': 'M', '回': 'H', '归': 'G',
|
||||
'集': 'J', '成': 'C', '接': 'J', '口': 'K',
|
||||
'安': 'A', '全': 'Q', '日': 'R', '志': 'Z',
|
||||
'监': 'J', '控': 'K', '优': 'Y', '化': 'H',
|
||||
'性': 'X', '能': 'N', '首': 'S', '尾': 'W',
|
||||
'开': 'K', '发': 'F', '测': 'C', '运': 'Y',
|
||||
'维': 'W', '设': 'S', '计': 'J', '研': 'Y',
|
||||
'发': 'F', '部': 'B', '组': 'Z', '个': 'G',
|
||||
'人': 'R', '公': 'G', '司': 'S', '有': 'Y',
|
||||
'限': 'X', '责': 'Z', '任': 'R', '股': 'G',
|
||||
'份': 'F', '集': 'J', '团': 'T', '科': 'K',
|
||||
'技': 'J', '网': 'W', '络': 'L', '信': 'X',
|
||||
'息': 'X', '软': 'R', '件': 'J', '系': 'X',
|
||||
'统': 'T', '解': 'J', '决': 'J', '方': 'F',
|
||||
'案': 'A', '服': 'F', '务': 'W', '支': 'Z',
|
||||
'持': 'C', '培': 'P', '训': 'X', '咨': 'Z',
|
||||
'询': 'X', '销': 'X', '售': 'S', '市': 'S',
|
||||
'场': 'C', '运': 'Y', '营': 'Y', '管': 'G',
|
||||
'理': 'L', '财': 'C', '务': 'W', '人': 'R',
|
||||
'力': 'L', '资': 'Z', '源': 'Y', '行': 'X',
|
||||
'政': 'Z', '法': 'F', '律': 'L', '合': 'H',
|
||||
'规': 'G', '质': 'Z', '量': 'L', '安': 'A',
|
||||
'全': 'Q', '环': 'H', '境': 'J', '职': 'Z',
|
||||
'能': 'N', '模': 'M', '块': 'K', '页': 'Y',
|
||||
'面': 'M', '窗': 'C', '口': 'K', '表': 'B',
|
||||
'单': 'D', '按': 'A', '钮': 'N', '链': 'L',
|
||||
'接': 'J', '图': 'T', '标': 'B', '菜': 'C',
|
||||
'单': 'D', '导': 'D', '航': 'H', '搜': 'S',
|
||||
'索': 'S', '过': 'G', '滤': 'L', '排': 'P',
|
||||
'序': 'X', '分': 'F', '页': 'Y', '导': 'D',
|
||||
'入': 'R', '导': 'D', '出': 'C', '打': 'D',
|
||||
'印': 'Y', '导': 'D', '出': 'C', '删': 'S',
|
||||
'除': 'C', '复': 'F', '制': 'Z', '粘': 'N',
|
||||
'贴': 'T', '剪': 'J', '切': 'Q', '保': 'B',
|
||||
'存': 'C', '取': 'Q', '消': 'X', '确': 'Q',
|
||||
'认': 'R', '提': 'T', '交': 'J', '审': 'S',
|
||||
'核': 'H', '批': 'P', '准': 'Z', '拒': 'J',
|
||||
'绝': 'J', '返': 'F', '回': 'H', '修': 'X',
|
||||
'改': 'G', '查': 'C', '看': 'K', '详': 'X',
|
||||
'情': 'Q', '列': 'L', '表': 'B', '统': 'T',
|
||||
'计': 'J', '图': 'T', '表': 'B', '报': 'B',
|
||||
'告': 'G', '日': 'R', '志': 'Z', '备': 'B',
|
||||
'注': 'Z', '描': 'M', '述': 'S', '名': 'M',
|
||||
'称': 'C', '编': 'B', '号': 'H', '类': 'L',
|
||||
'型': 'X', '状': 'Z', '态': 'T', '优': 'Y',
|
||||
'先': 'X', '级': 'J', '标': 'B', '签': 'Q',
|
||||
'关': 'G', '键': 'J', '词': 'C', '描': 'M',
|
||||
'述': 'S', '附': 'F', '件': 'J', '链': 'L',
|
||||
'接': 'J', '时': 'S', '间': 'J', '日': 'R',
|
||||
'期': 'Q', '数': 'S', '量': 'L', '金': 'J',
|
||||
'额': 'E', '价': 'J', '格': 'G', '数': 'S',
|
||||
'据': 'J', '库': 'K', '服': 'F', '务': 'W',
|
||||
'器': 'Q', '线': 'X', '程': 'C', '进': 'J',
|
||||
'度': 'D', '速': 'S', '度': 'D', '效': 'X',
|
||||
'率': 'L', '性': 'X', '能': 'N', '稳': 'W',
|
||||
'定': 'D', '可': 'K', '靠': 'K', '安': 'A',
|
||||
'全': 'Q', '兼': 'J', '容': 'R', '扩': 'K',
|
||||
'展': 'Z', '升': 'S', '级': 'J', '更': 'G',
|
||||
'新': 'X', '修': 'X', '补': 'B', '修': 'X',
|
||||
'复': 'F', '优': 'Y', '化': 'H', '重': 'Z',
|
||||
'构': 'G', '改': 'G', '造': 'Z', '移': 'Y',
|
||||
'植': 'Z', '集': 'J', '成': 'C', '接': 'J',
|
||||
'口': 'K', '调': 'T', '试': 'S', '测': 'C',
|
||||
'试': 'S', '验': 'Y', '证': 'Z', '确': 'Q',
|
||||
'认': 'R', '回': 'H', '归': 'G', '演': 'Y',
|
||||
'示': 'S', '培': 'P', '训': 'X', '指': 'Z',
|
||||
'导': 'D', '支': 'Z', '持': 'C', '帮': 'B',
|
||||
'助': 'Z', '反': 'F', '馈': 'K', '投': 'T',
|
||||
'诉': 'S', '建': 'J', '议': 'Y', '评': 'P',
|
||||
'独': 'D', '立': 'L', '站': 'Z', '采': 'C',
|
||||
'购': 'G', '供': 'G', '应': 'Y', '商': 'S',
|
||||
'价': 'J', '满': 'M', '意': 'Y', '度': 'D'
|
||||
}
|
||||
return pinyin_map.get(char, 'N')
|
||||
|
||||
@staticmethod
|
||||
def next_snapshot_version(session, case_id):
|
||||
|
||||
@@ -183,3 +183,23 @@ class RbacDao(object):
|
||||
def get_role_name_map(session):
|
||||
items = session.query(Role).filter(Role.is_delete == 0, Role.status == 1).all()
|
||||
return {item.id: item.name for item in items}
|
||||
|
||||
@staticmethod
|
||||
def get_menu_permission_codes(session, menu_ids):
|
||||
if not menu_ids:
|
||||
return []
|
||||
items = session.query(Menu.permission_code).filter(
|
||||
Menu.id.in_(menu_ids),
|
||||
Menu.is_delete == 0
|
||||
).all()
|
||||
return [item[0] for item in items if item[0]]
|
||||
|
||||
@staticmethod
|
||||
def get_permission_ids_by_codes(session, permission_codes):
|
||||
if not permission_codes:
|
||||
return []
|
||||
items = session.query(Permission.id).filter(
|
||||
Permission.code.in_(permission_codes),
|
||||
Permission.is_delete == 0
|
||||
).all()
|
||||
return [item[0] for item in items]
|
||||
|
||||
BIN
app/api/model/__pycache__/automationModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/automationModel.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
70
app/api/model/automationModel.py
Normal file
70
app/api/model/automationModel.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class AutoExecution(Base):
|
||||
__tablename__ = 'auto_execution'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
execution_no = Column(String(64), nullable=False, unique=True, comment='执行编号')
|
||||
trigger_type = Column(SmallInteger, nullable=False, comment='1:单条 2:计划')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
plan_id = Column(BigInteger, comment='计划id')
|
||||
plan_round_no = Column(Integer, comment='计划轮次')
|
||||
source_case_id = Column(BigInteger, comment='单条执行来源case_id')
|
||||
env_code = Column(String(32), nullable=False, comment='环境编码')
|
||||
run_mode = Column(SmallInteger, default=1, comment='1:串行 2:并行')
|
||||
status = Column(SmallInteger, nullable=False, default=0, comment='0:待触发 1:触发中 2:排队中 3:执行中 4:成功 5:失败 6:已取消 7:触发失败 8:回调异常')
|
||||
jenkins_job_name = Column(String(128), comment='Jenkins任务名称')
|
||||
jenkins_queue_id = Column(BigInteger, comment='Jenkins队列id')
|
||||
jenkins_build_number = Column(BigInteger, comment='Jenkins构建号')
|
||||
jenkins_build_url = Column(String(512), comment='Jenkins构建地址')
|
||||
console_url = Column(String(512), comment='控制台地址')
|
||||
report_url = Column(String(512), comment='报告地址')
|
||||
total_count = Column(Integer, default=0, comment='总数')
|
||||
pending_count = Column(Integer, default=0, comment='待执行数')
|
||||
running_count = Column(Integer, default=0, comment='执行中数')
|
||||
passed_count = Column(Integer, default=0, comment='通过数')
|
||||
failed_count = Column(Integer, default=0, comment='失败数')
|
||||
blocked_count = Column(Integer, default=0, comment='阻塞数')
|
||||
skipped_count = Column(Integer, default=0, comment='跳过数')
|
||||
not_found_count = Column(Integer, default=0, comment='未找到数')
|
||||
trigger_by = Column(BigInteger, comment='触发人')
|
||||
trigger_source = Column(String(32), server_default=text("'platform'"), comment='触发来源')
|
||||
trigger_message = Column(Text, comment='触发消息/失败原因')
|
||||
start_time = Column(TIMESTAMP, comment='开始时间')
|
||||
end_time = Column(TIMESTAMP, comment='结束时间')
|
||||
duration_seconds = Column(Integer, comment='耗时秒数')
|
||||
callback_token = Column(String(128), comment='回调token')
|
||||
ext = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展字段')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class AutoExecutionCase(Base):
|
||||
__tablename__ = 'auto_execution_case'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
execution_id = Column(BigInteger, nullable=False, comment='执行主单id')
|
||||
plan_case_id = Column(BigInteger, comment='计划用例id')
|
||||
case_id = Column(BigInteger, nullable=False, comment='用例id')
|
||||
case_key = Column(String(64), comment='用例编号快照')
|
||||
case_title = Column(String(255), comment='用例标题快照')
|
||||
run_order = Column(Integer, default=0, comment='执行顺序')
|
||||
status = Column(SmallInteger, nullable=False, default=0, comment='0:待执行 1:执行中 2:通过 3:失败 4:阻塞 5:跳过 6:未找到 7:已取消')
|
||||
pytest_nodeid = Column(String(512), comment='pytest节点标识')
|
||||
result_message = Column(Text, comment='结果摘要')
|
||||
error_message = Column(Text, comment='错误信息')
|
||||
stack_trace = Column(Text, comment='堆栈')
|
||||
report_url = Column(String(512), comment='单用例报告地址')
|
||||
duration_seconds = Column(Integer, comment='耗时秒数')
|
||||
started_time = Column(TIMESTAMP, comment='开始时间')
|
||||
finished_time = Column(TIMESTAMP, comment='结束时间')
|
||||
retry_count = Column(Integer, default=0, comment='重试次数')
|
||||
ext = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展字段')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
@@ -20,6 +20,8 @@ class TestPlan(Base):
|
||||
owner_id = Column(BigInteger, comment='负责人')
|
||||
status = Column(SmallInteger, default=0, comment='0:草稿 1:进行中 2:已完成 3:已归档 4:已通过')
|
||||
environment_id = Column(BigInteger, comment='环境id')
|
||||
jenkins_url = Column(String(512), comment='Jenkins构建URL')
|
||||
is_auto = Column(SmallInteger, default=0, comment='是否自动化测试计划:0-否,1-是')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
BIN
app/api/service/__pycache__/automationService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/automationService.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/api/service/__pycache__/jenkinsPollService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/jenkinsPollService.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
521
app/api/service/automationService.py
Normal file
521
app/api/service/automationService.py
Normal file
@@ -0,0 +1,521 @@
|
||||
# encoding: UTF-8
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
|
||||
from ..dao.automationDao import AutomationDao
|
||||
from ..model.automationModel import AutoExecution, AutoExecutionCase
|
||||
from ..model.planModel import PlanCase
|
||||
from ..service.planService import PlanService
|
||||
from common.jenkinsRequest import JenkinsRequest
|
||||
from const import JENKINS_DEFAULT_JOB, PLATFORM_BASE_URL
|
||||
from logger import logger
|
||||
|
||||
class AutomationService(object):
|
||||
STATUS_PENDING = 0
|
||||
STATUS_TRIGGERING = 1
|
||||
STATUS_QUEUED = 2
|
||||
STATUS_RUNNING = 3
|
||||
STATUS_SUCCESS = 4
|
||||
STATUS_FAILED = 5
|
||||
STATUS_CANCELED = 6
|
||||
STATUS_TRIGGER_FAILED = 7
|
||||
STATUS_CALLBACK_ERROR = 8
|
||||
|
||||
CASE_STATUS_PENDING = 0
|
||||
CASE_STATUS_RUNNING = 1
|
||||
CASE_STATUS_PASSED = 2
|
||||
CASE_STATUS_FAILED = 3
|
||||
CASE_STATUS_BLOCKED = 4
|
||||
CASE_STATUS_SKIPPED = 5
|
||||
CASE_STATUS_NOT_FOUND = 6
|
||||
CASE_STATUS_CANCELED = 7
|
||||
|
||||
PLAN_CASE_STATUS_MAP = {
|
||||
CASE_STATUS_PASSED: 1,
|
||||
CASE_STATUS_FAILED: 2,
|
||||
CASE_STATUS_BLOCKED: 3,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_execution_no():
|
||||
return 'AE' + datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3]
|
||||
|
||||
@staticmethod
|
||||
def generate_callback_token():
|
||||
return secrets.token_hex(16)
|
||||
|
||||
@staticmethod
|
||||
def create_case_execution(session, req_data, user_id):
|
||||
case_id = req_data.get('caseId') or req_data.get('case_id')
|
||||
env_code = req_data.get('envCode') or req_data.get('env_code')
|
||||
if not case_id or not env_code:
|
||||
return {}, 'caseId、envCode 为必传参数'
|
||||
case_item = AutomationDao.query_case_auto_item(session, case_id)
|
||||
if not case_item:
|
||||
return {}, '该用例不存在或未接入自动化'
|
||||
running_exists = AutomationService.get_running_execution_by_case(session, case_id, env_code)
|
||||
if running_exists:
|
||||
return {}, '该用例在当前环境已有执行中任务'
|
||||
callback_token = AutomationService.generate_callback_token()
|
||||
execution_obj, err_msg = AutomationDao.create_execution(session, {
|
||||
'execution_no': AutomationService.generate_execution_no(),
|
||||
'trigger_type': 1,
|
||||
'project_id': case_item.project_id,
|
||||
'source_case_id': case_item.id,
|
||||
'env_code': env_code,
|
||||
'run_mode': int(req_data.get('runMode') or req_data.get('run_mode') or 1),
|
||||
'status': AutomationService.STATUS_PENDING,
|
||||
'total_count': 1,
|
||||
'pending_count': 1,
|
||||
'running_count': 0,
|
||||
'passed_count': 0,
|
||||
'failed_count': 0,
|
||||
'blocked_count': 0,
|
||||
'skipped_count': 0,
|
||||
'not_found_count': 0,
|
||||
'trigger_by': user_id,
|
||||
'trigger_message': req_data.get('remark'),
|
||||
'callback_token': callback_token,
|
||||
'ext': {}
|
||||
})
|
||||
if err_msg:
|
||||
return {}, err_msg
|
||||
_, err_msg = AutomationDao.batch_create_execution_cases(session, [{
|
||||
'execution_id': execution_obj.id,
|
||||
'case_id': case_item.id,
|
||||
'case_key': case_item.case_key,
|
||||
'case_title': case_item.title,
|
||||
'run_order': 1,
|
||||
'status': AutomationService.CASE_STATUS_PENDING
|
||||
}])
|
||||
if err_msg:
|
||||
return {}, err_msg
|
||||
trigger_ok, trigger_msg = AutomationService.trigger_jenkins(session, execution_obj.id, req_data.get('jenkinsJobName'))
|
||||
if not trigger_ok:
|
||||
return {}, trigger_msg
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_obj.id)
|
||||
return execution.to_dict() if execution else {'id': execution_obj.id}, ''
|
||||
|
||||
@staticmethod
|
||||
def create_plan_execution(session, req_data, user_id):
|
||||
from logger import logger
|
||||
|
||||
plan_id = req_data.get('planId') or req_data.get('plan_id')
|
||||
env_code = req_data.get('envCode') or req_data.get('env_code')
|
||||
round_no = req_data.get('roundNo') or req_data.get('round_no')
|
||||
case_ids = req_data.get('caseIds') or req_data.get('case_ids') or []
|
||||
|
||||
logger.info(f'====== 计划自动化执行开始 ======')
|
||||
logger.info(f'请求参数: plan_id={plan_id}, env_code={env_code}, round_no={round_no}, case_ids={case_ids}, user_id={user_id}')
|
||||
|
||||
if not plan_id or not env_code:
|
||||
logger.error('参数校验失败: planId、envCode 为必传参数')
|
||||
return {}, 'planId、envCode 为必传参数'
|
||||
|
||||
running_exists = AutomationService.get_running_execution_by_plan(session, plan_id, env_code)
|
||||
if running_exists:
|
||||
logger.error(f'计划执行冲突: 计划 {plan_id} 在环境 {env_code} 已有执行中任务')
|
||||
return {}, '该计划在当前环境已有执行中任务'
|
||||
|
||||
logger.info(f'查询计划自动化用例: plan_id={plan_id}, round_no={round_no}')
|
||||
items = AutomationDao.query_plan_auto_cases(session, plan_id, round_no, case_ids)
|
||||
if not items:
|
||||
logger.error('计划下无可执行自动化用例')
|
||||
return {}, '计划下无可执行自动化用例'
|
||||
|
||||
logger.info(f'查询到 {len(items)} 条自动化用例')
|
||||
for idx, (plan_case, case_item) in enumerate(items, start=1):
|
||||
logger.info(f' {idx}. case_key={case_item.case_key}, case_title={case_item.title}')
|
||||
|
||||
project_id = items[0][1].project_id
|
||||
callback_token = AutomationService.generate_callback_token()
|
||||
execution_no = AutomationService.generate_execution_no()
|
||||
|
||||
logger.info(f'创建执行记录: execution_no={execution_no}, project_id={project_id}, plan_id={plan_id}')
|
||||
execution_obj, err_msg = AutomationDao.create_execution(session, {
|
||||
'execution_no': execution_no,
|
||||
'trigger_type': 2,
|
||||
'project_id': project_id,
|
||||
'plan_id': int(plan_id),
|
||||
'plan_round_no': int(round_no) if round_no not in (None, '') else None,
|
||||
'env_code': env_code,
|
||||
'run_mode': int(req_data.get('runMode') or req_data.get('run_mode') or 1),
|
||||
'status': AutomationService.STATUS_PENDING,
|
||||
'total_count': len(items),
|
||||
'pending_count': len(items),
|
||||
'running_count': 0,
|
||||
'passed_count': 0,
|
||||
'failed_count': 0,
|
||||
'blocked_count': 0,
|
||||
'skipped_count': 0,
|
||||
'not_found_count': 0,
|
||||
'trigger_by': user_id,
|
||||
'trigger_message': req_data.get('remark'),
|
||||
'callback_token': callback_token,
|
||||
'ext': {}
|
||||
})
|
||||
if err_msg:
|
||||
logger.error(f'创建执行记录失败: {err_msg}')
|
||||
return {}, err_msg
|
||||
|
||||
logger.info(f'执行记录创建成功: execution_id={execution_obj.id}')
|
||||
|
||||
batch_list = []
|
||||
for idx, (plan_case, case_item) in enumerate(items, start=1):
|
||||
batch_list.append({
|
||||
'execution_id': execution_obj.id,
|
||||
'plan_case_id': plan_case.id,
|
||||
'case_id': case_item.id,
|
||||
'case_key': case_item.case_key,
|
||||
'case_title': case_item.title,
|
||||
'run_order': idx,
|
||||
'status': AutomationService.CASE_STATUS_PENDING
|
||||
})
|
||||
|
||||
logger.info(f'批量创建执行明细: {len(batch_list)} 条')
|
||||
_, err_msg = AutomationDao.batch_create_execution_cases(session, batch_list)
|
||||
if err_msg:
|
||||
logger.error(f'批量创建执行明细失败: {err_msg}')
|
||||
return {}, err_msg
|
||||
|
||||
logger.info(f'触发Jenkins构建: execution_id={execution_obj.id}')
|
||||
trigger_ok, trigger_msg = AutomationService.trigger_jenkins(session, execution_obj.id, req_data.get('jenkinsJobName'))
|
||||
if not trigger_ok:
|
||||
logger.error(f'Jenkins触发失败: {trigger_msg}')
|
||||
return {}, trigger_msg
|
||||
|
||||
logger.info('计划自动化执行成功')
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_obj.id)
|
||||
logger.info(f'====== 计划自动化执行结束 ======')
|
||||
return execution.to_dict() if execution else {'id': execution_obj.id}, ''
|
||||
|
||||
@staticmethod
|
||||
def trigger_jenkins(session, execution_id, job_name=None):
|
||||
|
||||
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return False, '未查询到对应执行记录'
|
||||
AutomationDao.update_execution_by_id(session, execution_id, {'status': AutomationService.STATUS_TRIGGERING})
|
||||
|
||||
cases, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id)], 1, 100000)
|
||||
case_keys = [case.case_key for case in cases if case.case_key]
|
||||
test_target = ','.join(case_keys)
|
||||
|
||||
test_type = 'story' if case_keys else 'all'
|
||||
|
||||
params = {
|
||||
'EXECUTION_ID': execution.id,
|
||||
'CALLBACK_TOKEN': execution.callback_token,
|
||||
'PLATFORM_BASE_URL': PLATFORM_BASE_URL,
|
||||
'ENV_CODE': execution.env_code,
|
||||
'RUN_MODE': execution.run_mode,
|
||||
'TRIGGER_TYPE': execution.trigger_type,
|
||||
'TEST_TYPE': test_type,
|
||||
'TEST_TARGET': test_target
|
||||
}
|
||||
|
||||
jenkins_url = None
|
||||
jenkins_job_name = None
|
||||
if execution.plan_id:
|
||||
plan = AutomationDao.get_plan_by_id(session, execution.plan_id)
|
||||
if plan and plan.jenkins_url:
|
||||
jenkins_url = plan.jenkins_url
|
||||
if '/job/' in jenkins_url:
|
||||
import re
|
||||
match = re.match(r'^(https?://[^/]+)/(job/[^/]+)/?.*$', jenkins_url)
|
||||
if match:
|
||||
jenkins_url = match.group(1)
|
||||
jenkins_job_name = match.group(2).replace('job/', '')
|
||||
logger.info(f'从计划配置中解析 Jenkins: base_url={jenkins_url}, job_name={jenkins_job_name}')
|
||||
|
||||
jenkins_request = JenkinsRequest(jenkins_url=jenkins_url)
|
||||
target_job_name = jenkins_job_name or job_name or execution.jenkins_job_name or JENKINS_DEFAULT_JOB
|
||||
success, err_msg, payload = jenkins_request.build_with_parameters(params, target_job_name)
|
||||
if not success:
|
||||
AutomationDao.update_execution_by_id(session, execution_id, {
|
||||
'status': AutomationService.STATUS_TRIGGER_FAILED,
|
||||
'trigger_message': err_msg
|
||||
})
|
||||
return False, err_msg
|
||||
update_info = {
|
||||
'status': AutomationService.STATUS_QUEUED,
|
||||
'jenkins_job_name': payload.get('job_name') or job_name or JENKINS_DEFAULT_JOB,
|
||||
'jenkins_queue_id': payload.get('queue_id')
|
||||
}
|
||||
if payload.get('location'):
|
||||
update_info['trigger_message'] = payload.get('location')
|
||||
AutomationDao.update_execution_by_id(session, execution_id, update_info)
|
||||
return True, ''
|
||||
|
||||
@staticmethod
|
||||
def get_running_execution_by_case(session, case_id, env_code):
|
||||
items, _ = AutomationDao.list_execution_by_filters(session, [
|
||||
AutoExecution.source_case_id == int(case_id),
|
||||
AutoExecution.env_code == env_code,
|
||||
AutoExecution.status.in_([0, 1, 2, 3])
|
||||
], 1, 1)
|
||||
return items[0] if items else None
|
||||
|
||||
@staticmethod
|
||||
def get_running_execution_by_plan(session, plan_id, env_code):
|
||||
items, _ = AutomationDao.list_execution_by_filters(session, [
|
||||
AutoExecution.plan_id == int(plan_id),
|
||||
AutoExecution.env_code == env_code,
|
||||
AutoExecution.status.in_([0, 1, 2, 3])
|
||||
], 1, 1)
|
||||
return items[0] if items else None
|
||||
|
||||
@staticmethod
|
||||
def list_executions(session, req_data):
|
||||
filters = []
|
||||
project_id = req_data.get('projectId') or req_data.get('project_id')
|
||||
plan_id = req_data.get('planId') or req_data.get('plan_id')
|
||||
status = req_data.get('status')
|
||||
trigger_type = req_data.get('triggerType') or req_data.get('trigger_type')
|
||||
if project_id:
|
||||
filters.append(AutoExecution.project_id == int(project_id))
|
||||
if plan_id:
|
||||
filters.append(AutoExecution.plan_id == int(plan_id))
|
||||
if status not in (None, ''):
|
||||
filters.append(AutoExecution.status == int(status))
|
||||
if trigger_type not in (None, ''):
|
||||
filters.append(AutoExecution.trigger_type == int(trigger_type))
|
||||
items, total = AutomationDao.list_execution_by_filters(session, filters, req_data.get('pageNo') or req_data.get('page') or 1, req_data.get('pageSize') or req_data.get('size') or 20)
|
||||
return {'list': [item.to_dict() for item in items], 'total': total}
|
||||
|
||||
@staticmethod
|
||||
def get_execution_detail(session, execution_id):
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return {}, '未查询到对应执行记录'
|
||||
ret = execution.to_dict()
|
||||
summary = AutomationDao.count_execution_case_summary(session, execution_id)
|
||||
ret.update({
|
||||
'summary': {
|
||||
'total': summary.get('total', 0),
|
||||
'pending': summary.get(0, 0),
|
||||
'running': summary.get(1, 0),
|
||||
'passed': summary.get(2, 0),
|
||||
'failed': summary.get(3, 0),
|
||||
'blocked': summary.get(4, 0),
|
||||
'skipped': summary.get(5, 0),
|
||||
'notFound': summary.get(6, 0),
|
||||
'canceled': summary.get(7, 0)
|
||||
}
|
||||
})
|
||||
return ret, ''
|
||||
|
||||
@staticmethod
|
||||
def list_execution_cases(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
if not execution_id:
|
||||
return {}, 'executionId 为必传参数'
|
||||
filters = [AutoExecutionCase.execution_id == int(execution_id)]
|
||||
status = req_data.get('status')
|
||||
if status not in (None, ''):
|
||||
filters.append(AutoExecutionCase.status == int(status))
|
||||
items, total = AutomationDao.list_execution_case_by_filters(session, filters, req_data.get('pageNo') or req_data.get('page') or 1, req_data.get('pageSize') or req_data.get('size') or 20)
|
||||
return {'list': [item.to_dict() for item in items], 'total': total}, ''
|
||||
|
||||
@staticmethod
|
||||
def pull_execution_cases(session, execution_id, callback_token):
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return {}, '未查询到对应执行记录'
|
||||
if execution.callback_token != callback_token:
|
||||
return {}, '回调鉴权失败'
|
||||
case_items, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id)], 1, 100000)
|
||||
return {
|
||||
'executionId': execution.id,
|
||||
'executionNo': execution.execution_no,
|
||||
'triggerType': execution.trigger_type,
|
||||
'projectId': execution.project_id,
|
||||
'planId': execution.plan_id,
|
||||
'envCode': execution.env_code,
|
||||
'runMode': execution.run_mode,
|
||||
'items': [{
|
||||
'executionCaseId': item.id,
|
||||
'planCaseId': item.plan_case_id,
|
||||
'caseId': item.case_id,
|
||||
'caseKey': item.case_key,
|
||||
'caseTitle': item.case_title,
|
||||
'runOrder': item.run_order
|
||||
} for item in case_items]
|
||||
}, ''
|
||||
|
||||
@staticmethod
|
||||
def mark_execution_queued(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return 0, '未查询到对应执行记录'
|
||||
return AutomationDao.update_execution_by_id(session, execution_id, {
|
||||
'status': AutomationService.STATUS_QUEUED,
|
||||
'jenkins_queue_id': req_data.get('queueId') or req_data.get('queue_id'),
|
||||
'jenkins_job_name': req_data.get('jobName') or req_data.get('job_name') or execution.jenkins_job_name,
|
||||
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number'),
|
||||
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url')
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def mark_execution_started(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return 0, '未查询到对应执行记录'
|
||||
start_time = req_data.get('startTime') or req_data.get('start_time') or datetime.now()
|
||||
return AutomationDao.update_execution_by_id(session, execution_id, {
|
||||
'status': AutomationService.STATUS_RUNNING,
|
||||
'jenkins_job_name': req_data.get('jobName') or req_data.get('job_name') or execution.jenkins_job_name,
|
||||
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number'),
|
||||
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url'),
|
||||
'console_url': req_data.get('consoleUrl') or req_data.get('console_url'),
|
||||
'start_time': start_time
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def save_case_result(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
execution_case_id = req_data.get('executionCaseId') or req_data.get('execution_case_id')
|
||||
case_id = req_data.get('caseId') or req_data.get('case_id')
|
||||
if not execution_id or (not execution_case_id and not case_id):
|
||||
return 0, 'executionId、executionCaseId/caseId 为必传参数'
|
||||
execution_case = AutomationDao.get_execution_case_by_id(session, execution_case_id) if execution_case_id else None
|
||||
if not execution_case and case_id:
|
||||
execution_case = AutomationDao.get_execution_case_by_unique(session, execution_id, case_id, req_data.get('planCaseId') or req_data.get('plan_case_id'))
|
||||
if not execution_case:
|
||||
return 0, '未查询到对应执行明细'
|
||||
update_info = {
|
||||
'status': int(req_data.get('status')) if req_data.get('status') is not None else execution_case.status,
|
||||
'pytest_nodeid': req_data.get('pytestNodeid') or req_data.get('pytest_nodeid'),
|
||||
'result_message': req_data.get('resultMessage') or req_data.get('result_message'),
|
||||
'error_message': req_data.get('errorMessage') or req_data.get('error_message'),
|
||||
'stack_trace': req_data.get('stackTrace') or req_data.get('stack_trace'),
|
||||
'report_url': req_data.get('reportUrl') or req_data.get('report_url'),
|
||||
'duration_seconds': req_data.get('durationSeconds') or req_data.get('duration_seconds'),
|
||||
'started_time': req_data.get('startedTime') or req_data.get('started_time') or execution_case.started_time,
|
||||
'finished_time': req_data.get('finishedTime') or req_data.get('finished_time') or datetime.now(),
|
||||
'ext': req_data.get('ext') if req_data.get('ext') is not None else execution_case.ext
|
||||
}
|
||||
update_id, err_msg = AutomationDao.update_execution_case_by_id(session, execution_case.id, update_info)
|
||||
if err_msg:
|
||||
return update_id, err_msg
|
||||
execution_case = AutomationDao.get_execution_case_by_id(session, execution_case.id)
|
||||
if execution_case and execution_case.plan_case_id:
|
||||
AutomationService.sync_plan_case_result(session, execution_case)
|
||||
AutomationService.refresh_execution_summary(session, execution_id)
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if execution and execution.plan_id:
|
||||
AutomationService.refresh_plan_status(session, execution.plan_id)
|
||||
return execution_case.id, ''
|
||||
|
||||
@staticmethod
|
||||
def finish_execution(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return 0, '未查询到对应执行记录'
|
||||
end_time = req_data.get('endTime') or req_data.get('end_time') or datetime.now()
|
||||
start_time = req_data.get('startTime') or req_data.get('start_time') or execution.start_time
|
||||
update_info = {
|
||||
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number') or execution.jenkins_build_number,
|
||||
'jenkins_build_url': req_data.get('buildUrl') or req_data.get('build_url') or execution.jenkins_build_url,
|
||||
'console_url': req_data.get('consoleUrl') or req_data.get('console_url') or execution.console_url,
|
||||
'report_url': req_data.get('reportUrl') or req_data.get('report_url') or execution.report_url,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'duration_seconds': req_data.get('durationSeconds') or req_data.get('duration_seconds') or AutomationService.calc_duration_seconds(start_time, end_time)
|
||||
}
|
||||
update_id, err_msg = AutomationDao.update_execution_by_id(session, execution_id, update_info)
|
||||
if err_msg:
|
||||
return update_id, err_msg
|
||||
AutomationService.refresh_execution_summary(session, execution_id, force_finish=True)
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if execution and execution.plan_id:
|
||||
AutomationService.refresh_plan_status(session, execution.plan_id)
|
||||
return int(execution_id), ''
|
||||
|
||||
@staticmethod
|
||||
def abort_execution(session, req_data):
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return 0, '未查询到对应执行记录'
|
||||
case_items, _ = AutomationDao.list_execution_case_by_filters(session, [AutoExecutionCase.execution_id == int(execution_id), AutoExecutionCase.status.in_([0, 1])], 1, 100000)
|
||||
for item in case_items:
|
||||
AutomationDao.update_execution_case_by_id(session, item.id, {'status': AutomationService.CASE_STATUS_CANCELED, 'finished_time': datetime.now()})
|
||||
update_id, err_msg = AutomationDao.update_execution_by_id(session, execution_id, {
|
||||
'status': int(req_data.get('status') or AutomationService.STATUS_CANCELED),
|
||||
'trigger_message': req_data.get('message') or req_data.get('trigger_message'),
|
||||
'jenkins_build_number': req_data.get('buildNumber') or req_data.get('build_number') or execution.jenkins_build_number,
|
||||
'console_url': req_data.get('consoleUrl') or req_data.get('console_url') or execution.console_url,
|
||||
'end_time': datetime.now()
|
||||
})
|
||||
if err_msg:
|
||||
return update_id, err_msg
|
||||
AutomationService.refresh_execution_summary(session, execution_id, keep_terminal_status=True)
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if execution and execution.plan_id:
|
||||
AutomationService.refresh_plan_status(session, execution.plan_id)
|
||||
return int(execution_id), ''
|
||||
|
||||
@staticmethod
|
||||
def sync_plan_case_result(session, execution_case):
|
||||
status = AutomationService.PLAN_CASE_STATUS_MAP.get(execution_case.status)
|
||||
update_info = {
|
||||
'actual_result': execution_case.error_message or execution_case.result_message,
|
||||
'executed_time': execution_case.finished_time or datetime.now(),
|
||||
'execution_duration': execution_case.duration_seconds
|
||||
}
|
||||
if status is not None:
|
||||
update_info['status'] = status
|
||||
AutomationDao.update_plan_case_result(session, execution_case.plan_case_id, update_info)
|
||||
|
||||
@staticmethod
|
||||
def refresh_execution_summary(session, execution_id, force_finish=False, keep_terminal_status=False):
|
||||
summary = AutomationDao.count_execution_case_summary(session, execution_id)
|
||||
execution = AutomationDao.get_execution_by_id(session, execution_id)
|
||||
if not execution:
|
||||
return
|
||||
update_info = {
|
||||
'total_count': summary.get('total', 0),
|
||||
'pending_count': summary.get(0, 0),
|
||||
'running_count': summary.get(1, 0),
|
||||
'passed_count': summary.get(2, 0),
|
||||
'failed_count': summary.get(3, 0),
|
||||
'blocked_count': summary.get(4, 0),
|
||||
'skipped_count': summary.get(5, 0),
|
||||
'not_found_count': summary.get(6, 0)
|
||||
}
|
||||
total = summary.get('total', 0)
|
||||
running_count = summary.get(1, 0)
|
||||
finished_count = summary.get(2, 0) + summary.get(3, 0) + summary.get(4, 0) + summary.get(5, 0) + summary.get(6, 0) + summary.get(7, 0)
|
||||
if not keep_terminal_status:
|
||||
if running_count > 0:
|
||||
update_info['status'] = AutomationService.STATUS_RUNNING
|
||||
elif total > 0 and finished_count == total:
|
||||
if summary.get(3, 0) + summary.get(4, 0) + summary.get(6, 0) > 0:
|
||||
update_info['status'] = AutomationService.STATUS_FAILED
|
||||
else:
|
||||
update_info['status'] = AutomationService.STATUS_SUCCESS
|
||||
if force_finish or (total > 0 and finished_count == total):
|
||||
end_time = execution.end_time or datetime.now()
|
||||
update_info['end_time'] = end_time
|
||||
if execution.start_time:
|
||||
update_info['duration_seconds'] = AutomationService.calc_duration_seconds(execution.start_time, end_time)
|
||||
AutomationDao.update_execution_by_id(session, execution_id, update_info)
|
||||
|
||||
@staticmethod
|
||||
def refresh_plan_status(session, plan_id):
|
||||
PlanService.refresh_plan_status(session, plan_id)
|
||||
|
||||
@staticmethod
|
||||
def calc_duration_seconds(start_time, end_time):
|
||||
if not start_time or not end_time:
|
||||
return None
|
||||
if isinstance(start_time, str):
|
||||
start_time = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(end_time, str):
|
||||
end_time = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S')
|
||||
return int((end_time - start_time).total_seconds())
|
||||
@@ -26,8 +26,8 @@ class CaseService(object):
|
||||
return CaseDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def next_case_key(session, project_id):
|
||||
return CaseDao.next_case_key(session, project_id)
|
||||
def next_case_key(session, project_id, module_id=None, product_id=None):
|
||||
return CaseDao.next_case_key(session, project_id, module_id, product_id)
|
||||
|
||||
@staticmethod
|
||||
def next_snapshot_version(session, case_id):
|
||||
|
||||
302
app/api/service/jenkinsPollService.py
Normal file
302
app/api/service/jenkinsPollService.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# encoding: UTF-8
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from const import JENKINS_BASE_URL, JENKINS_USER, JENKINS_TOKEN
|
||||
from logger import logger
|
||||
from app.api.model.automationModel import AutoExecution, AutoExecutionCase
|
||||
|
||||
|
||||
class JenkinsPollService(object):
|
||||
STATUS_QUEUED = 2
|
||||
STATUS_RUNNING = 3
|
||||
STATUS_SUCCESS = 4
|
||||
STATUS_FAILED = 5
|
||||
|
||||
@staticmethod
|
||||
def poll_jenkins_build_status(session, execution_id):
|
||||
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
|
||||
if not execution:
|
||||
logger.error(f'执行记录不存在: execution_id={execution_id}')
|
||||
return False, '执行记录不存在'
|
||||
|
||||
if execution.status not in [JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING]:
|
||||
logger.info(f'执行状态不需要轮询: execution_id={execution_id}, status={execution.status}')
|
||||
return True, ''
|
||||
|
||||
base_url = JENKINS_BASE_URL.rstrip('/')
|
||||
job_name = execution.jenkins_job_name
|
||||
build_number = execution.jenkins_build_number
|
||||
|
||||
if not job_name:
|
||||
if execution.jenkins_build_url:
|
||||
import re
|
||||
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/\d+/', execution.jenkins_build_url)
|
||||
if match:
|
||||
job_name = match.group(1).replace('/job/', '/')
|
||||
logger.info(f'从构建URL中提取job_name: {job_name}')
|
||||
else:
|
||||
logger.error(f'无法从构建URL中提取job_name: {execution.jenkins_build_url}')
|
||||
return False, 'Jenkins job 名称为空'
|
||||
else:
|
||||
logger.error(f'Jenkins job 名称为空: execution_id={execution_id}')
|
||||
return False, 'Jenkins job 名称为空'
|
||||
|
||||
auth = HTTPBasicAuth(JENKINS_USER, JENKINS_TOKEN) if JENKINS_USER and JENKINS_TOKEN else None
|
||||
|
||||
try:
|
||||
if not build_number:
|
||||
if execution.jenkins_build_url:
|
||||
import re
|
||||
match = re.search(r'/job/([^/]+(?:/job/[^/]+)*)/(\d+)/', execution.jenkins_build_url)
|
||||
if match:
|
||||
job_name = match.group(1).replace('/job/', '/')
|
||||
build_number = match.group(2)
|
||||
logger.info(f'从构建URL中提取: job_name={job_name}, build_number={build_number}')
|
||||
else:
|
||||
logger.error(f'无法从构建URL中提取信息: {execution.jenkins_build_url}')
|
||||
|
||||
queue_id = execution.jenkins_queue_id
|
||||
if queue_id:
|
||||
queue_url = f'{base_url}/queue/item/{queue_id}/api/json'
|
||||
response = requests.get(queue_url, auth=auth, timeout=30)
|
||||
if response.status_code == 200:
|
||||
queue_data = response.json()
|
||||
logger.debug(f'队列数据: execution_id={execution_id}, queue_data={json.dumps(queue_data, ensure_ascii=False)[:500]}')
|
||||
|
||||
if queue_data.get('executable'):
|
||||
build_number = queue_data['executable'].get('number')
|
||||
logger.info(f'队列任务已开始执行: execution_id={execution_id}, build_number={build_number}')
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
||||
'jenkins_build_number': build_number,
|
||||
'status': JenkinsPollService.STATUS_RUNNING,
|
||||
'start_time': datetime.now()
|
||||
})
|
||||
session.done(close=False)
|
||||
elif queue_data.get('cancelled') or queue_data.get('blocked'):
|
||||
logger.error(f'队列任务已取消或阻塞: execution_id={execution_id}, cancelled={queue_data.get("cancelled")}, blocked={queue_data.get("blocked")}')
|
||||
end_time = datetime.now()
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
||||
'status': JenkinsPollService.STATUS_FAILED,
|
||||
'end_time': end_time,
|
||||
'trigger_message': queue_data.get('why', '队列任务已取消或阻塞')
|
||||
})
|
||||
session.done(close=False)
|
||||
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
||||
if execution.plan_id:
|
||||
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
||||
return True, '队列任务已取消或阻塞'
|
||||
elif queue_data.get('why'):
|
||||
logger.info(f'队列任务等待中: execution_id={execution_id}, reason={queue_data.get("why")}')
|
||||
return True, f'队列等待中: {queue_data.get("why")}'
|
||||
else:
|
||||
logger.info(f'队列任务等待中: execution_id={execution_id}, queue_id={queue_id}')
|
||||
return True, '队列等待中'
|
||||
else:
|
||||
logger.warning(f'获取队列状态失败: execution_id={execution_id}, status_code={response.status_code}')
|
||||
|
||||
if response.status_code == 404:
|
||||
logger.info(f'队列项已不存在,尝试查询执行状态: execution_id={execution_id}')
|
||||
builds_url = f'{base_url}/job/{job_name}/builds/api/json?limit=10'
|
||||
try:
|
||||
builds_response = requests.get(builds_url, auth=auth, timeout=30)
|
||||
logger.info(f'构建历史查询: url={builds_url}, status_code={builds_response.status_code}')
|
||||
|
||||
if builds_response.status_code == 200:
|
||||
builds_data = builds_response.json()
|
||||
logger.info(f'构建历史数据: count={len(builds_data) if builds_data else 0}')
|
||||
|
||||
if builds_data:
|
||||
latest_build = builds_data[0]
|
||||
build_number = latest_build.get('number')
|
||||
is_building = latest_build.get('building', False)
|
||||
result = latest_build.get('result')
|
||||
timestamp = latest_build.get('timestamp', 0)
|
||||
|
||||
logger.info(f'最新构建信息: build_number={build_number}, is_building={is_building}, result={result}')
|
||||
|
||||
if is_building:
|
||||
status = JenkinsPollService.STATUS_RUNNING
|
||||
elif result == 'SUCCESS':
|
||||
status = JenkinsPollService.STATUS_SUCCESS
|
||||
else:
|
||||
status = JenkinsPollService.STATUS_FAILED
|
||||
|
||||
logger.info(f'更新执行状态: execution_id={execution_id}, build_number={build_number}, status={status}')
|
||||
update_info = {
|
||||
'jenkins_build_number': build_number,
|
||||
'status': status,
|
||||
'start_time': datetime.fromtimestamp(timestamp/1000) if timestamp else datetime.now()
|
||||
}
|
||||
|
||||
if not is_building and result:
|
||||
update_info['end_time'] = datetime.now()
|
||||
update_info['jenkins_build_url'] = f'{base_url}/job/{job_name}/{build_number}/'
|
||||
update_info['console_url'] = f'{base_url}/job/{job_name}/{build_number}/console'
|
||||
update_info['report_url'] = f'{base_url}/job/{job_name}/{build_number}/allure/'
|
||||
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
||||
session.done(close=False)
|
||||
|
||||
if not is_building:
|
||||
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
||||
if execution.plan_id:
|
||||
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
||||
|
||||
return True, f'队列不存在,使用最新构建: {build_number}'
|
||||
else:
|
||||
logger.error(f'获取构建历史失败: status_code={builds_response.status_code}, body={builds_response.text[:200]}')
|
||||
except Exception as err:
|
||||
logger.error(f'查询构建历史异常: {err}')
|
||||
|
||||
return True, '获取队列状态失败'
|
||||
else:
|
||||
logger.warning(f'缺少 queue_id 和 build_number: execution_id={execution_id}')
|
||||
return False, '无法轮询,缺少构建信息'
|
||||
|
||||
if build_number:
|
||||
build_url = f'{base_url}/job/{job_name}/{build_number}/api/json'
|
||||
response = requests.get(build_url, auth=auth, timeout=30)
|
||||
if response.status_code == 200:
|
||||
build_data = response.json()
|
||||
is_running = build_data.get('building', False)
|
||||
result = build_data.get('result')
|
||||
|
||||
console_url = f'{base_url}/job/{job_name}/{build_number}/console'
|
||||
build_url_full = f'{base_url}/job/{job_name}/{build_number}/'
|
||||
|
||||
if is_running:
|
||||
logger.info(f'构建执行中: execution_id={execution_id}, build_number={build_number}')
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update({
|
||||
'status': JenkinsPollService.STATUS_RUNNING,
|
||||
'jenkins_build_url': build_url_full,
|
||||
'console_url': console_url
|
||||
})
|
||||
session.done(close=False)
|
||||
return True, '执行中'
|
||||
else:
|
||||
logger.info(f'构建完成: execution_id={execution_id}, result={result}')
|
||||
end_time = datetime.now()
|
||||
report_url = f'{base_url}/job/{job_name}/{build_number}/allure/'
|
||||
update_info = {
|
||||
'status': JenkinsPollService.STATUS_SUCCESS if result == 'SUCCESS' else JenkinsPollService.STATUS_FAILED,
|
||||
'jenkins_build_url': build_url_full,
|
||||
'console_url': console_url,
|
||||
'report_url': report_url,
|
||||
'end_time': end_time
|
||||
}
|
||||
if execution.start_time:
|
||||
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
||||
session.done(close=False)
|
||||
|
||||
JenkinsPollService.refresh_execution_summary(session, execution_id, force_finish=True)
|
||||
if execution.plan_id:
|
||||
JenkinsPollService.refresh_plan_status(session, execution.plan_id)
|
||||
|
||||
return True, f'构建完成: {result}'
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f'轮询 Jenkins 状态失败: execution_id={execution_id}, error={err}')
|
||||
return False, str(err)
|
||||
|
||||
return True, ''
|
||||
|
||||
@staticmethod
|
||||
def refresh_execution_summary(session, execution_id, force_finish=False):
|
||||
from sqlalchemy import func
|
||||
|
||||
rows = session.query(AutoExecutionCase.status, func.count(AutoExecutionCase.id)).filter(
|
||||
AutoExecutionCase.execution_id == int(execution_id)
|
||||
).group_by(AutoExecutionCase.status).all()
|
||||
|
||||
summary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
|
||||
for status, count in rows:
|
||||
summary[int(status)] = int(count)
|
||||
total = sum(summary.values())
|
||||
|
||||
execution = session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).first()
|
||||
if execution:
|
||||
update_info = {
|
||||
'total_count': total,
|
||||
'pending_count': summary.get(0, 0),
|
||||
'running_count': summary.get(1, 0),
|
||||
'passed_count': summary.get(2, 0),
|
||||
'failed_count': summary.get(3, 0),
|
||||
'blocked_count': summary.get(4, 0),
|
||||
'skipped_count': summary.get(5, 0),
|
||||
'not_found_count': summary.get(6, 0)
|
||||
}
|
||||
|
||||
running_count = summary.get(1, 0)
|
||||
finished_count = summary.get(2, 0) + summary.get(3, 0) + summary.get(4, 0) + summary.get(5, 0) + summary.get(6, 0) + summary.get(7, 0)
|
||||
|
||||
if running_count > 0:
|
||||
update_info['status'] = JenkinsPollService.STATUS_RUNNING
|
||||
elif total > 0 and finished_count == total:
|
||||
if summary.get(3, 0) + summary.get(4, 0) + summary.get(6, 0) > 0:
|
||||
update_info['status'] = JenkinsPollService.STATUS_FAILED
|
||||
else:
|
||||
update_info['status'] = JenkinsPollService.STATUS_SUCCESS
|
||||
|
||||
if force_finish or (total > 0 and finished_count == total):
|
||||
end_time = execution.end_time or datetime.now()
|
||||
update_info['end_time'] = end_time
|
||||
if execution.start_time:
|
||||
update_info['duration_seconds'] = int((end_time - execution.start_time).total_seconds())
|
||||
|
||||
session.query(AutoExecution).filter(AutoExecution.id == int(execution_id)).update(update_info)
|
||||
session.done(close=False)
|
||||
|
||||
@staticmethod
|
||||
def refresh_plan_status(session, plan_id):
|
||||
from sqlalchemy import func
|
||||
|
||||
rows = session.query(
|
||||
AutoExecution.status, func.count(AutoExecution.id)
|
||||
).filter(
|
||||
AutoExecution.plan_id == int(plan_id),
|
||||
AutoExecution.status.in_([JenkinsPollService.STATUS_RUNNING, JenkinsPollService.STATUS_SUCCESS, JenkinsPollService.STATUS_FAILED])
|
||||
).group_by(AutoExecution.status).all()
|
||||
|
||||
status_counts = {}
|
||||
for status, count in rows:
|
||||
status_counts[status] = count
|
||||
|
||||
running_count = status_counts.get(JenkinsPollService.STATUS_RUNNING, 0)
|
||||
success_count = status_counts.get(JenkinsPollService.STATUS_SUCCESS, 0)
|
||||
failed_count = status_counts.get(JenkinsPollService.STATUS_FAILED, 0)
|
||||
|
||||
from app.api.model.planModel import TestPlan
|
||||
|
||||
if running_count > 0:
|
||||
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 1})
|
||||
elif success_count > 0 and failed_count == 0:
|
||||
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 4})
|
||||
elif success_count + failed_count > 0:
|
||||
session.query(TestPlan).filter(TestPlan.id == int(plan_id)).update({'status': 2})
|
||||
|
||||
session.done(close=False)
|
||||
|
||||
@staticmethod
|
||||
def poll_all_pending_executions(session):
|
||||
pending_executions = session.query(AutoExecution).filter(
|
||||
AutoExecution.status.in_([JenkinsPollService.STATUS_QUEUED, JenkinsPollService.STATUS_RUNNING])
|
||||
).all()
|
||||
|
||||
for execution in pending_executions:
|
||||
try:
|
||||
success, msg = JenkinsPollService.poll_jenkins_build_status(session, execution.id)
|
||||
logger.info(f'轮询执行 {execution.id}: success={success}, msg={msg}')
|
||||
except Exception as err:
|
||||
logger.error(f'轮询执行 {execution.id} 异常: {err}')
|
||||
|
||||
session.done(close=False)
|
||||
@@ -1,5 +1,6 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.planDao import PlanDao
|
||||
from ..model.planModel import PlanCase, TestPlan
|
||||
|
||||
|
||||
class PlanService(object):
|
||||
@@ -32,3 +33,22 @@ class PlanService(object):
|
||||
@staticmethod
|
||||
def plan_stats(session, plan_id):
|
||||
return PlanDao.plan_stats(session, plan_id)
|
||||
|
||||
@staticmethod
|
||||
def refresh_plan_status(session, plan_id):
|
||||
total = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id)).count()
|
||||
if total == 0:
|
||||
return
|
||||
unexecuted_count = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 0).count()
|
||||
failed_count = session.query(PlanCase).filter(PlanCase.plan_id == int(plan_id), PlanCase.status.in_([2, 3])).count()
|
||||
plan = PlanDao.get_by_id(session, TestPlan, plan_id)
|
||||
if not plan or plan.status == 3:
|
||||
return
|
||||
if unexecuted_count == 0:
|
||||
new_status = 4 if failed_count == 0 else 2
|
||||
elif unexecuted_count < total:
|
||||
new_status = 1
|
||||
else:
|
||||
new_status = plan.status
|
||||
if new_status != plan.status:
|
||||
PlanDao.update_by_id(session, TestPlan, plan_id, {'status': new_status})
|
||||
|
||||
@@ -59,6 +59,14 @@ class RbacService(object):
|
||||
def get_role_menu_ids(session, role_id):
|
||||
return RbacDao.get_role_menu_ids(session, role_id)
|
||||
|
||||
@staticmethod
|
||||
def get_menu_permission_codes(session, menu_ids):
|
||||
return RbacDao.get_menu_permission_codes(session, menu_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_permission_ids_by_codes(session, permission_codes):
|
||||
return RbacDao.get_permission_ids_by_codes(session, permission_codes)
|
||||
|
||||
@staticmethod
|
||||
def build_menu_tree(session, filters, role_ids=None, menu_ids=None):
|
||||
items = RbacDao.get_menu_tree_items(session, filters)
|
||||
|
||||
Binary file not shown.
@@ -16,10 +16,22 @@ from common.sqlSession import SqlSession
|
||||
|
||||
TOKEN_PREFIX = 'effekt:token:'
|
||||
TOKEN_CONTEXT_PREFIX = 'effekt:token:ctx:'
|
||||
REFRESH_TOKEN_PREFIX = 'effekt:refresh:'
|
||||
TOKEN_EXPIRE_SECONDS = 7200
|
||||
REFRESH_TOKEN_EXPIRE_SECONDS = 86400 * 7
|
||||
TOKEN_REFRESH_THRESHOLD_SECONDS = 1800
|
||||
TOKEN_CONTEXT_EXPIRE_SECONDS = 300
|
||||
WHITELIST_PATHS = ['/it/api/auth/login', '/it/api/auth/register']
|
||||
WHITELIST_PATHS = [
|
||||
'/it/api/auth/login',
|
||||
'/it/api/auth/register',
|
||||
'/it/api/auth/refresh',
|
||||
'/it/api/automation/execution/case/pull',
|
||||
'/it/api/automation/execution/queued',
|
||||
'/it/api/automation/execution/start',
|
||||
'/it/api/automation/execution/case/result',
|
||||
'/it/api/automation/execution/finish',
|
||||
'/it/api/automation/execution/abort'
|
||||
]
|
||||
|
||||
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
_redis_client.ping()
|
||||
@@ -32,6 +44,34 @@ def create_token(user_id):
|
||||
return token, TOKEN_EXPIRE_SECONDS
|
||||
|
||||
|
||||
def create_refresh_token(user_id):
|
||||
refresh_token = uuid.uuid4().hex
|
||||
key = REFRESH_TOKEN_PREFIX + refresh_token
|
||||
_redis_client.setex(key, REFRESH_TOKEN_EXPIRE_SECONDS, str(user_id))
|
||||
return refresh_token, REFRESH_TOKEN_EXPIRE_SECONDS
|
||||
|
||||
|
||||
def validate_refresh_token(refresh_token):
|
||||
key = REFRESH_TOKEN_PREFIX + refresh_token
|
||||
user_id = _redis_client.get(key)
|
||||
if user_id:
|
||||
return int(user_id)
|
||||
return None
|
||||
|
||||
|
||||
def revoke_refresh_token(refresh_token):
|
||||
if refresh_token:
|
||||
_redis_client.delete(REFRESH_TOKEN_PREFIX + refresh_token)
|
||||
|
||||
|
||||
def revoke_all_refresh_tokens(user_id):
|
||||
keys = _redis_client.keys(REFRESH_TOKEN_PREFIX + '*')
|
||||
for key in keys:
|
||||
stored_user_id = _redis_client.get(key)
|
||||
if stored_user_id == str(user_id):
|
||||
_redis_client.delete(key)
|
||||
|
||||
|
||||
def get_token_ttl(token):
|
||||
return _redis_client.ttl(TOKEN_PREFIX + token)
|
||||
|
||||
@@ -71,10 +111,10 @@ def login_required(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
token = parse_token()
|
||||
if not token:
|
||||
return ApiResponse.build_failure(40004, msg='未登录或缺少token!')
|
||||
return ApiResponse.build_failure(40001, msg='缺少token!')
|
||||
user_id = get_current_user_id(token)
|
||||
if not user_id:
|
||||
return ApiResponse.build_failure(40004, msg='token无效或已过期!')
|
||||
return ApiResponse.build_failure(451, msg='token无效或已过期!')
|
||||
session = None
|
||||
try:
|
||||
token_context = get_token_context(token)
|
||||
@@ -133,10 +173,10 @@ def permission_required(permission_code):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not getattr(g, 'current_user_id', None):
|
||||
return ApiResponse.build_failure(40004, msg='未登录或缺少token!')
|
||||
return ApiResponse.build_failure(40001, msg='缺少token!')
|
||||
current_permission_codes = getattr(g, 'current_permission_codes', [])
|
||||
if not has_permission(permission_code, current_permission_codes):
|
||||
return ApiResponse.build_failure(40004, msg='无权限访问该接口!')
|
||||
return ApiResponse.build_failure(40003, msg='无权限访问该接口!')
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
233
app/api/views.py
233
app/api/views.py
@@ -17,6 +17,7 @@ from .controller.rbacController import RbacController
|
||||
from .controller.userController import UserController
|
||||
from .controller.bugController import BugController, BugUploadController
|
||||
from .controller.projectHookController import ProjectHookController
|
||||
from .controller.automationController import AutomationController
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
|
||||
@@ -27,7 +28,7 @@ def api_before_request():
|
||||
return None
|
||||
token = request.headers.get('accessToken') or request.headers.get('accesstoken') or request.headers.get('Authorization')
|
||||
if not token:
|
||||
return ApiResponse.build_failure(40004, msg='未登录或缺少token!')
|
||||
return ApiResponse.build_failure(40001, msg='缺少token!')
|
||||
return None
|
||||
|
||||
|
||||
@@ -728,6 +729,199 @@ def plan_progress():
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
|
||||
|
||||
@api.route('/automation/case/run', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('automation:run')
|
||||
def automation_case_run():
|
||||
controller = AutomationController(request.get_json() or {})
|
||||
try:
|
||||
ret, err_msg = controller.case_run()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40009, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/plan/run', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('automation:run')
|
||||
def automation_plan_run():
|
||||
controller = AutomationController(request.get_json() or {})
|
||||
try:
|
||||
ret, err_msg = controller.plan_run()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40009, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/list', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('automation:list')
|
||||
def automation_execution_list():
|
||||
controller = AutomationController(request.args)
|
||||
try:
|
||||
return ApiResponse.build_success(20000, data=controller.execution_list())
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/detail', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('automation:detail')
|
||||
def automation_execution_detail():
|
||||
controller = AutomationController(request.args)
|
||||
try:
|
||||
ret, err_msg = controller.execution_detail()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40011, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/case/list', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('automation:detail')
|
||||
def automation_execution_case_list():
|
||||
controller = AutomationController(request.args)
|
||||
try:
|
||||
ret, err_msg = controller.execution_case_list()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40011, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/poll', methods=['POST'])
|
||||
@login_required
|
||||
@permission_required('automation:detail')
|
||||
def automation_execution_poll():
|
||||
from ..api.service.jenkinsPollService import JenkinsPollService
|
||||
from ..api.dao.automationDao import AutomationDao
|
||||
|
||||
req_data = request.get_json() or {}
|
||||
execution_id = req_data.get('executionId') or req_data.get('execution_id')
|
||||
|
||||
from ..api.controller.baseCrudController import BaseCrudController
|
||||
controller = BaseCrudController(req_data)
|
||||
|
||||
try:
|
||||
if execution_id:
|
||||
success, msg = JenkinsPollService.poll_jenkins_build_status(controller.session, execution_id)
|
||||
if not success:
|
||||
return ApiResponse.build_failure(40012, msg=msg)
|
||||
execution = AutomationDao.get_execution_by_id(controller.session, execution_id)
|
||||
return ApiResponse.build_success(20000, data=execution.to_dict() if execution else {'id': execution_id, 'message': msg})
|
||||
else:
|
||||
JenkinsPollService.poll_all_pending_executions(controller.session)
|
||||
return ApiResponse.build_success(20000, data={'message': '轮询完成'})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/case/pull', methods=['GET'])
|
||||
def automation_execution_case_pull():
|
||||
req_data = dict(request.args)
|
||||
req_data['_callback_token'] = request.headers.get('X-CALLBACK-TOKEN', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ret, err_msg = controller.execution_case_pull()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40011, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data=ret)
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/queued', methods=['POST'])
|
||||
def automation_execution_queued():
|
||||
req_data = request.get_json() or {}
|
||||
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ok, err_msg = controller.validate_callback_secret()
|
||||
if not ok:
|
||||
return ApiResponse.build_failure(40004, msg=err_msg)
|
||||
update_id, err_msg = controller.execution_queued()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40012, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data={'id': update_id})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/start', methods=['POST'])
|
||||
def automation_execution_start():
|
||||
req_data = request.get_json() or {}
|
||||
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ok, err_msg = controller.validate_callback_secret()
|
||||
if not ok:
|
||||
return ApiResponse.build_failure(40004, msg=err_msg)
|
||||
update_id, err_msg = controller.execution_start()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40012, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data={'id': update_id})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/case/result', methods=['POST'])
|
||||
def automation_execution_case_result():
|
||||
req_data = request.get_json() or {}
|
||||
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ok, err_msg = controller.validate_callback_secret()
|
||||
if not ok:
|
||||
return ApiResponse.build_failure(40004, msg=err_msg)
|
||||
update_id, err_msg = controller.execution_case_result()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40012, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data={'id': update_id})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/finish', methods=['POST'])
|
||||
def automation_execution_finish():
|
||||
req_data = request.get_json() or {}
|
||||
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ok, err_msg = controller.validate_callback_secret()
|
||||
if not ok:
|
||||
return ApiResponse.build_failure(40004, msg=err_msg)
|
||||
update_id, err_msg = controller.execution_finish()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40012, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data={'id': update_id})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/automation/execution/abort', methods=['POST'])
|
||||
def automation_execution_abort():
|
||||
req_data = request.get_json() or {}
|
||||
req_data['_callback_secret'] = request.headers.get('X-CALLBACK-SECRET', '')
|
||||
controller = AutomationController(req_data)
|
||||
try:
|
||||
ok, err_msg = controller.validate_callback_secret()
|
||||
if not ok:
|
||||
return ApiResponse.build_failure(40004, msg=err_msg)
|
||||
update_id, err_msg = controller.execution_abort()
|
||||
if err_msg:
|
||||
return ApiResponse.build_failure(40012, msg=err_msg)
|
||||
return ApiResponse.build_success(20000, data={'id': update_id})
|
||||
finally:
|
||||
controller.close_session()
|
||||
|
||||
|
||||
# =========================
|
||||
# 报告接口
|
||||
# =========================
|
||||
@@ -1250,6 +1444,43 @@ def auth_login():
|
||||
controller.close_session()
|
||||
|
||||
|
||||
@api.route('/auth/refresh', methods=['POST'])
|
||||
def auth_refresh():
|
||||
from .utils.authMiddleware import validate_refresh_token, create_token, create_refresh_token, revoke_refresh_token, get_current_user_id
|
||||
|
||||
req_json = request.get_json() or {}
|
||||
refresh_token = req_json.get('refreshToken') or req_json.get('refresh_token')
|
||||
access_token = req_json.get('accessToken') or req_json.get('access_token')
|
||||
|
||||
if refresh_token:
|
||||
user_id = validate_refresh_token(refresh_token)
|
||||
if user_id:
|
||||
revoke_refresh_token(refresh_token)
|
||||
new_token, expire_seconds = create_token(user_id)
|
||||
new_refresh_token, refresh_expire_seconds = create_refresh_token(user_id)
|
||||
return ApiResponse.build_success(20000, data={
|
||||
'token': new_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': expire_seconds,
|
||||
'refresh_token': new_refresh_token,
|
||||
'refresh_expires_in': refresh_expire_seconds
|
||||
})
|
||||
return ApiResponse.build_failure(40001, msg='refresh_token无效或已过期')
|
||||
|
||||
elif access_token:
|
||||
user_id = get_current_user_id(access_token)
|
||||
if user_id:
|
||||
new_token, expire_seconds = create_token(user_id)
|
||||
return ApiResponse.build_success(20000, data={
|
||||
'token': new_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': expire_seconds
|
||||
})
|
||||
return ApiResponse.build_failure(451, msg='access_token无效或已过期')
|
||||
|
||||
return ApiResponse.build_failure(40003, msg='请提供refresh_token或access_token')
|
||||
|
||||
|
||||
@api.route('/bug/list', methods=['GET'])
|
||||
@login_required
|
||||
@permission_required('bug:list')
|
||||
|
||||
Reference in New Issue
Block a user