提交所有代码到 qiaoxinjiu 分支
This commit is contained in:
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)
|
||||
|
||||
Reference in New Issue
Block a user