Compare commits

...
2 Commits
Author SHA1 Message Date
feifei.xu 45e4f1ecf2 更新优化 2026-07-10 13:35:29 +08:00
feifei.xu 4c5fc718c1 上传初版 2026-07-10 13:35:16 +08:00
27 changed files with 1529 additions and 816 deletions
+14 -20
View File
@@ -1,25 +1,19 @@
"""main.py - MIL SDK 测试示例"""
from pathlib import Path
from src.core import (
setup_logging,
read_excel_data,
read_excel_case,
create_excel_case,
update_case_excel,
)
from main_ui import Ui_MainWindow
from PySide6.QtWidgets import QApplication, QMainWindow
import sys
from qt_material import apply_stylesheet
def main() -> None:
"""MIL SDK 示例程序主函数"""
setup_logging()
class MainWindow(QMainWindow,Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
excel_path = Path(__file__).parent
data_dict = read_excel_data("new_data.xlsx", return_object=False)
old_data_dict = read_excel_data("old_data.xlsx", return_object=False)
update_case_excel("new_case.xlsx", old_data_dict, data_dict)
apply_stylesheet(self, style=None,theme='dark_teal.xml')
if __name__ == "__main__":
main()
if __name__ == '__main__':
app = QApplication(sys.argv)
main: MainWindow = MainWindow()
main.show()
sys.exit(app.exec())
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="FrameMILTool" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>FrameMILTool</class>
<extends>QFrame</extends>
<header>src.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'main.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QMainWindow,
QMenuBar, QSizePolicy, QStatusBar, QWidget)
from mil import FrameMILTool
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.setObjectName(u"gridLayout")
self.frame = FrameMILTool(self.centralwidget)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.gridLayout.addWidget(self.frame, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 800, 21))
MainWindow.setMenuBar(self.menubar)
self.statusbar = QStatusBar(MainWindow)
self.statusbar.setObjectName(u"statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
# retranslateUi
+8
View File
@@ -0,0 +1,8 @@
{
"DataPath": "",
"FilePath": "",
"AddTimeEn": true,
"GeratePath": true,
"CurrProject": "",
"ItemConfigs": {}
}
+27
View File
@@ -0,0 +1,27 @@
# This file was generated by Nuitka
# Stubs included by default
from openpyxl import Workbook
from ui.mil_tool import FrameMILTool
def create_mil_tool(parent: typing.Any) -> typing.Any:
...
__name__ = ...
# Modules used internally, to allow implicit dependencies to be seen:
import os
import openpyxl
import logging
import dataclasses
import typing
import copy
import openpyxl.worksheet
import openpyxl.worksheet.worksheet
import _frozen_importlib_external
import PySide6
import PySide6.QtWidgets
import PySide6.QtCore
import PySide6.QtGui
+4
View File
@@ -0,0 +1,4 @@
from .ui.mil_tool import FrameMILTool
def create_mil_tool(parent=None):
return FrameMILTool(parent)
@@ -3,14 +3,12 @@
提供 MIL 仿真数据读取功能
使用示例:
from src.core import setup_logging, read_excel_data
from src.core import read_excel_data
setup_logging()
result = read_excel_data("simulation.xlsx")
"""
import logging
from .base import DataLog, SignalData, ExcelDataResult
from .base import DataLog, SignalData, ExcelDataResult,Config
from .mil_read_data_excel import read_excel_data, ExcelReaderConfig
from .mil_read_case_excel import read_excel_case
from .mil_create_data_excel import create_excel_case
@@ -23,7 +21,9 @@ from .exceptions import (
CaseDataError,
ExcelWriteError,
)
from .logging_config import setup_logging, get_logger
__all__ = [
"DataLog",
@@ -39,6 +39,5 @@ __all__ = [
"ExcelFormatError",
"CaseDataError",
"ExcelWriteError",
"setup_logging",
"get_logger",
"Config"
]
+195
View File
@@ -0,0 +1,195 @@
"""MIL SDK 核心数据模型"""
import os
import json
import logging
from dataclasses import dataclass, field, fields
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class Config:
"""MIL SDK 全局配置
承载测试工程相关的全局开关与路径信息,是 UI 层与 core 层之间的
配置传输载体。序列化使用 JSON 文件持久化。
Attributes:
DataPath: 仿真数据目录(Excel 原始文件所在路径)
FilePath: 当前打开的 Excel 文件路径
AddTimeEn: 用例步骤时间是否按累加方式记录
GeratePath: 是否为生成的用例另存新文件
CurrProject: 当前工程名称
ItemConfigs: 各模块/用例项的细粒度配置
"""
DataPath:str = ""
FilePath:str = ""
AddTimeEn:bool = True
GeratePath:bool = True
CurrProject:str = ""
# dict 是可变类型,必须用 default_factory 显式实例化,避免多个 Config 共享同一对象
ItemConfigs: dict = field(default_factory=dict)
def to_dict(self):
"""将 Config 实例序列化为 dict,便于写入 JSON 文件。"""
return {
"DataPath":self.DataPath,
"FilePath":self.FilePath,
"AddTimeEn":self.AddTimeEn,
"GeratePath":self.GeratePath,
"CurrProject":self.CurrProject,
"ItemConfigs":self.ItemConfigs
}
def load_config(self, config_path: str) -> "Config":
"""从 JSON 文件读取配置并填充到当前实例的各个字段。
解析规则:
- JSON 中存在的字段会被回写到 Config 的对应字段;
- JSON 中缺失的字段保持当前 Config 实例的默认值;
- JSON 中多余字段被忽略(向前兼容:增加字段不会破坏老配置)。
Args:
config_path: 配置文件路径。
Returns:
self:填充后的 Config 实例,便于链式调用。
Raises:
FileNotFoundError: 配置文件不存在。
json.JSONDecodeError: 文件内容不是合法 JSON。
"""
try:
# 以 UTF-8 读取 JSON,避免 Windows 默认编码带来的乱码问题
with open(config_path, 'r', encoding='utf-8') as f:
raw = json.load(f)
except FileNotFoundError:
# 显式记录并重抛,遵循"错误必须显式处理,禁止静默失败"
logger.error(f"配置文件{config_path}未找到")
raise
if not isinstance(raw, dict):
# 文件不合法(非 dict 根节点)→ 用一个空 dict 填充,保持实例可用
raw = {}
# 用反射取出本类已声明的字段名白名单,避免 JSON 脏字段污染
allowed = {f.name for f in fields(self.__class__)}
for key, value in raw.items():
if key in allowed:
setattr(self, key, value)
return self
def save_config(self,config_path:str):
"""将当前配置以 JSON 格式写入磁盘。
Args:
config_path: 配置文件路径。
Raises:
Exception: 写入失败时记录日志并原样抛出异常。
"""
try:
with open(config_path,'w',encoding='utf-8') as f:
json.dump(self.to_dict(), f, indent=4, ensure_ascii=False)
except Exception as e:
logger.error(f"配置文件{config_path}写入失败{e.args}")
@dataclass
class DataLog:
"""仿真数据日志记录
Attributes:
time: 时间戳(秒)
value: 信号值(可以是任意类型)
"""
time: float = 0.0
value: str = ""
@dataclass
class SignalData:
"""信号数据封装
描述 Excel 中某一列信号的完整信息:
- 所在列号(column
- 列上方的属性行(attributes,例如 Signal Name / BlockPath 等)
- 时间-值采样序列(datalog)
Attributes:
column: 信号在 Excel 工作表中的列索引(1-based)
attributes: 列上方各属性行(行号 -> 文本)的字典映射
datalog: 时间戳-数值采样点列表
"""
# signal_type: str | None = None
column: int = 0
attributes: dict[str, str] = field(default_factory=dict)
datalog: list[DataLog] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
"""将 SignalData 转为 dict,便于跨层传输与持久化。
Returns:
包含 column、attributes、datalog 字段的字典。
"""
return {
"column": self.column,
"attributes": self.attributes,
"datalog": self.datalog
}
@dataclass
class ExcelDataResult:
"""Excel 数据读取结果封装
提供对 Excel 数据的类型安全访问,隐藏内部实现细节
Attributes:
sheet_name: 工作表名称
source_row: Source: Input 所在行号
signals: 信号名称到信号数据的映射
"""
sheet_name: str = "Scenario1"
source_row: int = 0
signals: dict[str, SignalData] = field(default_factory=dict)
def get_signal(self, name: str) -> SignalData | None:
"""获取指定信号的数据
Args:
name: 信号名称
Returns:
信号数据对象,如果不存在返回 None
"""
return self.signals.get(name)
def get_signal_names(self) -> list[str]:
"""获取所有信号名称
Returns:
信号名称列表
"""
return list(self.signals.keys())
def to_dict(self) -> dict[str, Any]:
"""转换为字典格式(兼容旧 API
Returns:
包含所有信号的字典,保留原有的数据结构
"""
result = {
"sheet_name": self.sheet_name,
"source_row": self.source_row
}
for name, signal in self.signals.items():
result[name] = signal.to_dict()
return result
+33
View File
@@ -0,0 +1,33 @@
"""MIL SDK 自定义异常模块
约定:
- 所有 SDK 主动抛出的异常均继承自 MILSDKError,便于上层统一捕获;
- 异常命名体现"出错阶段"(读取 / 格式 / 用例数据 / 写入),
调用方只需按需捕获细分异常,或在外层兜底捕获 MILSDKError。
"""
class MILSDKError(Exception):
"""MIL SDK 所有自定义异常的根类,UI 层可针对此类型统一兜底。"""
pass
class ExcelReadError(MILSDKError):
"""Excel 读取阶段错误:文件不存在、权限不足、被占用、底层解析失败等。"""
pass
class ExcelFormatError(MILSDKError):
"""Excel 内容格式错误:缺少约定的工作表、缺少 'Source: Input' 标记、
模板版本号缺失等结构性异常。"""
pass
class CaseDataError(MILSDKError):
"""用例数据语义错误:信号名未在数据字典中找到、步骤时间非数值等。"""
pass
class ExcelWriteError(MILSDKError):
"""Excel 写入阶段错误:磁盘权限、文件被占用、保存失败等。"""
pass
@@ -24,18 +24,23 @@ def create_excel_case(
data_dict: dict,
sheets_dict: dict,
) -> None:
"""生成测试用例 Excel 文件
"""根据读入的数据字典与用例模板,批量生成测试用例 Excel 文件
每个用例以"用例名.xlsx"的形式输出到 excel_path 目录下文件格式
read_excel_data 读入的模板保持一致
Args:
excel_path: 输出目录路径
data_dict: 数据字典
sheets_dict: 工作表字典
excel_path: 输出目录路径
data_dict: read_excel_data 返回的字典需要包含 wb / sheet / source_row
以及每个信号的 column / datalog
sheets_dict: read_excel_case 返回的用例字典
Raises:
CaseDataError: 数据异常
ExcelWriteError: 文件写入错误
CaseDataError: 传入的数据字典类型不符合 Workbook/Worksheet
ExcelWriteError: 保存失败路径非法权限不足等
"""
logger.info(f"开始生成测试用例,输出目录: {excel_path}")
# 取出原始工作簿的引用;后续每个用例都基于原 wb 做 deepcopy,避免相互污染
wb = data_dict.get('wb')
sheet = data_dict.get('sheet')
source_row = data_dict.get('source_row')
@@ -44,11 +49,13 @@ def create_excel_case(
logger.error("数据类型错误,Workbook 或 Worksheet 类型不匹配")
raise CaseDataError("数据异常,Workbook 或 Worksheet 类型错误")
# 拆出"信号 -> 数据"部分,剥离 Workbook 元数据,便于对每个用例独立处理
data_dict_copy = {k: v for k, v in data_dict.items()
if k not in ('wb', 'sheet', 'source_row')}
datas_dict = _analysis_case(data_dict_copy, sheets_dict)
case_count = 0
# 对每个用例:拷贝原工作簿 → 写入信号值 → 另存为独立文件
for name in datas_dict.keys():
case_count += 1
logger.debug(f"正在生成用例: {name}")
@@ -58,7 +65,8 @@ def create_excel_case(
generate_path = f"{excel_path}/{name}.xlsx"
_write_excel_data(sheet, datas_dict[name], source_row)
wb.save(generate_path)
# 注意必须保存副本,不能回写源 wb(修复后的关键点)
new_wb.save(generate_path)
logger.info(f"已生成用例: {name},路径: {generate_path}")
@@ -66,16 +74,17 @@ def create_excel_case(
def _write_excel_data(sheet: Worksheet, data_dict: dict, source_row: int) -> None:
"""写入 Excel 数据
"""将每个信号的 datalog 序列写回到对应单元格,并校正末尾时间戳。
Args:
sheet: Worksheet 对象
data_dict: 数据字典
source_row: Source: Input 所在行号
sheet: 目标 Worksheet 对象
data_dict: 当前用例的"信号 -> {column, datalog}"字典
source_row: "Source: Input" 所在行号数据起始行
"""
for name in data_dict.keys():
column = data_dict[name]["column"]
datalog = data_dict[name]["datalog"]
# 逐点写入:根据值类型设置 data_type,便于后续读取时类型还原
for index, data in enumerate(datalog):
try:
value = int(data.value)
@@ -85,10 +94,12 @@ def _write_excel_data(sheet: Worksheet, data_dict: dict, source_row: int) -> Non
sheet.cell(row=source_row + index, column=column).data_type = "str"
finally:
sheet.cell(row=source_row + index, column=column).value = value
# 第 2 列(首个信号列)额外把 time 写进 A 列,保持和原模板一致
if column == 2:
sheet.cell(row=source_row + index, column=1).value = data.time
sheet.cell(row=source_row + index, column=1).data_type = "float"
# 校正尾部时间戳:当最后一行的时间小于信号最后采样的 time 时,沿用信号末尾时间
column = 2
while sheet.cell(1, column).value != "time":
column += 1
@@ -102,14 +113,18 @@ def _write_excel_data(sheet: Worksheet, data_dict: dict, source_row: int) -> Non
def _analysis_case(data_dict: dict, sheets_dict: dict) -> CaseResultDict:
"""分析用例字典,将用例中的信号名替换为信号值
"""遍历每个用例模板,把"信号名"展开成"信号值"序列,得到每个用例的最终数据字典。
处理逻辑
- enable=True 的用例跳过不生成
- 缺失 enable 字段也跳过避免模板脏数据导致运行期错误
Args:
data_dict: 数据字典
sheets_dict: 工作表字典
data_dict: 去除 wb/sheet/source_row 后的数据字典
sheets_dict: 用例字典read_excel_case 的返回
Returns:
处理后的用例字典
用例名 -> 用例专属数据字典 的映射
"""
datas_dict: CaseResultDict = {}
for sheet_name in sheets_dict.keys():
@@ -118,10 +133,13 @@ def _analysis_case(data_dict: dict, sheets_dict: dict) -> CaseResultDict:
for case in sheet_dict.keys():
try:
if sheet_dict[case]["enable"]:
# 已完成的用例跳过生成
continue
except KeyError:
# 缺少 enable 字段:保守跳过
continue
# 拷贝数据并重置 datalog,然后按步骤逐条填充
datas_dict[case] = copy.deepcopy(data_dict)
_init_data_log(datas_dict[case])
_analysis_step(sheet_dict[case]["step"], datas_dict[case])
@@ -129,34 +147,40 @@ def _analysis_case(data_dict: dict, sheets_dict: dict) -> CaseResultDict:
def _analysis_step(step_dict: dict, data_dict: dict) -> None:
"""分析步骤字典,将步骤中的信号名替换为信号值
"""步骤字典依次处理每一步:有 action 走 action;无 action 走"信号保持"
Args:
step_dict: 步骤字典
data_dict: 数据字典
step_dict: 步骤字典 {"step0": {...}, "step1": {...}, ...}
data_dict: 当前用例的数据字典in-place 修改
"""
for step_key in step_dict.keys():
if "action" in step_dict[step_key].keys():
# 该步骤显式定义了信号赋值
_analysis_data(step_dict[step_key]["action"], data_dict, step_dict[step_key]["time"])
else:
# 无 action:对每个信号按"前一帧值保持"在时间轴上插一个数据点
for name in data_dict.keys():
datalog = data_dict[name]["datalog"]
datalog.append(DataLog(step_dict[step_key]["time"], datalog[-1].value))
def _analysis_data(action_dict: dict, data_dict: dict, time: float) -> None:
"""分析数据字典,将数据字典中的信号名替换为信号值
"""对单步 action 展开:为每个被赋值的信号追加或修改 datalog 中的当前时间点。
注意data_log_len 用于本步结束后将"未参与动作的其他信号"补齐到同等长度
保持所有信号在同一时间轴上对齐
Args:
action_dict: 操作字典
data_dict: 数据字典
time: 时间戳
action_dict: 操作字典信号名 -> 字符串值
data_dict: 当前用例的数据字典in-place 修改
time: 本步骤的时间戳
Raises:
CaseDataError: 信号不存在
CaseDataError: action 中存在 data_dict 找不到的信号
"""
data_log_len: int | None = None
for name in action_dict.keys():
# 大小写不敏感匹配:避免 Excel 中信号名大小写差异导致的 KeyError
matched_key: str | None = None
for key in data_dict.keys():
if name.lower() == key.strip().lower():
@@ -167,11 +191,14 @@ def _analysis_data(action_dict: dict, data_dict: dict, time: float) -> None:
else:
datalog = data_dict[matched_key]["datalog"]
if datalog[-1].time == time:
# 同一时间戳已存在 → 修改末点值;多步合并到同一时刻
datalog[-1].value = action_dict[name]
data_log_len = len(datalog)
else:
# 追加新的采样点
datalog.append(DataLog(time, action_dict[name]))
data_log_len = len(datalog)
# 对未参与动作的信号补点(保持上一帧值),保证时间轴对齐
for name in data_dict.keys():
datalog = data_dict[name]["datalog"]
if len(datalog) != data_log_len:
@@ -179,10 +206,10 @@ def _analysis_data(action_dict: dict, data_dict: dict, time: float) -> None:
def _init_data_log(data_dict: dict) -> None:
"""初始化数据日志
"""把每个信号的 datalog 重置为只保留首点,作为各用例的初始状态。
Args:
data_dict: 数据字典
data_dict: 数据字典in-place 修改
"""
for name in data_dict.keys():
data_dict[name]["datalog"] = data_dict[name]["datalog"][:1]
@@ -28,7 +28,13 @@ CaseDict = dict[str, Any]
class CaseColumns:
"""用例 Excel 列索引常量"""
"""用例 Excel 各列含义的常量定义。
TITLE: 用例标题用于分组与启停判断
STATUS: 用例状态"完成测试"
ACTION: 操作描述 "signal1=value1; signal2=value2"
NAME: 当前步骤的名称
TIME: 当前步骤的时间戳浮点
"""
TITLE = 1
STATUS = 2
ACTION = 3
@@ -36,6 +42,7 @@ class CaseColumns:
TIME = 5
# STATUS 列等于此值表示该用例已被勾选为"已完成测试"
STATUS_COMPLETE = "完成测试"
@@ -44,22 +51,35 @@ def read_excel_case(
data_dict: dict,
addTimeEn: bool,
) -> CaseDict:
"""读取 Excel 用例模板
"""读取 Excel 用例模板并解析为内存中的结构化用例字典。
Args:
excel_path: Excel 模板文件路径
data_dict: 信号数据字典
addTimeEn: 是否累加时间
excel_path: Excel 模板文件路径
data_dict: 数据字典来自 read_excel_data用于校验 action 中的信号名
addTimeEn: True 表示按"累加"方式记录每一步的时间戳
False 表示直接使用步骤中填写的时间
Returns:
用例字典
用例字典
{
sheet_title: {
case_name: {
"enable": bool,
"step": {
"step0": {"name": ..., "time": ..., "action": {...} 可选},
...
}
}
}
}
Raises:
ExcelReadError: 文件读取失败
ExcelFormatError: 格式错误
CaseDataError: 用例数据错误
ExcelReadError: 文件读取失败不存在 / 权限问题
ExcelFormatError: 缺少 Atech-Hefei / 缺少模板版本号
CaseDataError: 标题缺失 / 时间非数值 / action 信号未在 data_dict 中找到
"""
logger.info(f"开始读取用例模板: {excel_path}")
# 用只读 + 取值模式打开,避免触发公式重算、降低内存占用
try:
wb = load_workbook(excel_path, read_only=True, data_only=True)
logger.debug(f"Excel 文件加载成功,Sheet 数量: {len(wb.sheetnames)}")
@@ -69,6 +89,7 @@ def read_excel_case(
logger.error(f"读取 Excel 文件失败: {e}")
raise ExcelReadError(f"无法读取Excel文件 {excel_path}: {str(e)}")
# 版本表 Atech-Hefei 必须存在;解析后将其从工作簿移除,避免进入后续遍历
try:
version = _get_template_version(wb["Atech-Hefei"])
if version is None:
@@ -82,25 +103,31 @@ def read_excel_case(
result_dict: CaseDict = {}
sheet_count = 0
# 按 sheet 维度组织用例:每个 sheet 内连续的行通过 "标题" 列切换归属
for sheet in wb.worksheets:
sheet_count += 1
logger.debug(f"正在读取用例模板表: {sheet.title}")
result_dict[sheet.title] = {}
# 当前行所属标题 / 上一次见到过的标题,用于检测标题变化、新用例的开始
new_head = None
old_head = None
step_id = 0
row = 2
# 累加模式下的当前累计时间
old_time = 0.0
while True:
# 自上而下扫描 TITLE 列;遇到 None 才视为结束
if sheet.cell(row=row, column=CaseColumns.TITLE).value is not None:
new_head = sheet.cell(row=row, column=CaseColumns.TITLE).value
if new_head is None:
# 标题列出现连续空,说明用例段落已结束(不能一开始就为空)
raise CaseDataError(
f"Excel文件 {excel_path} 格式错误,{sheet.title}{row} 行第 {CaseColumns.TITLE} 列必须有标题名"
)
# 标题切换:意味着进入新用例,重置步骤序号与累加时间
if new_head != old_head:
step_id = 0
old_time = 0.0
@@ -109,6 +136,7 @@ def read_excel_case(
result_dict[sheet.title][new_head] = {}
result_dict[sheet.title][new_head]["step"] = {}
# 是否启用(仅看该用例首行的 STATUS 即可)
if sheet.cell(row=row, column=CaseColumns.STATUS).value == STATUS_COMPLETE:
result_dict[sheet.title][new_head]["enable"] = True
else:
@@ -116,8 +144,10 @@ def read_excel_case(
step_name = sheet.cell(row, CaseColumns.NAME).value
step_time = sheet.cell(row, CaseColumns.TIME).value
# 时间为空 → 当前用例段落的所有步骤处理完毕
if step_time is None:
break
# 时间列必须是浮点数字,否则视为脏数据
try:
step_time = float(step_time)
except (ValueError, TypeError):
@@ -129,9 +159,11 @@ def read_excel_case(
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["name"] = step_name
if addTimeEn:
# 累加模式下:用 old_time 累加,写出"绝对时间"
old_time += step_time
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["time"] = old_time
# 解析 action 字符串(信号名=信号值;...),缺省视为无动作
strings = sheet.cell(row, CaseColumns.ACTION).value
if strings is not None:
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["action"] = \
@@ -144,16 +176,17 @@ def read_excel_case(
def _get_template_version(sheet: Worksheet) -> str | None:
"""Excel 模板版本号
"""'Atech-Hefei' 表中 TITLE 列自第 2 行起的全部版本号文本。
Args:
sheet: Excel 工作表
sheet: Excel 工作表对象
Returns:
模板版本号如果未找到返回 None
模板版本号字符串若整列均为空返回 None
"""
row = 2
version = None
# 持续向下读取直到遇到空单元格;保留最后一个非空值作为版本号
while sheet.cell(row=row, column=CaseColumns.TITLE).value is not None:
version = sheet.cell(row=row, column=CaseColumns.TITLE).value
row += 1
@@ -168,23 +201,30 @@ def _analysis_action(
data_dict: dict,
excel_path: str
) -> dict[str, str]:
"""解析操作字符串
""""signal1=value1; signal2=value2" 形式的操作字符串解析为字典。
解析规则
- 兼容中文分号 "" 与换行符
- 容忍空格
- "=" 的片段直接跳过
- 信号名按"忽略大小写 + 去空格" data_dict 中匹配匹配失败抛 CaseDataError
Args:
sheet: Excel 工作表
row: 行号
column: 列号
strings: 操作字符串
data_dict: 数据字典
excel_path: Excel 文件路径
sheet: Excel 工作表用于错误信息中显示 sheet
row: 当前所在行号
column: 当前所在列号
strings: 原始操作字符串
data_dict: 来自 read_excel_data 的信号字典
excel_path: 仅用于错误信息中上下文
Returns:
操作字典
信号名 -> 信号值的字典
Raises:
CaseDataError: 信号不存在或格式错误
CaseDataError: 信号 data_dict 中找不到
"""
action_dict: dict[str, str] = {}
# 中文分号替换为半角;同时剔除换行避免空片段
strings = strings.replace("", ";").replace("\n", "")
for item in strings.split(";"):
@@ -192,13 +232,16 @@ def _analysis_action(
if not item:
continue
# 容忍信号与值之间的空格,统一去掉
item = item.replace(" ", "")
if "=" not in item:
# 没有 "=" 视为非法片段,直接跳过(保持容错)
continue
signal_name, signal_value = item.split("=", 1)
signal_name = signal_name.strip()
# 大小写不敏感地在 data_dict 中查找原始 key(保留原拼写用于回写)
matched_key: str | None = None
for key in data_dict.keys():
if signal_name.lower() == key.strip().lower():
@@ -33,7 +33,10 @@ class ExcelReaderConfig:
time_column: 时间列索引默认 1A
header_row: 信号名称所在行号默认 1
type_row: 信号类型所在行号默认 3
interp_row: 插值策略所在行号默认 6
data_start_row_offset: 相对于 source_row 的数据起始行偏移量默认 1
output_header: 输出数据标记行文本默认 "Source: Output"
block_path_row: BlockPath 属性所在行号默认 4
"""
sheet_name: str = "Scenario1"
source_header: str = "Source: Input"
@@ -75,9 +78,11 @@ def read_excel_data(
ExcelReadError: 文件不存在或读取失败
ExcelFormatError: Excel 格式不符合预期
"""
# 未显式传入配置时,退回到默认约定值
config = config or ExcelReaderConfig()
logger.info(f"开始读取文件: {excel_path}")
# 1) 加载底层 Excel 文件:openpyxl 自身抛出的两类异常分别映射到自定义异常类
try:
wb = load_workbook(excel_path)
logger.info(f"文件加载成功,Sheet 数量: {len(wb.sheetnames)}")
@@ -88,43 +93,46 @@ def read_excel_data(
logger.error(f"读取文件失败: {e}")
raise ExcelReadError(f"读取Excel文件 {excel_path} 失败: {e}")
# 2) 取出约定名称的工作表,KeyError 表示工作表缺失,视为格式错误
try:
sheet = wb[config.sheet_name]
except KeyError:
logger.error(f"缺少 '{config.sheet_name}'")
raise ExcelFormatError(f"Excel文件 {excel_path} 缺少 '{config.sheet_name}'")
# 3) 自第 1 行起纵向扫描 "Source: Input" 标记行
column = 2
source_row = 1
while sheet.cell(row=source_row, column=column).value != config.source_header:
source_row += 1
# 扫描到表格末尾仍未命中,认定为格式错误
if source_row >= sheet.max_row:
logger.error(f"缺少 {config.source_header} 标记")
raise ExcelFormatError(f"Excel格式错误,缺少 {config.source_header}")
logger.debug(f"找到 {config.source_header} 标记,行号: {source_row}")
# 4) 沿 source_row 横向遍历每一列,识别并解析出每个有效信号
signals: SignalDict = {}
column = 2
current_header = None
while True:
# 当列所在单元格的标记不再是 source_header 时,说明信号区域结束
value = sheet.cell(source_row, column).value
if value is not None:
current_header = value
if current_header is None:
logger.error(f"缺少 {config.source_header} 标记")
raise ExcelFormatError(f"Excel格式错误,缺少 {config.source_header}")
if current_header == config.source_header:
if value != config.source_header:
break
# 取 header_row 行的列名作为信号名
name = sheet.cell(config.header_row, column).value
if name is None:
break
# 时间列与若干固定属性名跳过
if name == "time":
column += 1
continue
if name in ("Parameter:", "Value", "BlockPath"):
column += 1
continue
# 收集列上方属性 + 数据序列,写入 SignalData
attributes = __get_signal_attributes(sheet, column, source_row)
if attributes is None:
break
@@ -134,13 +142,12 @@ def read_excel_data(
datalog=__get_data_log(sheet, source_row + config.data_start_row_offset, column, config.time_column)
)
logger.debug(f"解析信号: {name}, 数据点: {len(signals[name].datalog)}")
else:
break
column += 1
signal_count = len(signals)
logger.info(f"解析完成,共 {signal_count} 个信号")
# 新接口:返回强类型 ExcelDataResult(推荐)
if return_object:
return ExcelDataResult(
sheet_name=config.sheet_name,
@@ -148,6 +155,7 @@ def read_excel_data(
signals=signals
)
# 旧接口:保持返回 dict 以便调用方继续拿到 Workbook / Worksheet 引用
data: DataDict = {
"wb": wb,
"sheet": sheet,
@@ -164,15 +172,15 @@ def __get_signal_attributes(
column: int,
max_row: int
) -> dict[int, str]:
"""从指定位置读取信号属性字典
"""读取指定列在 [1, max_row) 范围内各属性行的内容。
Args:
sheet: Worksheet 对象
column: 列号
max_row: 最大行号
max_row: 信号名行不含以内的最大行号
Returns:
信号属性字典键为行号值为单元格
信号属性字典键为行号int值为单元格文本str
"""
attributes: dict[int, str] = {}
for row in range(1, max_row):
@@ -185,19 +193,20 @@ def __get_data_log(
column: int,
time_column: int = 1
) -> list[DataLog]:
"""从指定位置读取时间-值数据对列表
"""从指定起始行向下读取 (time, value) 序列,遇到空时间戳即终止。
Args:
sheet: Worksheet 对象
row: 起始行号
column: 数据列号
time_column: 时间列索引默认 1
time_column: 时间列索引默认 1A
Returns:
DataLog 对象列表直到遇到空时间戳为止
"""
data_log: list[DataLog] = []
while True:
# 同时读取时间列与数据列,时间列空即视为数据终止
time = sheet.cell(row=row, column=time_column).value
value = sheet.cell(row=row, column=column).value
if time is None:
@@ -16,18 +16,25 @@ logger = logging.getLogger(__name__)
def update_case_excel(filename: str, old_data: dict, new_data: dict) -> None:
"""新 Excel 文件中的信号列
"""将旧 Excel 同步到新 Excel 的信号列集合(增 / 删)。
流程
1. 拆出 wb / sheet / source_row 等元数据
2. diff要新增的信号要删除的信号
3. 先删后插保持列号稳定
4. 回写文件
Args:
filename: Excel 文件路径
old_data: 旧数据字典
new_data: 新数据字典
filename: 目标 Excel 文件路径
old_data: 旧数据字典 wb / sheet / source_row 及各信号
new_data: 新数据字典结构同上用于 diff 比较
Raises:
CaseDataError: 数据类型错误
CaseDataError: 数据字典中 Workbook/Worksheet 类型不匹配
"""
logger.info(f"开始更新 Excel 文件 {filename}")
# 注意:pop 会从字典中移除键,调用前请确认数据不再被复用
old_wb = old_data.pop('wb')
old_sheet = old_data.pop('sheet')
old_source_row = old_data.pop('source_row')
@@ -40,25 +47,32 @@ def update_case_excel(filename: str, old_data: dict, new_data: dict) -> None:
logger.error("数据类型错误,Workbook 或 Worksheet 类型不匹配")
raise CaseDataError("数据异常,Workbook 或 Worksheet 类型错误")
# 计算"新增"与"删除"两组信号
add_input = _get_add_input(old_data, new_data)
del_input = _get_del_input(old_data, new_data)
datalog_len = _get_datalog_len(old_data)
# 先删除旧列(逆序删除是为了让被删列的索引不会影响后续操作)
for name, info in reversed(del_input.items()):
old_sheet.delete_cols(info['column'])
logger.info(f"删除第{info['column']}\t信号名: {name}")
# 再插入新增列:先 insert_cols 占位,再填充属性行与数据点
for name, info in add_input.items():
old_sheet.insert_cols(info['column'])
for index in range(1,old_source_row):
if index == old_source_row -1 :
old_sheet.cell(index, info['column']).value = info['attributes'][new_source_row - 1]
# 1) 属性行(header 行以下至 source_row - 1
for index in range(1, old_source_row):
if index == old_source_row - 1:
# source_header 行:从 attributes 字典里取出 source_header 文本
# 注:attributes 的行号空间与 old_data 保持一致,应使用 old_source_row
old_sheet.cell(index, info['column']).value = info['attributes'][old_source_row - 1]
old_sheet.cell(index, info['column']).data_type = "str"
else:
old_sheet.cell(index, info['column']).value = info['attributes'][index]
old_sheet.cell(index, info['column']).data_type = "str"
# 2) 数据行:根据值类型设置 data_type
for index in range(old_source_row + 1, datalog_len + old_source_row + 1):
try:
value = int(info['datalog'][0].value)
@@ -75,14 +89,14 @@ def update_case_excel(filename: str, old_data: dict, new_data: dict) -> None:
def _get_add_input(old_data: dict, new_data: dict) -> dict[str, SignalData]:
"""获取新增输入数据
"""计算"新增"信号集合:存在于 new_data 但不存在于 old_data 的信号。
Args:
old_data: 旧数据字典
new_data: 新数据字典
old_data: 旧数据字典
new_data: 新数据字典
Returns:
新增信号字典
新增信号字典
"""
add_input: dict[str, SignalData] = {}
@@ -93,14 +107,14 @@ def _get_add_input(old_data: dict, new_data: dict) -> dict[str, SignalData]:
def _get_del_input(old_data: dict, new_data: dict) -> dict[str, SignalData]:
"""获取删除输入数据
"""计算"删除"信号集合:存在于 old_data 但不存在于 new_data 的信号。
Args:
old_data: 旧数据字典
new_data: 新数据字典
old_data: 旧数据字典
new_data: 新数据字典
Returns:
删除信号字典
删除信号字典
"""
del_input: dict[str, SignalData] = {}
@@ -111,14 +125,18 @@ def _get_del_input(old_data: dict, new_data: dict) -> dict[str, SignalData]:
def _get_datalog_len(old_data: dict) -> int:
"""获取数据日志长度
"""获取数据中第一个信号的 datalog 长度,作为新增列填充行数的基准。
假设 old_data 中各信号 datalog 长度一致来自同一时间轴
Args:
old_data: 数据字典
old_data: 数据字典
Returns:
数据日志长度
数据日志长度行数空字典返回 0
"""
for name in old_data.keys():
return len(old_data[name]['datalog'])
if not old_data:
return 0
# 取首项的 datalog 长度即可,无需遍历整张字典
first_name = next(iter(old_data))
return len(old_data[first_name]['datalog'])
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FrameProject</class>
<widget class="QFrame" name="FrameProject">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>740</width>
<height>95</height>
</rect>
</property>
<property name="windowTitle">
<string>Frame</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>模型项目:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>数据文件:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>测试用例:</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLineEdit" name="lineEditName">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="lineEditDataPath">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLineEdit" name="lineEditFilePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QPushButton" name="pushButtonDataPath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>选择数据文件</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButtonFilePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>选择测试用例</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+114
View File
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'mil_project.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QLabel,
QLineEdit, QPushButton, QSizePolicy, QWidget)
class Ui_FrameProject(object):
def setupUi(self, FrameProject):
if not FrameProject.objectName():
FrameProject.setObjectName(u"FrameProject")
FrameProject.resize(740, 95)
self.gridLayout_4 = QGridLayout(FrameProject)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout = QGridLayout()
self.gridLayout.setObjectName(u"gridLayout")
self.label = QLabel(FrameProject)
self.label.setObjectName(u"label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.label_2 = QLabel(FrameProject)
self.label_2.setObjectName(u"label_2")
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
self.label_3 = QLabel(FrameProject)
self.label_3.setObjectName(u"label_3")
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
self.gridLayout_4.addLayout(self.gridLayout, 0, 0, 1, 1)
self.gridLayout_2 = QGridLayout()
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.lineEditName = QLineEdit(FrameProject)
self.lineEditName.setObjectName(u"lineEditName")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lineEditName.sizePolicy().hasHeightForWidth())
self.lineEditName.setSizePolicy(sizePolicy)
self.gridLayout_2.addWidget(self.lineEditName, 0, 0, 1, 1)
self.lineEditDataPath = QLineEdit(FrameProject)
self.lineEditDataPath.setObjectName(u"lineEditDataPath")
sizePolicy.setHeightForWidth(self.lineEditDataPath.sizePolicy().hasHeightForWidth())
self.lineEditDataPath.setSizePolicy(sizePolicy)
self.gridLayout_2.addWidget(self.lineEditDataPath, 1, 0, 1, 1)
self.lineEditFilePath = QLineEdit(FrameProject)
self.lineEditFilePath.setObjectName(u"lineEditFilePath")
sizePolicy.setHeightForWidth(self.lineEditFilePath.sizePolicy().hasHeightForWidth())
self.lineEditFilePath.setSizePolicy(sizePolicy)
self.gridLayout_2.addWidget(self.lineEditFilePath, 2, 0, 1, 1)
self.gridLayout_4.addLayout(self.gridLayout_2, 0, 1, 1, 1)
self.gridLayout_3 = QGridLayout()
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.pushButtonDataPath = QPushButton(FrameProject)
self.pushButtonDataPath.setObjectName(u"pushButtonDataPath")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.pushButtonDataPath.sizePolicy().hasHeightForWidth())
self.pushButtonDataPath.setSizePolicy(sizePolicy1)
self.gridLayout_3.addWidget(self.pushButtonDataPath, 0, 0, 1, 1)
self.pushButtonFilePath = QPushButton(FrameProject)
self.pushButtonFilePath.setObjectName(u"pushButtonFilePath")
sizePolicy1.setHeightForWidth(self.pushButtonFilePath.sizePolicy().hasHeightForWidth())
self.pushButtonFilePath.setSizePolicy(sizePolicy1)
self.gridLayout_3.addWidget(self.pushButtonFilePath, 1, 0, 1, 1)
self.gridLayout_4.addLayout(self.gridLayout_3, 0, 2, 1, 1)
self.retranslateUi(FrameProject)
QMetaObject.connectSlotsByName(FrameProject)
# setupUi
def retranslateUi(self, FrameProject):
FrameProject.setWindowTitle(QCoreApplication.translate("FrameProject", u"Frame", None))
self.label.setText(QCoreApplication.translate("FrameProject", u"\u6a21\u578b\u9879\u76ee\uff1a", None))
self.label_2.setText(QCoreApplication.translate("FrameProject", u"\u6570\u636e\u6587\u4ef6\uff1a", None))
self.label_3.setText(QCoreApplication.translate("FrameProject", u"\u6d4b\u8bd5\u7528\u4f8b\uff1a", None))
self.lineEditName.setText("")
self.pushButtonDataPath.setText(QCoreApplication.translate("FrameProject", u"\u9009\u62e9\u6570\u636e\u6587\u4ef6", None))
self.pushButtonFilePath.setText(QCoreApplication.translate("FrameProject", u"\u9009\u62e9\u6d4b\u8bd5\u7528\u4f8b", None))
# retranslateUi
+26
View File
@@ -0,0 +1,26 @@
import os
import logging
from .mil_tool_ui import Ui_FrameMILTool
from PySide6.QtWidgets import QFrame
from mil.core import Config
logger = logging.getLogger(__name__)
CONF = "mil.json"
class FrameMILTool(QFrame,Ui_FrameMILTool):
def __init__(self,parent=None):
super().__init__(parent)
self.setupUi(self)
self.initUI()
def initUI(self):
self.config = Config()
if os.path.exists(CONF):
self.config.load_config(CONF)
else:
self.config.save_config(CONF)
pass
+367
View File
@@ -0,0 +1,367 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FrameMILTool</class>
<widget class="QFrame" name="FrameMILTool">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>706</width>
<height>308</height>
</rect>
</property>
<property name="windowTitle">
<string>Frame</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">
<enum>QTabWidget::TabPosition::South</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="widget">
<attribute name="title">
<string>功能面板</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_12">
<item row="0" column="0">
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>项目</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QLineEdit" name="FilePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="DataPath">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="FileButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>累计时间</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="radioButtonFile">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QPushButton" name="pushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>生成</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>测试用例 </string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>数据文件 </string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="DataButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="radioButtonData">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>名称</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>名称</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="lineEdit_4"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>类型</string>
</property>
</widget>
</item>
<item row="1" column="1" rowspan="2">
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
<item row="1" column="2">
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>类型</string>
</property>
</widget>
</item>
<item row="1" column="3" rowspan="2">
<widget class="QLineEdit" name="lineEdit_5"/>
</item>
<item row="2" column="0" rowspan="2">
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>值定义</string>
</property>
</widget>
</item>
<item row="2" column="2" rowspan="2">
<widget class="QLabel" name="label_9">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>值定义</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_3"/>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="lineEdit_6"/>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>加载测试用例</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="pushButton_4">
<property name="text">
<string>删除</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="pushButton_5">
<property name="text">
<string>查找</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>替换</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QPushButton" name="UpdateButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>更新</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>项目列表</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_11">
<item row="0" column="0">
<widget class="QListWidget" name="listWidget"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+316
View File
@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'mil_tool.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFrame,
QGridLayout, QLabel, QLineEdit, QListWidget,
QListWidgetItem, QPushButton, QRadioButton, QSizePolicy,
QSpacerItem, QTabWidget, QWidget)
class Ui_FrameMILTool(object):
def setupUi(self, FrameMILTool):
if not FrameMILTool.objectName():
FrameMILTool.setObjectName(u"FrameMILTool")
FrameMILTool.resize(706, 308)
self.gridLayout_8 = QGridLayout(FrameMILTool)
self.gridLayout_8.setObjectName(u"gridLayout_8")
self.tabWidget = QTabWidget(FrameMILTool)
self.tabWidget.setObjectName(u"tabWidget")
self.tabWidget.setTabPosition(QTabWidget.TabPosition.South)
self.widget = QWidget()
self.widget.setObjectName(u"widget")
self.gridLayout_12 = QGridLayout(self.widget)
self.gridLayout_12.setObjectName(u"gridLayout_12")
self.frame_3 = QFrame(self.widget)
self.frame_3.setObjectName(u"frame_3")
self.frame_3.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
self.gridLayout_3 = QGridLayout(self.frame_3)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.gridLayout_2 = QGridLayout()
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.label = QLabel(self.frame_3)
self.label.setObjectName(u"label")
self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1)
self.comboBox = QComboBox(self.frame_3)
self.comboBox.setObjectName(u"comboBox")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.comboBox.sizePolicy().hasHeightForWidth())
self.comboBox.setSizePolicy(sizePolicy)
self.comboBox.setStyleSheet(u"color: rgb(255, 255, 255);")
self.gridLayout_2.addWidget(self.comboBox, 0, 1, 1, 1)
self.gridLayout_3.addLayout(self.gridLayout_2, 0, 0, 1, 1)
self.gridLayout = QGridLayout()
self.gridLayout.setObjectName(u"gridLayout")
self.FilePath = QLineEdit(self.frame_3)
self.FilePath.setObjectName(u"FilePath")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.FilePath.sizePolicy().hasHeightForWidth())
self.FilePath.setSizePolicy(sizePolicy1)
self.FilePath.setStyleSheet(u"color: rgb(255, 255, 255);")
self.gridLayout.addWidget(self.FilePath, 1, 2, 1, 1)
self.DataPath = QLineEdit(self.frame_3)
self.DataPath.setObjectName(u"DataPath")
sizePolicy1.setHeightForWidth(self.DataPath.sizePolicy().hasHeightForWidth())
self.DataPath.setSizePolicy(sizePolicy1)
self.DataPath.setStyleSheet(u"color: rgb(255, 255, 255);")
self.gridLayout.addWidget(self.DataPath, 0, 2, 1, 1)
self.FileButton = QPushButton(self.frame_3)
self.FileButton.setObjectName(u"FileButton")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.FileButton.sizePolicy().hasHeightForWidth())
self.FileButton.setSizePolicy(sizePolicy2)
self.gridLayout.addWidget(self.FileButton, 1, 3, 1, 1)
self.checkBox = QCheckBox(self.frame_3)
self.checkBox.setObjectName(u"checkBox")
self.gridLayout.addWidget(self.checkBox, 0, 4, 1, 1)
self.radioButtonFile = QRadioButton(self.frame_3)
self.radioButtonFile.setObjectName(u"radioButtonFile")
self.gridLayout.addWidget(self.radioButtonFile, 1, 0, 1, 1)
self.pushButton = QPushButton(self.frame_3)
self.pushButton.setObjectName(u"pushButton")
sizePolicy2.setHeightForWidth(self.pushButton.sizePolicy().hasHeightForWidth())
self.pushButton.setSizePolicy(sizePolicy2)
self.gridLayout.addWidget(self.pushButton, 1, 4, 1, 1)
self.label_3 = QLabel(self.frame_3)
self.label_3.setObjectName(u"label_3")
self.gridLayout.addWidget(self.label_3, 1, 1, 1, 1)
self.label_2 = QLabel(self.frame_3)
self.label_2.setObjectName(u"label_2")
self.gridLayout.addWidget(self.label_2, 0, 1, 1, 1)
self.DataButton = QPushButton(self.frame_3)
self.DataButton.setObjectName(u"DataButton")
sizePolicy2.setHeightForWidth(self.DataButton.sizePolicy().hasHeightForWidth())
self.DataButton.setSizePolicy(sizePolicy2)
self.gridLayout.addWidget(self.DataButton, 0, 3, 1, 1)
self.radioButtonData = QRadioButton(self.frame_3)
self.radioButtonData.setObjectName(u"radioButtonData")
self.radioButtonData.setChecked(True)
self.gridLayout.addWidget(self.radioButtonData, 0, 0, 1, 1)
self.gridLayout_3.addLayout(self.gridLayout, 1, 0, 1, 1)
self.gridLayout_12.addWidget(self.frame_3, 0, 0, 1, 1)
self.frame = QFrame(self.widget)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.gridLayout_6 = QGridLayout(self.frame)
self.gridLayout_6.setObjectName(u"gridLayout_6")
self.gridLayout_5 = QGridLayout()
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.label_4 = QLabel(self.frame)
self.label_4.setObjectName(u"label_4")
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
sizePolicy3.setHorizontalStretch(0)
sizePolicy3.setVerticalStretch(0)
sizePolicy3.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())
self.label_4.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_4, 0, 0, 1, 1)
self.lineEdit = QLineEdit(self.frame)
self.lineEdit.setObjectName(u"lineEdit")
self.gridLayout_5.addWidget(self.lineEdit, 0, 1, 1, 1)
self.label_7 = QLabel(self.frame)
self.label_7.setObjectName(u"label_7")
sizePolicy3.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth())
self.label_7.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_7, 0, 2, 1, 1)
self.lineEdit_4 = QLineEdit(self.frame)
self.lineEdit_4.setObjectName(u"lineEdit_4")
self.gridLayout_5.addWidget(self.lineEdit_4, 0, 3, 1, 1)
self.label_5 = QLabel(self.frame)
self.label_5.setObjectName(u"label_5")
sizePolicy3.setHeightForWidth(self.label_5.sizePolicy().hasHeightForWidth())
self.label_5.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_5, 1, 0, 1, 1)
self.lineEdit_2 = QLineEdit(self.frame)
self.lineEdit_2.setObjectName(u"lineEdit_2")
self.gridLayout_5.addWidget(self.lineEdit_2, 1, 1, 2, 1)
self.label_8 = QLabel(self.frame)
self.label_8.setObjectName(u"label_8")
sizePolicy3.setHeightForWidth(self.label_8.sizePolicy().hasHeightForWidth())
self.label_8.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_8, 1, 2, 1, 1)
self.lineEdit_5 = QLineEdit(self.frame)
self.lineEdit_5.setObjectName(u"lineEdit_5")
self.gridLayout_5.addWidget(self.lineEdit_5, 1, 3, 2, 1)
self.label_6 = QLabel(self.frame)
self.label_6.setObjectName(u"label_6")
sizePolicy3.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth())
self.label_6.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_6, 2, 0, 2, 1)
self.label_9 = QLabel(self.frame)
self.label_9.setObjectName(u"label_9")
sizePolicy3.setHeightForWidth(self.label_9.sizePolicy().hasHeightForWidth())
self.label_9.setSizePolicy(sizePolicy3)
self.gridLayout_5.addWidget(self.label_9, 2, 2, 2, 1)
self.lineEdit_3 = QLineEdit(self.frame)
self.lineEdit_3.setObjectName(u"lineEdit_3")
self.gridLayout_5.addWidget(self.lineEdit_3, 3, 1, 1, 1)
self.lineEdit_6 = QLineEdit(self.frame)
self.lineEdit_6.setObjectName(u"lineEdit_6")
self.gridLayout_5.addWidget(self.lineEdit_6, 3, 3, 1, 1)
self.gridLayout_6.addLayout(self.gridLayout_5, 0, 0, 1, 1)
self.gridLayout_10 = QGridLayout()
self.gridLayout_10.setObjectName(u"gridLayout_10")
self.pushButton_2 = QPushButton(self.frame)
self.pushButton_2.setObjectName(u"pushButton_2")
sizePolicy.setHeightForWidth(self.pushButton_2.sizePolicy().hasHeightForWidth())
self.pushButton_2.setSizePolicy(sizePolicy)
self.gridLayout_10.addWidget(self.pushButton_2, 0, 0, 1, 1)
self.pushButton_4 = QPushButton(self.frame)
self.pushButton_4.setObjectName(u"pushButton_4")
self.gridLayout_10.addWidget(self.pushButton_4, 0, 4, 1, 1)
self.pushButton_5 = QPushButton(self.frame)
self.pushButton_5.setObjectName(u"pushButton_5")
self.gridLayout_10.addWidget(self.pushButton_5, 0, 2, 1, 1)
self.pushButton_3 = QPushButton(self.frame)
self.pushButton_3.setObjectName(u"pushButton_3")
self.gridLayout_10.addWidget(self.pushButton_3, 0, 3, 1, 1)
self.UpdateButton = QPushButton(self.frame)
self.UpdateButton.setObjectName(u"UpdateButton")
sizePolicy2.setHeightForWidth(self.UpdateButton.sizePolicy().hasHeightForWidth())
self.UpdateButton.setSizePolicy(sizePolicy2)
self.gridLayout_10.addWidget(self.UpdateButton, 0, 5, 1, 1)
self.gridLayout_6.addLayout(self.gridLayout_10, 1, 0, 1, 1)
self.gridLayout_12.addWidget(self.frame, 1, 0, 1, 1)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.gridLayout_12.addItem(self.verticalSpacer, 2, 0, 1, 1)
self.tabWidget.addTab(self.widget, "")
self.tab_2 = QWidget()
self.tab_2.setObjectName(u"tab_2")
self.gridLayout_11 = QGridLayout(self.tab_2)
self.gridLayout_11.setObjectName(u"gridLayout_11")
self.listWidget = QListWidget(self.tab_2)
self.listWidget.setObjectName(u"listWidget")
self.gridLayout_11.addWidget(self.listWidget, 0, 0, 1, 1)
self.tabWidget.addTab(self.tab_2, "")
self.gridLayout_8.addWidget(self.tabWidget, 0, 0, 1, 1)
self.retranslateUi(FrameMILTool)
self.tabWidget.setCurrentIndex(0)
QMetaObject.connectSlotsByName(FrameMILTool)
# setupUi
def retranslateUi(self, FrameMILTool):
FrameMILTool.setWindowTitle(QCoreApplication.translate("FrameMILTool", u"Frame", None))
self.label.setText(QCoreApplication.translate("FrameMILTool", u"\u9879\u76ee", None))
self.FileButton.setText(QCoreApplication.translate("FrameMILTool", u"...", None))
self.checkBox.setText(QCoreApplication.translate("FrameMILTool", u"\u7d2f\u8ba1\u65f6\u95f4", None))
self.radioButtonFile.setText("")
self.pushButton.setText(QCoreApplication.translate("FrameMILTool", u"\u751f\u6210", None))
self.label_3.setText(QCoreApplication.translate("FrameMILTool", u"\u6d4b\u8bd5\u7528\u4f8b ", None))
self.label_2.setText(QCoreApplication.translate("FrameMILTool", u"\u6570\u636e\u6587\u4ef6 ", None))
self.DataButton.setText(QCoreApplication.translate("FrameMILTool", u"...", None))
self.radioButtonData.setText("")
self.label_4.setText(QCoreApplication.translate("FrameMILTool", u"\u540d\u79f0", None))
self.label_7.setText(QCoreApplication.translate("FrameMILTool", u"\u540d\u79f0", None))
self.label_5.setText(QCoreApplication.translate("FrameMILTool", u"\u7c7b\u578b", None))
self.label_8.setText(QCoreApplication.translate("FrameMILTool", u"\u7c7b\u578b", None))
self.label_6.setText(QCoreApplication.translate("FrameMILTool", u"\u503c\u5b9a\u4e49", None))
self.label_9.setText(QCoreApplication.translate("FrameMILTool", u"\u503c\u5b9a\u4e49", None))
self.pushButton_2.setText(QCoreApplication.translate("FrameMILTool", u"\u52a0\u8f7d\u6d4b\u8bd5\u7528\u4f8b", None))
self.pushButton_4.setText(QCoreApplication.translate("FrameMILTool", u"\u5220\u9664", None))
self.pushButton_5.setText(QCoreApplication.translate("FrameMILTool", u"\u67e5\u627e", None))
self.pushButton_3.setText(QCoreApplication.translate("FrameMILTool", u"\u66ff\u6362", None))
self.UpdateButton.setText(QCoreApplication.translate("FrameMILTool", u"\u66f4\u65b0", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.widget), QCoreApplication.translate("FrameMILTool", u"\u529f\u80fd\u9762\u677f", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("FrameMILTool", u"\u9879\u76ee\u5217\u8868", None))
# retranslateUi
-92
View File
@@ -1,92 +0,0 @@
"""MIL SDK 核心数据模型"""
from dataclasses import dataclass, field
from typing import Any
@dataclass
class DataLog:
"""仿真数据日志记录
Attributes:
time: 时间戳(秒)
value: 信号值(可以是任意类型)
"""
time: float = 0.0
value: str = ""
@dataclass
class SignalData:
"""信号数据封装
Attributes:
signal_type: 信号类型
column: 列索引
datalog: 数据日志列表
"""
# signal_type: str | None = None
column: int = 0
attributes: dict[str, str] = field(default_factory=dict)
datalog: list[DataLog] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
"""转换为字典格式
Returns:
包含 signal_type、column、datalog 的字典
"""
return {
"column": self.column,
"attributes": self.attributes,
"datalog": self.datalog
}
@dataclass
class ExcelDataResult:
"""Excel 数据读取结果封装
提供对 Excel 数据的类型安全访问,隐藏内部实现细节
Attributes:
sheet_name: 工作表名称
source_row: Source: Input 所在行号
signals: 信号名称到信号数据的映射
"""
sheet_name: str = "Scenario1"
source_row: int = 0
signals: dict[str, SignalData] = field(default_factory=dict)
def get_signal(self, name: str) -> SignalData | None:
"""获取指定信号的数据
Args:
name: 信号名称
Returns:
信号数据对象,如果不存在返回 None
"""
return self.signals.get(name)
def get_signal_names(self) -> list[str]:
"""获取所有信号名称
Returns:
信号名称列表
"""
return list(self.signals.keys())
def to_dict(self) -> dict[str, Any]:
"""转换为字典格式(兼容旧 API
Returns:
包含所有信号的字典,保留原有的数据结构
"""
result = {
"sheet_name": self.sheet_name,
"source_row": self.source_row
}
for name, signal in self.signals.items():
result[name] = signal.to_dict()
return result
-26
View File
@@ -1,26 +0,0 @@
"""MIL SDK 自定义异常模块"""
class MILSDKError(Exception):
"""MIL SDK 基础异常类"""
pass
class ExcelReadError(MILSDKError):
"""Excel 文件读取错误(文件不存在、权限问题等)"""
pass
class ExcelFormatError(MILSDKError):
"""Excel 格式错误(缺少 Sheet、格式不匹配等)"""
pass
class CaseDataError(MILSDKError):
"""用例数据错误(信号不存在、类型错误等)"""
pass
class ExcelWriteError(MILSDKError):
"""Excel 文件写入错误(权限问题、保存失败等)"""
pass
-61
View File
@@ -1,61 +0,0 @@
"""日志配置模块"""
import logging
import sys
from pathlib import Path
from typing import Literal
def setup_logging(
level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO",
log_file: str | Path | None = "logs/mil_sdk.log",
console_output: bool = True,
) -> logging.Logger:
"""配置日志系统
Args:
level: 日志级别,默认 INFO
log_file: 日志文件路径,默认 logs/mil_sdk.log。设为 None 则不写入文件
console_output: 是否输出到控制台,默认 True
Returns:
根日志记录器
"""
logger = logging.getLogger()
logger.setLevel(level)
if logger.hasHandlers():
logger.handlers.clear()
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
if log_file is not None:
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
if console_output:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
return logger
def get_logger(name: str) -> logging.Logger:
"""获取指定名称的日志记录器
Args:
name: 日志记录器名称,通常使用 __name__
Returns:
日志记录器实例
"""
return logging.getLogger(name)
-47
View File
@@ -1,47 +0,0 @@
import pytest
from pathlib import Path
from openpyxl import Workbook
@pytest.fixture
def sample_excel_path() -> Path:
"""返回根目录下的 sample.xlsx 路径"""
path = Path(__file__).parent.parent / "sample.xlsx"
if not path.exists():
pytest.skip(f"测试文件 {path} 不存在")
return path
@pytest.fixture
def invalid_excel_path(tmp_path: Path) -> Path:
"""创建缺少 Scenario1 表的无效 Excel 文件"""
wb = Workbook()
wb.save(tmp_path / "invalid.xlsx")
return tmp_path / "invalid.xlsx"
@pytest.fixture
def sample_data_dict() -> dict:
"""返回示例数据字典"""
return {
"Scenario1": {
"headers": ["Column1", "Column2", "Column3"],
"rows": [
["Value1", "Value2", "Value3"],
["Value4", "Value5", "Value6"]
]
}
}
@pytest.fixture
def sample_sheets_dict() -> dict:
"""返回示例工作表字典"""
return {
"Scenario1": {
"A1": "Header1",
"B1": "Header2",
"A2": "Data1",
"B2": "Data2"
}
}
-20
View File
@@ -1,20 +0,0 @@
import pytest
from src.core.base import DataLog
def test_datalog_defaults():
log = DataLog()
assert log.time == 0.0
assert log.value == ""
def test_datalog_with_values():
log = DataLog(time=1.5, value="test")
assert log.time == 1.5
assert log.value == "test"
def test_datalog_equality():
log1 = DataLog(time=1.0, value="a")
log2 = DataLog(time=1.0, value="a")
assert log1 == log2
-292
View File
@@ -1,292 +0,0 @@
import pytest
from pathlib import Path
from openpyxl import Workbook
from src.core.mil_read_case_excel import (
read_excel_case,
_get_template_version,
_analysis_action,
)
from src.core.mil_create_data_excel import (
create_excel_case,
_init_data_log,
_analysis_case,
_analysis_step,
)
from src.core.exceptions import ExcelReadError, ExcelFormatError, CaseDataError
from src.core.base import DataLog
@pytest.fixture
def sample_data_dict():
"""返回示例数据字典"""
return {
"signal1": {
"type": "Type1",
"column": 2,
"datalog": [DataLog(0.0, "initial"), DataLog(1.0, "value1")]
},
"signal2": {
"type": "Type2",
"column": 3,
"datalog": [DataLog(0.0, "init2"), DataLog(1.0, "value2")]
}
}
@pytest.fixture
def sample_sheets_dict():
"""返回示例工作表字典"""
return {
"TestSheet": {
"TestCase1": {
"enable": False,
"step": {
"step0": {
"name": "Step1",
"time": 1.0,
"action": {"signal1": "new_value1"}
},
"step1": {
"name": "Step2",
"time": 2.0,
"action": {"signal2": "new_value2"}
}
}
},
"TestCase2": {
"enable": True,
"step": {}
}
}
}
def test_get_template_version():
"""验证获取模板版本号"""
wb = Workbook()
sheet = wb.active
sheet.cell(2, 1, "v1.0.0")
sheet.cell(3, 1, None)
version = _get_template_version(sheet)
assert version == "v1.0.0"
def test_get_template_version_empty():
"""验证空工作表的版本号"""
wb = Workbook()
sheet = wb.active
version = _get_template_version(sheet)
assert version is None
def test_analysis_action_valid(sample_data_dict):
"""验证解析有效操作字符串"""
wb = Workbook()
sheet = wb.create_sheet("TestSheet")
sheet.cell(1, 1, "Test")
action = _analysis_action(sheet, 1, 3, "signal1=newvalue", sample_data_dict, "test.xlsx")
assert "signal1" in action
assert action["signal1"] == "newvalue"
def test_analysis_action_multiple(sample_data_dict):
"""验证解析多个操作"""
wb = Workbook()
sheet = wb.create_sheet("TestSheet")
sheet.cell(1, 1, "Test")
action = _analysis_action(sheet, 1, 3, "signal1=v1; signal2=v2", sample_data_dict, "test.xlsx")
assert "signal1" in action
assert "signal2" in action
assert action["signal1"] == "v1"
assert action["signal2"] == "v2"
def test_analysis_action_invalid_signal(sample_data_dict):
"""验证解析无效信号时抛出异常"""
wb = Workbook()
sheet = wb.create_sheet("TestSheet")
sheet.cell(1, 1, "Test")
with pytest.raises(CaseDataError, match="信号 .* 不存在"):
_analysis_action(sheet, 1, 3, "invalid_signal=value", sample_data_dict, "test.xlsx")
def test_init_data_log(sample_data_dict):
"""验证初始化数据日志"""
data_copy = {k: v.copy() for k, v in sample_data_dict.items()}
for name in data_copy:
data_copy[name]["datalog"] = data_copy[name]["datalog"].copy()
_init_data_log(data_copy)
for name in data_copy:
assert len(data_copy[name]["datalog"]) == 1
assert data_copy[name]["datalog"][0].time == 0.0
def test_analysis_step_with_action(sample_data_dict):
"""验证分析带操作的步骤"""
step_dict = {
"step0": {
"name": "TestStep",
"time": 1.5,
"action": {"signal1": "newvalue"}
}
}
data_copy = {k: v.copy() for k, v in sample_data_dict.items()}
for name in data_copy:
data_copy[name]["datalog"] = data_copy[name]["datalog"].copy()
_analysis_step(step_dict, data_copy)
assert len(data_copy["signal1"]["datalog"]) == 3
assert data_copy["signal1"]["datalog"][-1].time == 1.5
assert data_copy["signal1"]["datalog"][-1].value == "newvalue"
def test_analysis_step_without_action(sample_data_dict):
"""验证分析不带操作的步骤"""
step_dict = {
"step0": {
"name": "TestStep",
"time": 1.0
}
}
data_copy = {k: v.copy() for k, v in sample_data_dict.items()}
for name in data_copy:
data_copy[name]["datalog"] = data_copy[name]["datalog"].copy()
initial_length = len(data_copy["signal1"]["datalog"])
_analysis_step(step_dict, data_copy)
assert len(data_copy["signal1"]["datalog"]) == initial_length + 1
def test_analysis_case(sample_data_dict, sample_sheets_dict):
"""验证用例分析"""
result = _analysis_case(sample_data_dict, sample_sheets_dict)
assert "TestCase1" in result
assert "TestCase2" not in result
def test_analysis_case_skips_enabled_cases(sample_data_dict):
"""验证跳过早启用的用例"""
sheets_dict = {
"TestSheet": {
"EnabledCase": {
"enable": True,
"step": {}
}
}
}
result = _analysis_case(sample_data_dict, sheets_dict)
assert "EnabledCase" not in result
def test_read_excel_case_file_not_found():
"""验证文件不存在时抛出异常"""
with pytest.raises(ExcelReadError, match="不存在"):
read_excel_case("nonexistent.xlsx", {}, False)
def test_read_excel_case_missing_atech_sheet(tmp_path: Path):
"""验证缺少 Atech-Hefei 表时抛出异常"""
wb = Workbook()
wb.create_sheet("OtherSheet")
invalid_path = tmp_path / "invalid.xlsx"
wb.save(invalid_path)
with pytest.raises(ExcelFormatError, match="缺少 Atech-Hefei 表"):
read_excel_case(invalid_path, {}, False)
def test_read_excel_case_missing_version(tmp_path: Path, sample_data_dict):
"""验证缺少版本号时抛出异常"""
wb = Workbook()
wb.create_sheet("Atech-Hefei")
wb.create_sheet("TestSheet")
invalid_path = tmp_path / "no_version.xlsx"
wb.save(invalid_path)
with pytest.raises(ExcelFormatError, match="缺少模板版本号"):
read_excel_case(invalid_path, sample_data_dict, False)
def test_read_excel_case_success(tmp_path: Path, sample_data_dict):
"""验证成功读取用例模板"""
wb = Workbook()
wb.remove(wb.active)
version_sheet = wb.create_sheet("Atech-Hefei")
version_sheet.cell(2, 1, "v1.0.0")
test_sheet = wb.create_sheet("TestSheet")
test_sheet.cell(2, 1, "TestCase1")
test_sheet.cell(2, 2, "未完成")
test_sheet.cell(2, 3, None)
test_sheet.cell(2, 4, "Step1")
test_sheet.cell(2, 5, 1.0)
test_sheet.cell(2, 6, None)
test_sheet.cell(3, 1, "TestCase1")
test_sheet.cell(3, 2, "未完成")
test_sheet.cell(3, 3, None)
test_sheet.cell(3, 4, "Step2")
test_sheet.cell(3, 5, 2.0)
test_sheet.cell(3, 6, None)
valid_path = tmp_path / "valid.xlsx"
wb.save(valid_path)
result = read_excel_case(valid_path, sample_data_dict, False)
assert "TestSheet" in result
assert "TestCase1" in result["TestSheet"]
def test_read_excel_case_missing_title(tmp_path: Path, sample_data_dict):
"""验证缺少标题时抛出异常"""
wb = Workbook()
version_sheet = wb.create_sheet("Atech-Hefei")
version_sheet.cell(2, 1, "v1.0.0")
test_sheet = wb.create_sheet("TestSheet")
test_sheet.cell(2, 1, None)
invalid_path = tmp_path / "missing_title.xlsx"
wb.save(invalid_path)
with pytest.raises(CaseDataError, match="必须有标题名"):
read_excel_case(invalid_path, sample_data_dict, False)
def test_read_excel_case_invalid_time(tmp_path: Path, sample_data_dict):
"""验证时间格式错误时抛出异常"""
wb = Workbook()
wb.remove(wb.active)
version_sheet = wb.create_sheet("Atech-Hefei")
version_sheet.cell(2, 1, "v1.0.0")
test_sheet = wb.create_sheet("TestSheet")
test_sheet.cell(2, 1, "TestCase1")
test_sheet.cell(2, 2, "未完成")
test_sheet.cell(2, 4, "Step1")
test_sheet.cell(2, 5, "invalid_time")
test_sheet.cell(2, 6, None)
invalid_path = tmp_path / "invalid_time.xlsx"
wb.save(invalid_path)
with pytest.raises(CaseDataError, match="时间必须是数字"):
read_excel_case(invalid_path, sample_data_dict, False)
-150
View File
@@ -1,150 +0,0 @@
import pytest
from pathlib import Path
from openpyxl import Workbook
from src.core.mil_read_data_excel import read_excel_data, ExcelReaderConfig
from src.core.exceptions import ExcelReadError, ExcelFormatError
def test_read_excel_data_returns_required_keys(sample_excel_path: Path):
"""验证返回结果包含必需键"""
result = read_excel_data(sample_excel_path)
assert "wb" in result
assert "sheet" in result
assert "source_row" in result
def test_read_excel_data_contains_signals(sample_excel_path: Path):
"""验证能解析出信号数据"""
result = read_excel_data(sample_excel_path)
signal_keys = [k for k in result.keys() if k not in ("wb", "sheet", "source_row")]
assert len(signal_keys) > 0, "应至少包含一个信号"
for name in signal_keys:
assert "type" in result[name]
assert "column" in result[name]
assert "datalog" in result[name]
def test_read_excel_file_not_found():
"""文件不存在时抛出 ExcelReadError"""
with pytest.raises(ExcelReadError, match="不存在"):
read_excel_data("nonexistent.xlsx")
def test_read_excel_missing_sheet(tmp_path: Path):
"""缺少 Scenario1 表时抛出 ExcelFormatError"""
wb = Workbook()
wb.create_sheet("WrongSheet")
invalid_path = tmp_path / "invalid.xlsx"
wb.save(invalid_path)
with pytest.raises(ExcelFormatError, match="缺少"):
read_excel_data(invalid_path)
def test_read_excel_missing_source_header(tmp_path: Path):
"""缺少 Source: Input 标记时抛出 ExcelFormatError"""
wb = Workbook()
sheet = wb.active
sheet.title = ExcelReaderConfig().sheet_name
sheet.cell(1, 1, "time")
sheet.cell(1, 2, "signal1")
invalid_path = tmp_path / "missing_header.xlsx"
wb.save(invalid_path)
with pytest.raises(ExcelFormatError, match="缺少"):
read_excel_data(invalid_path)
def test_datalog_is_list(sample_excel_path: Path):
"""验证 datalog 是 DataLog 对象列表"""
result = read_excel_data(sample_excel_path)
signal_keys = [k for k in result.keys() if k not in ("wb", "sheet", "source_row")]
assert len(signal_keys) > 0
first_signal = result[signal_keys[0]]
assert len(first_signal["datalog"]) > 0
assert hasattr(first_signal["datalog"][0], "time")
assert hasattr(first_signal["datalog"][0], "value")
def test_read_excel_data_return_object(sample_excel_path: Path):
"""验证 return_object=True 时返回 ExcelDataResult 对象"""
from src.core.base import ExcelDataResult, SignalData
result = read_excel_data(sample_excel_path, return_object=True)
assert isinstance(result, ExcelDataResult)
assert result.sheet_name == "Scenario1"
assert result.source_row > 0
assert len(result.signals) > 0
def test_read_excel_data_signal_data_access(sample_excel_path: Path):
"""验证 ExcelDataResult 的信号访问方法"""
from src.core.base import SignalData
result = read_excel_data(sample_excel_path, return_object=True)
signal_names = result.get_signal_names()
assert len(signal_names) > 0
first_signal_name = signal_names[0]
signal = result.get_signal(first_signal_name)
assert isinstance(signal, SignalData)
assert signal.datalog is not None
def test_excel_reader_config_defaults():
"""验证 ExcelReaderConfig 默认值"""
config = ExcelReaderConfig()
assert config.sheet_name == "Scenario1"
assert config.source_header == "Source: Input"
assert config.time_column == 1
assert config.header_row == 1
assert config.type_row == 3
assert config.data_start_row_offset == 1
def test_excel_reader_config_custom():
"""验证 ExcelReaderConfig 自定义值"""
config = ExcelReaderConfig(
sheet_name="CustomSheet",
source_header="CustomHeader",
time_column=2,
header_row=2,
type_row=4,
data_start_row_offset=2
)
assert config.sheet_name == "CustomSheet"
assert config.source_header == "CustomHeader"
assert config.time_column == 2
assert config.header_row == 2
assert config.type_row == 4
assert config.data_start_row_offset == 2
def test_read_excel_with_custom_config(tmp_path: Path):
"""验证使用自定义配置读取 Excel"""
wb = Workbook()
sheet = wb.active
sheet.title = "CustomSheet"
sheet.cell(1, 1, "time")
sheet.cell(1, 2, "signal1")
sheet.cell(2, 1, "CustomHeader")
sheet.cell(2, 2, "CustomHeader")
sheet.cell(3, 1, "Type1")
sheet.cell(4, 1, 0.0)
sheet.cell(4, 2, "value1")
sheet.cell(5, 1, 1.0)
sheet.cell(5, 2, "value2")
custom_path = tmp_path / "custom.xlsx"
wb.save(custom_path)
config = ExcelReaderConfig(
sheet_name="CustomSheet",
source_header="CustomHeader"
)
result = read_excel_data(custom_path, return_object=True, config=config)
assert result.sheet_name == "CustomSheet"
assert "signal1" in result.signals