提交所有代码到 qiaoxinjiu 分支

This commit is contained in:
qiaoxinjiu
2026-05-11 14:29:16 +08:00
parent 01a4ac8ea1
commit 2fea5adb44
59 changed files with 4957 additions and 1603 deletions

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