import sys import os from PyQt5.QtCore import Qt, QTimer, QAbstractTableModel, QModelIndex, QPoint, QSize, pyqtSignal, QFile,QTextStream from PyQt5.QtGui import QBrush, QColor, QFont, QStandardItemModel, QStandardItem from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableView, QPushButton, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QCheckBox, QSpinBox, QMenu, QFileDialog, QTabWidget, QTabBar, QListWidget, QListWidgetItem, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, QHeaderView, QToolBar, QAction, QStatusBar, QComboBox, QSplitter, QAbstractItemView, QDoubleSpinBox) from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_PARAGRAPH_ALIGNMENT import qtawesome as qta from datetime import datetime # 导入其他模块 from model.ProcedureModel.ProcedureProcessor import ExcelParser, StepTableModel from utils.DBModels.ProcedureModel import DatabaseManager from utils import Globals # 修改导入路径 class StepExecutor(QWidget): # 添加标签锁定信号 tabLockRequired = pyqtSignal(bool) # 添加执行完成信号 executionFinished = pyqtSignal(object) # 发送执行器实例 def __init__(self, procedureData, procedureId, dbManager): super().__init__() self.procedureData = procedureData self.procedureId = procedureId # 新增规程ID self.dbManager = dbManager # 新增数据库管理器 self.isRunning = False self.isActive = False testSteps = procedureData["测试步骤"] self.initUi(testSteps) self.currentIndex = 0 self.timer = QTimer() self.timer.timeout.connect(self.autoExecuteStep) self.remainingCycles = 1 self.infiniteCycles = False self.isFirstRun = True # 新增标志位,用于区分首次执行 self.stepResults = [] # 新增:存储所有步骤执行结果的列表 # 新增:倒计时相关变量 self.countdownTimer = QTimer() self.countdownTimer.timeout.connect(self.updateCountdown) self.remainingTime = 0 # 当前轮次剩余时间(秒) self.totalSteps = 0 # 当前轮次总步骤数 self.protocolManage = Globals.getValue("protocolManage") def initUi(self, testSteps): layout = QVBoxLayout() info_layout = QFormLayout() info_layout.setLabelAlignment(Qt.AlignRight) info_layout.addRow("规程名称:", QLabel(self.procedureData["规程信息"]["规程名称"])) info_layout.addRow("规程编号:", QLabel(self.procedureData["规程信息"]["规程编号"])) info_layout.addRow("规程类型:", QLabel(self.procedureData["规程信息"]["规程类型"])) info_layout.addRow("测试用例:", QLabel(self.procedureData["测试用例信息"]["测试用例"])) info_layout.addRow("用例编号:", QLabel(self.procedureData["测试用例信息"]["用例编号"])) info_layout.addRow("工况描述:", QLabel(self.procedureData["测试用例信息"]["工况描述"])) layout.addLayout(info_layout) layout.addSpacing(20) self.tableModel = StepTableModel(testSteps) self.tableView = QTableView() self.tableView.setModel(self.tableModel) self.tableView.setContextMenuPolicy(Qt.CustomContextMenu) self.tableView.customContextMenuRequested.connect(self.showContextMenu) # QHeaderView保护 hh = self.tableView.horizontalHeader() if hh: hh.setSectionResizeMode(0, QHeaderView.ResizeToContents) hh.setSectionResizeMode(1, QHeaderView.Stretch) hh.setSectionResizeMode(2, QHeaderView.ResizeToContents) hh.setSectionResizeMode(3, QHeaderView.ResizeToContents) hh.setSectionResizeMode(5, QHeaderView.Stretch) vh = self.tableView.verticalHeader() if vh: self.tableView.setWordWrap(True) vh.setSectionResizeMode(QHeaderView.ResizeToContents) layout.addWidget(QLabel("测试步骤:")) layout.addWidget(self.tableView) # 创建控制按钮布局 control_layout = QHBoxLayout() self.autoButton = QPushButton(" 开始自动执行") self.autoButton.clicked.connect(self.startAutoExecute) self.autoButton.setIcon(qta.icon('fa5s.play', color='green')) self.stopButton = QPushButton(" 停止自动执行") self.stopButton.clicked.connect(self.stopAutoExecute) self.stopButton.setEnabled(False) self.stopButton.setIcon(qta.icon('fa5s.stop', color='red')) self.nextButton = QPushButton(" 执行下一步") self.nextButton.clicked.connect(self.executeNextStep) self.nextButton.setIcon(qta.icon('fa5s.step-forward', color='blue')) self.resetButton = QPushButton(" 完全重置") self.resetButton.clicked.connect(self.resetExecution) self.resetButton.setIcon(qta.icon('fa5s.redo', color='orange')) self.exportButton = QPushButton(" 生成报告") self.exportButton.clicked.connect(self.onExportReportClicked) self.exportButton.setIcon(qta.icon('fa5s.file-alt', color='purple')) control_layout.addWidget(self.autoButton) control_layout.addWidget(self.stopButton) control_layout.addWidget(self.nextButton) control_layout.addWidget(self.resetButton) control_layout.addWidget(self.exportButton) # 添加循环设置 cycle_layout = QHBoxLayout() cycle_layout.addWidget(QLabel("执行轮次:")) self.cycleSpin = QSpinBox() self.cycleSpin.setRange(1, 999) self.cycleSpin.setValue(1) cycle_layout.addWidget(self.cycleSpin) self.infiniteCheckbox = QCheckBox("无限循环") cycle_layout.addWidget(self.infiniteCheckbox) # 新增:状态显示标签 self.statusLabel = QLabel("就绪") self.statusLabel.setStyleSheet("color: blue; font-weight: bold;") cycle_layout.addWidget(self.statusLabel) # 新增:倒计时显示标签 self.countdownLabel = QLabel("") self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") cycle_layout.addWidget(self.countdownLabel) # 新增:步骤间隔时间设置 cycle_layout.addWidget(QLabel("步骤间隔(秒):")) self.stepIntervalSpin = QDoubleSpinBox() self.stepIntervalSpin.setRange(0, 60.0) self.stepIntervalSpin.setValue(1) self.stepIntervalSpin.setDecimals(2) # 支持一位小数 self.stepIntervalSpin.setSingleStep(0.1) # 每次调整0.1秒 self.stepIntervalSpin.setToolTip("设置步骤执行计时器的间隔时间(秒)") cycle_layout.addWidget(self.stepIntervalSpin) cycle_layout.addStretch() # 将所有布局添加到主布局 layout.addLayout(control_layout) layout.addLayout(cycle_layout) # 设置主布局 self.setLayout(layout) # 初始化工具栏 self.toolbar = QToolBar("执行工具栏") self.toolbar.setIconSize(QSize(24, 24)) # 工具栏操作 self.toolbar.addAction(qta.icon('fa5s.play', color='green'), "开始执行", self.startAutoExecute) self.toolbar.addAction(qta.icon('fa5s.stop', color='red'), "停止执行", self.stopAutoExecute) self.toolbar.addAction(qta.icon('fa5s.step-forward', color='blue'), "下一步", self.executeNextStep) self.toolbar.addSeparator() self.toolbar.addAction(qta.icon('fa5s.redo', color='orange'), "重置", self.resetExecution) self.toolbar.addSeparator() self.toolbar.addAction(qta.icon('fa5s.file-alt', color='purple'), "生成报告", self.onExportReportClicked) def showContextMenu(self, pos): index = self.tableView.indexAt(pos) if index.isValid(): menu = QMenu() jumpAction = menu.addAction("从该步骤开始执行") if jumpAction: jumpAction.triggered.connect(lambda: self.jumpExecute(index.row())) detailAction = menu.addAction("查看步骤详情") if detailAction: detailAction.triggered.connect(lambda: self.showStepDetail(index.row())) if hasattr(self.tableView, 'viewport') and hasattr(self.tableView.viewport(), 'mapToGlobal'): menu.exec_(self.tableView.viewport().mapToGlobal(pos)) def jumpExecute(self, rowIndex): if 0 <= rowIndex < self.tableModel.rowCount(): stepInfo = self.tableModel.getStepInfo(rowIndex) if stepInfo and not stepInfo['isMain']: self.executeStep(rowIndex) self.currentIndex = rowIndex + 1 def showStepDetail(self, row): step_info = self.tableModel.getStepInfo(row) if not step_info: return detail_dialog = QDialog(self) detail_dialog.setWindowTitle("步骤详情") detail_dialog.setMinimumWidth(500) layout = QVBoxLayout() form_layout = QFormLayout() form_layout.addRow("步骤ID", QLabel(step_info['stepId'])) form_layout.addRow("步骤类型", QLabel("主步骤" if step_info['isMain'] else "子步骤")) form_layout.addRow("步骤描述", QLabel(step_info['description'])) if step_info['time']: form_layout.addRow("执行时间", QLabel(step_info['time'].strftime("%Y-%m-%d %H:%M:%S"))) if step_info['result'] is not None: status = "成功" if step_info['result'] else "失败" form_layout.addRow("执行结果", QLabel(status)) layout.addLayout(form_layout) button_box = QDialogButtonBox(QDialogButtonBox.Ok) button_box.accepted.connect(detail_dialog.accept) layout.addWidget(button_box) detail_dialog.setLayout(layout) detail_dialog.exec_() def updateStatusDisplay(self, message, color="blue"): """更新状态显示""" self.statusLabel.setText(message) self.statusLabel.setStyleSheet(f"color: {color}; font-weight: bold;") def startAutoExecute(self): self.isRunning = True self.isActive = True # 发送标签锁定信号 self.tabLockRequired.emit(True) self.autoButton.setEnabled(False) self.stopButton.setEnabled(True) self.nextButton.setEnabled(False) self.resetButton.setEnabled(False) self.exportButton.setEnabled(False) # 如果是首次执行,则初始化步骤结果集合 if self.isFirstRun: self.stepResults = [] # 重置步骤结果集合 self.tableModel.resetExecutionState() self.isFirstRun = False # 标记已执行过 # 创建第一个执行记录 self.createNewExecutionRecord() self.remainingCycles = self.cycleSpin.value() self.infiniteCycles = self.infiniteCheckbox.isChecked() # 使用用户设置的步骤间隔时间启动计时器 stepInterval = int(self.stepIntervalSpin.value() * 1000) # 转换为毫秒 self.timer.start(stepInterval) # 更新状态显示 self.updateStatusDisplay(f"开始执行 - 第1轮", "green") # 开始倒计时(在第一个步骤执行前) self.startCountdown() def stopAutoExecute(self): self.isRunning = False # 清除自动执行状态 self.timer.stop() self.autoButton.setEnabled(True) self.stopButton.setEnabled(False) self.nextButton.setEnabled(True) self.resetButton.setEnabled(True) self.exportButton.setEnabled(True) # 注意: 这里不重置isActive,因为执行器仍处于激活状态 # 执行结束时保存当前轮次的步骤结果到数据库 if hasattr(self, 'current_execution_id') and self.stepResults: import json try: json.dumps(self.stepResults) except Exception as e: print("stepResults不能序列化", self.stepResults) raise self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) print(f"执行停止,当前轮次结果已保存") # 更新状态显示 self.updateStatusDisplay("执行已停止", "orange") # 停止倒计时 self.stopCountdown() # 发送执行完成信号 self.executionFinished.emit(self) # 执行完毕后自动解锁标签页 self.tabLockRequired.emit(False) # self.resetExecution() # 自动执行完毕后自动重置 def autoExecuteStep(self): if self.currentIndex < self.tableModel.rowCount(): step_info = self.tableModel.getStepInfo(self.currentIndex) if step_info and not step_info['isMain']: self.executeStep(self.currentIndex) self.currentIndex += 1 # 步骤执行完成后,更新倒计时 self.startCountdown() else: # 当前轮次执行完成,立即存储执行结果 if hasattr(self, 'current_execution_id') and self.stepResults: import json try: json.dumps(self.stepResults) except Exception as e: print("stepResults不能序列化", self.stepResults) raise self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) print(f"第 {self.getCurrentCycleNumber()} 轮执行完成,结果已存储") if self.infiniteCycles or self.remainingCycles > 0: if not self.infiniteCycles: self.remainingCycles -= 1 self.timer.stop() self.currentIndex = 0 should_continue = self.infiniteCycles or self.remainingCycles > 0 if should_continue: # 开始新一轮执行前,创建新的执行记录 self.createNewExecutionRecord() self.tableModel.resetAll() # 确保每轮自动清除颜色 self.stepResults = [] # 清空步骤结果,准备新一轮 # 更新状态显示 cycle_num = self.getCurrentCycleNumber() self.updateStatusDisplay(f"执行中 - 第{cycle_num}轮", "green") # 重新开始倒计时 self.startCountdown() # 使用用户设置的步骤间隔时间重新启动计时器 stepInterval = int(self.stepIntervalSpin.value() * 1000) # 转换为毫秒 self.timer.start(stepInterval) else: self.stopAutoExecute() # 发送执行完成信号 self.executionFinished.emit(self) else: self.stopAutoExecute() # 发送执行完成信号 self.executionFinished.emit(self) def createNewExecutionRecord(self): """创建新的执行记录""" execution_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.current_execution_id = self.dbManager.insertProcedureExecution( self.procedureId, execution_time ) print(f"创建新的执行记录,ID: {self.current_execution_id}") def getCurrentCycleNumber(self): """获取当前执行的轮次数""" if self.infiniteCycles: return "无限" else: total_cycles = self.cycleSpin.value() completed_cycles = total_cycles - self.remainingCycles return f"{completed_cycles + 1}/{total_cycles}" def executeNextStep(self): self.isActive = True # 发送标签锁定信号 self.tabLockRequired.emit(True) if self.currentIndex < self.tableModel.rowCount(): step_info = self.tableModel.getStepInfo(self.currentIndex) if step_info and not step_info['isMain']: self.executeStep(self.currentIndex) self.currentIndex += 1 # 手动执行不需要修改isRunning状态 def executeStep(self, row): stepInfo = self.tableModel.getStepInfo(row) if not stepInfo: return False print(f"开始执行步骤 {stepInfo['stepId']}: {stepInfo['description']}") result = self.handleStep(row, stepInfo) if result is None: print(f"警告:步骤 {stepInfo['stepId']} 返回了None结果,设置为False") result = False execution_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 只保存基础类型,避免递归 step_result = { 'step_id': str(stepInfo.get('stepId', '')), 'step_description': str(stepInfo.get('description', '')), 'execution_time': execution_time, 'result': bool(result) } self.stepResults.append(step_result) if hasattr(self, 'current_execution_id'): self.dbManager.updateStepResults(self.current_execution_id, self.stepResults) success = self.tableModel.updateStepResult(row, result, datetime.now()) return result def handleStep(self, rowIndex, stepInfo): # 修改参数名 description = stepInfo['description'] stepId = stepInfo['stepId'] print(f"处理步骤 {stepId}: {description}") if "设置" in description: return self.performWrite(stepId, description) # 修改方法名 elif "检查" in description: return self.performDataImport(stepId, description) # 修改方法名 elif "接收" in description: return self.performValidation(stepId, description) # 修改方法名 elif "导出" in description: return self.performExport(stepId, description) # 修改方法名 elif "备份" in description: return self.performBackup(stepId, description) # 修改方法名 elif "发送" in description: return self.performNotification(stepId, description) # 修改方法名 def simulateExecution(self, stepId, description): # 修改方法名 import random return random.random() < 0.9 def performWrite(self, stepId, description): # 修改方法名 print(f"执行登录操作 (步骤 {stepId}): {description}") return True def performDataImport(self, stepId, description): # 修改方法名 import random return random.random() < 0.95 def performValidation(self, stepId, description): # 修改方法名 import random return random.random() < 0.85 def performExport(self, stepId, description): # 修改方法名 import random return random.random() < 0.98 def performBackup(self, stepId, description): # 修改方法名 return True def performNotification(self, stepId, description): # 修改方法名 import random return random.random() < 0.99 def performWait(self, stepId, description): # 修改方法名 import time waitTime = 1 # 修改变量名 if "s" in description: try: waitTime = int(description.split("等待")[1].split("s")[0].strip()) except: pass time.sleep(waitTime) return True def performSetting(self, stepId, description): # 修改方法名 return True def resetExecution(self, fromAuto=False): self.tableModel.resetAll() self.currentIndex = 0 if not fromAuto: self.stopAutoExecute() self.cycleSpin.setValue(1) self.infiniteCheckbox.setChecked(False) self.isRunning = False self.isActive = False self.isFirstRun = True # 重置标志位 self.stepResults = [] # 重置步骤结果集合,禁止赋值为tableModel.stepData if hasattr(self, 'current_execution_id'): del self.current_execution_id self.tabLockRequired.emit(False) self.updateStatusDisplay("已重置", "blue") self.resetCountdown() def onExportReportClicked(self): self.generateReport() def startCountdown(self): """开始倒计时""" # 计算当前轮次的总步骤数(只计算子步骤) self.totalSteps = sum(1 for step in self.tableModel.stepData if not step['isMain']) # 计算剩余步骤数(从当前索引开始到结束) remainingSteps = 0 for i in range(self.currentIndex, len(self.tableModel.stepData)): if not self.tableModel.stepData[i]['isMain']: remainingSteps += 1 # 获取步骤间隔时间 stepInterval = self.stepIntervalSpin.value() # 计算当前轮次的剩余时间(剩余步骤数 * 步骤间隔时间) if remainingSteps > 0: self.remainingTime = int(remainingSteps * stepInterval) # 转换为整数秒 self.countdownTimer.start(1000) # 每秒更新一次 self.updateCountdown() else: # 如果没有剩余步骤,清空显示 self.countdownLabel.setText("") def stopCountdown(self): """停止倒计时""" self.countdownTimer.stop() self.countdownLabel.setText("") def updateCountdown(self): """更新倒计时显示""" if self.remainingTime > 0: minutes = self.remainingTime // 60 seconds = self.remainingTime % 60 if minutes > 0: countdown_text = f"当前轮次剩余: {minutes:02d}:{seconds:02d}" else: countdown_text = f"当前轮次剩余: {seconds}秒" # 根据剩余时间调整颜色 if self.remainingTime <= 10: self.countdownLabel.setStyleSheet("color: red; font-weight: bold; font-size: 14px;") elif self.remainingTime <= 30: self.countdownLabel.setStyleSheet("color: orange; font-weight: bold; font-size: 14px;") else: self.countdownLabel.setStyleSheet("color: blue; font-weight: bold; font-size: 14px;") self.countdownLabel.setText(countdown_text) self.remainingTime -= 1 else: self.countdownLabel.setText("当前轮次完成") self.countdownLabel.setStyleSheet("color: green; font-weight: bold; font-size: 14px;") self.countdownTimer.stop() def updateCycleCountdown(self): """更新轮次剩余时间倒计时 - 已废弃,保留兼容性""" self.updateCountdown() def resetCountdown(self): """重置倒计时""" self.stopCountdown() self.remainingTime = 0 self.totalSteps = 0 def isExecutionCompleted(self): """检查规程是否执行完成""" # 检查是否所有步骤都执行完成 return self.currentIndex >= self.tableModel.rowCount() def getExecutionProgress(self): """获取执行进度""" total_steps = self.tableModel.rowCount() completed_steps = self.currentIndex return completed_steps, total_steps def showEvent(self, event): super().showEvent(event) # 只在第一次显示时调整行高 if not hasattr(self, '_hasResizedRows'): self.tableView.resizeRowsToContents() self._hasResizedRows = True