完成初版
This commit is contained in:
@@ -173,4 +173,6 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
*.xlsx
|
||||
*.md
|
||||
|
||||
|
||||
@@ -1,2 +1,29 @@
|
||||
# mil_sdk
|
||||
|
||||
MIL (Model-in-the-Loop) SDK for reading simulation data from Excel files.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from src.core import read_excel_data, DataLog
|
||||
|
||||
result = read_excel_data("path/to/simulation.xlsx")
|
||||
print(result["signal1"]["datalog"]) # List[DataLog]
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Python >= 3.10
|
||||
- openpyxl >= 3.0.0
|
||||
@@ -0,0 +1,26 @@
|
||||
"""main.py - MIL SDK 测试示例"""
|
||||
from pathlib import Path
|
||||
from src.core import setup_logging, read_excel_data,read_excel_case,create_excel_case
|
||||
|
||||
|
||||
def main():
|
||||
setup_logging()
|
||||
|
||||
sample_path = Path(__file__).parent / "sample.xlsx"
|
||||
case_path = Path(__file__).parent / "case.xlsx"
|
||||
excel_path = Path(__file__).parent
|
||||
|
||||
|
||||
# print(f"读取文件: {sample_path}")
|
||||
data_dict = read_excel_data(sample_path, return_object=False)
|
||||
pass
|
||||
# print(data_dict)
|
||||
case_dict = read_excel_case(case_path,data_dict,True)
|
||||
pass
|
||||
create_excel_case(excel_path,data_dict,case_dict)
|
||||
# print(case_dict)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
[project]
|
||||
name = "mil_sdk"
|
||||
version = "0.1.0"
|
||||
description = "MIL SDK for reading simulation data from Excel files"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openpyxl>=3.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"mypy>=1.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
@@ -0,0 +1,7 @@
|
||||
# mil_sdk 依赖列表
|
||||
|
||||
# 主要依赖
|
||||
openpyxl>=3.0.0
|
||||
|
||||
# 开发依赖
|
||||
pytest>=7.0.0
|
||||
@@ -0,0 +1,46 @@
|
||||
"""MIL SDK 核心模块
|
||||
|
||||
提供 MIL 仿真数据读取功能
|
||||
|
||||
使用示例:
|
||||
from src.core import setup_logging, read_excel_data
|
||||
|
||||
setup_logging()
|
||||
result = read_excel_data("simulation.xlsx")
|
||||
"""
|
||||
import logging
|
||||
|
||||
from .base import DataLog, SignalData, ExcelDataResult
|
||||
from .mil_data_excel import read_excel_data, get_data_log, ExcelReaderConfig
|
||||
from .mil_case_excel import create_excel_case, read_excel_case
|
||||
from .exceptions import (
|
||||
MILSDKError,
|
||||
ExcelReadError,
|
||||
ExcelFormatError,
|
||||
CaseDataError,
|
||||
ExcelWriteError,
|
||||
)
|
||||
from .logging_config import setup_logging, get_logger
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DataLog",
|
||||
"SignalData",
|
||||
"ExcelDataResult",
|
||||
"ExcelReaderConfig",
|
||||
"read_excel_data",
|
||||
"get_data_log",
|
||||
"read_excel_case",
|
||||
"create_excel_case",
|
||||
"MILSDKError",
|
||||
"ExcelReadError",
|
||||
"ExcelFormatError",
|
||||
"CaseDataError",
|
||||
"ExcelWriteError",
|
||||
"setup_logging",
|
||||
"get_logger",
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""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
|
||||
datalog: list[DataLog] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式
|
||||
|
||||
Returns:
|
||||
包含 signal_type、column、datalog 的字典
|
||||
"""
|
||||
return {
|
||||
"type": self.signal_type,
|
||||
"column": self.column,
|
||||
"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
|
||||
@@ -0,0 +1,26 @@
|
||||
"""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
|
||||
@@ -0,0 +1,61 @@
|
||||
"""日志配置模块"""
|
||||
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] %(name)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)
|
||||
@@ -0,0 +1,333 @@
|
||||
"""MIL 测试用例 Excel 生成模块"""
|
||||
import copy
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from .base import DataLog
|
||||
from .exceptions import ExcelReadError, ExcelFormatError, CaseDataError, ExcelWriteError
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_excel_case(
|
||||
excel_path: str,
|
||||
data_dict: dict,
|
||||
addTimeEn: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""读取 Excel 用例模板
|
||||
|
||||
Args:
|
||||
excel_path: Excel 模板文件路径
|
||||
data_dict: 信号数据字典
|
||||
addTimeEn: 是否累加时间
|
||||
|
||||
Returns:
|
||||
用例字典
|
||||
|
||||
Raises:
|
||||
ExcelReadError: 文件读取失败
|
||||
ExcelFormatError: 格式错误
|
||||
CaseDataError: 用例数据错误
|
||||
"""
|
||||
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)}")
|
||||
except FileNotFoundError:
|
||||
raise ExcelReadError(f"Excel文件 {excel_path} 不存在")
|
||||
except Exception as e:
|
||||
logger.error(f"读取 Excel 文件失败: {e}")
|
||||
raise ExcelReadError(f"无法读取Excel文件 {excel_path}: {str(e)}")
|
||||
|
||||
try:
|
||||
version = __get_template_version(wb["Atech-Hefei"])
|
||||
if version is None:
|
||||
logger.error("缺少模板版本号")
|
||||
raise ExcelFormatError("Excel格式错误,缺少模板版本号")
|
||||
logger.info(f"用例模板版本号: {version}")
|
||||
del wb["Atech-Hefei"]
|
||||
except KeyError:
|
||||
logger.error(f"缺少 'Atech-Hefei' 表")
|
||||
raise ExcelFormatError(f"Excel文件 {excel_path} 格式错误,缺少 Atech-Hefei 表")
|
||||
|
||||
result_dict = {}
|
||||
sheet_count = 0
|
||||
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:
|
||||
if sheet.cell(row=row, column=1).value is not None:
|
||||
new_head = sheet.cell(row=row, column=1).value
|
||||
if new_head is None:
|
||||
raise CaseDataError(
|
||||
f"Excel文件 {excel_path} 格式错误,{sheet.title} 第 {row} 行第 1 列必须有标题名"
|
||||
)
|
||||
if new_head != old_head:
|
||||
step_id = 0
|
||||
old_time = 0.0
|
||||
old_head = new_head
|
||||
|
||||
result_dict[sheet.title][new_head] = {}
|
||||
result_dict[sheet.title][new_head]["step"] = {}
|
||||
|
||||
if sheet.cell(row=row, column=2).value == '完成测试':
|
||||
result_dict[sheet.title][new_head]["enable"] = True
|
||||
else:
|
||||
result_dict[sheet.title][new_head]["enable"] = False
|
||||
|
||||
step_name = sheet.cell(row, 4).value
|
||||
step_time = sheet.cell(row, 5).value
|
||||
if step_time is None:
|
||||
break
|
||||
try:
|
||||
step_time = float(step_time)
|
||||
except (ValueError, TypeError):
|
||||
raise CaseDataError(
|
||||
f"Excel文件 {excel_path} 格式错误,{sheet.title} 第 {row} 行第 5 列时间必须是数字"
|
||||
)
|
||||
|
||||
result_dict[sheet.title][new_head]["step"][f"step{step_id}"] = {}
|
||||
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["name"] = step_name
|
||||
|
||||
if addTimeEn:
|
||||
old_time += step_time
|
||||
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["time"] = old_time
|
||||
|
||||
strings = sheet.cell(row, 3).value
|
||||
if strings is not None:
|
||||
result_dict[sheet.title][new_head]["step"][f"step{step_id}"]["action"] = \
|
||||
__analysis_action(sheet, row, 3, strings, data_dict, excel_path)
|
||||
step_id += 1
|
||||
row += 1
|
||||
|
||||
logger.info(f"用例模板读取完成,共 {sheet_count} 个表,{sum(len(v) for v in result_dict.values())} 个用例")
|
||||
return result_dict
|
||||
|
||||
|
||||
def create_excel_case(
|
||||
excel_path: str,
|
||||
data_dict: dict,
|
||||
sheets_dict: dict,
|
||||
) -> None:
|
||||
"""生成测试用例 Excel 文件
|
||||
|
||||
Args:
|
||||
excel_path: 输出目录路径
|
||||
data_dict: 数据字典
|
||||
sheets_dict: 工作表字典
|
||||
|
||||
Raises:
|
||||
CaseDataError: 数据异常
|
||||
ExcelWriteError: 文件写入错误
|
||||
"""
|
||||
logger.info(f"开始生成测试用例,输出目录: {excel_path}")
|
||||
wb = data_dict.get('wb')
|
||||
sheet = data_dict.get('sheet')
|
||||
source_row = data_dict.get('source_row')
|
||||
|
||||
if not isinstance(wb, Workbook) or not isinstance(sheet, Worksheet):
|
||||
logger.error("数据类型错误,Workbook 或 Worksheet 类型不匹配")
|
||||
raise CaseDataError("数据异常,Workbook 或 Worksheet 类型错误")
|
||||
|
||||
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}")
|
||||
new_wb = copy.deepcopy(wb)
|
||||
sheet = new_wb["Scenario1"]
|
||||
|
||||
generate_path = f"{excel_path}/{name}.xlsx"
|
||||
__write_excel_data(sheet, datas_dict[name], source_row)
|
||||
new_wb.save(generate_path)
|
||||
|
||||
logger.info(f"测试用例生成完成,共 {case_count} 个用例")
|
||||
|
||||
|
||||
def __write_excel_data(sheet: Worksheet, data_dict: dict, source_row: int) -> None:
|
||||
"""写入 Excel 数据"""
|
||||
for name in data_dict.keys():
|
||||
column = data_dict[name]["column"]
|
||||
datalog = data_dict[name]["datalog"]
|
||||
for index, data in enumerate(datalog):
|
||||
try:
|
||||
value = int(data.value)
|
||||
sheet.cell(row=source_row + index, column=column).data_type = "int"
|
||||
except (ValueError, TypeError):
|
||||
value = data.value
|
||||
sheet.cell(row=source_row + index, column=column).data_type = "str"
|
||||
finally:
|
||||
sheet.cell(row=source_row + index, column=column).value = value
|
||||
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, time_column).value != "time":
|
||||
time_column += 1
|
||||
row = 1
|
||||
while sheet.cell(source_row + row, time_column).value is not None:
|
||||
row += 1
|
||||
stop_time = sheet.cell(source_row + row - 1, time_column).value
|
||||
if stop_time < datalog[-1].time:
|
||||
sheet.cell(source_row + row - 1, time_column).value = datalog[-1].time
|
||||
sheet.cell(source_row + row - 1, time_column).data_type = "float"
|
||||
|
||||
|
||||
def __get_template_version(sheet: Worksheet) -> str | None:
|
||||
"""获取 Excel 模板版本号
|
||||
|
||||
Args:
|
||||
sheet: Excel 工作表
|
||||
|
||||
Returns:
|
||||
模板版本号,如果未找到返回 None
|
||||
"""
|
||||
row = 2
|
||||
version = None
|
||||
while sheet.cell(row=row, column=1).value is not None:
|
||||
version = sheet.cell(row=row, column=1).value
|
||||
row += 1
|
||||
return version
|
||||
|
||||
|
||||
def __analysis_action(
|
||||
sheet: Worksheet,
|
||||
row: int,
|
||||
column: int,
|
||||
strings: str,
|
||||
data_dict: dict,
|
||||
excel_path: str
|
||||
) -> dict[str, str]:
|
||||
"""解析操作字符串
|
||||
|
||||
Args:
|
||||
sheet: Excel 工作表
|
||||
row: 行号
|
||||
column: 列号
|
||||
strings: 操作字符串
|
||||
data_dict: 数据字典
|
||||
excel_path: Excel 文件路径
|
||||
|
||||
Returns:
|
||||
操作字典
|
||||
|
||||
Raises:
|
||||
CaseDataError: 信号不存在或格式错误
|
||||
"""
|
||||
action_dict = {}
|
||||
strings = strings.replace(";", ";")
|
||||
strings = strings.replace("\n", "").split(';')
|
||||
for string in strings:
|
||||
if string == '':
|
||||
continue
|
||||
string = string.replace(" ", "")
|
||||
signal_name = string[:string.find('=')]
|
||||
if signal_name.lower() not in [k.strip().lower() for k in data_dict.keys()]:
|
||||
raise CaseDataError(
|
||||
f"{sheet.title} 第 {row} 行第 3 列操作中信号 {signal_name} 不存在"
|
||||
)
|
||||
else:
|
||||
action_dict[signal_name] = string[string.find('=') + 1:]
|
||||
return action_dict
|
||||
|
||||
|
||||
def __analysis_case(data_dict: dict, sheets_dict: dict) -> dict:
|
||||
"""分析用例字典,将用例中的信号名替换为信号值
|
||||
|
||||
Args:
|
||||
data_dict: 数据字典
|
||||
sheets_dict: 工作表字典
|
||||
|
||||
Returns:
|
||||
处理后的用例字典
|
||||
"""
|
||||
datas_dict = {}
|
||||
for sheet_name in sheets_dict.keys():
|
||||
sheet_dict = sheets_dict[sheet_name]
|
||||
|
||||
for case in sheet_dict.keys():
|
||||
try:
|
||||
if sheet_dict[case]["enable"]:
|
||||
continue
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
datas_dict[case] = copy.deepcopy(data_dict)
|
||||
__init_data_log(datas_dict[case])
|
||||
__analysis_step(sheet_dict[case]["step"], datas_dict[case])
|
||||
return datas_dict
|
||||
|
||||
|
||||
def __analysis_step(step_dict: dict, data_dict: dict) -> None:
|
||||
"""分析步骤字典,将步骤中的信号名替换为信号值
|
||||
|
||||
Args:
|
||||
step_dict: 步骤字典
|
||||
data_dict: 数据字典
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
"""分析数据字典,将数据字典中的信号名替换为信号值
|
||||
|
||||
Args:
|
||||
action_dict: 操作字典
|
||||
data_dict: 数据字典
|
||||
time: 时间戳
|
||||
|
||||
Raises:
|
||||
CaseDataError: 信号不存在
|
||||
"""
|
||||
data_log_len = None
|
||||
for name in action_dict.keys():
|
||||
matched_key = None
|
||||
for key in data_dict.keys():
|
||||
if name.lower() == key.strip().lower():
|
||||
matched_key = key
|
||||
break
|
||||
if matched_key is None:
|
||||
raise CaseDataError(f"数据字典中不存在信号 {name}")
|
||||
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:
|
||||
datalog.append(DataLog(time, datalog[-1].value))
|
||||
|
||||
|
||||
def __init_data_log(data_dict: dict) -> None:
|
||||
"""初始化数据日志
|
||||
|
||||
Args:
|
||||
data_dict: 数据字典
|
||||
"""
|
||||
for name in data_dict.keys():
|
||||
data_dict[name]["datalog"] = data_dict[name]["datalog"][:1]
|
||||
@@ -0,0 +1,185 @@
|
||||
"""MIL 仿真数据 Excel 解析模块
|
||||
|
||||
Excel 文件格式约定:
|
||||
- Sheet 名称: Scenario1
|
||||
- 行 1: 信号名称(time, signal1, signal2...)
|
||||
- 行 2: 预留行(通常为空)
|
||||
- 行 3: "Source: Input" 标记行,标识数据列开始
|
||||
- 行 4+: 时间-值数据对
|
||||
"""
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
|
||||
from .base import DataLog, SignalData, ExcelDataResult
|
||||
from .exceptions import ExcelReadError, ExcelFormatError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExcelReaderConfig:
|
||||
"""Excel 读取配置
|
||||
|
||||
Attributes:
|
||||
sheet_name: 工作表名称,默认 "Scenario1"
|
||||
source_header: 数据源标记行文本,默认 "Source: Input"
|
||||
time_column: 时间列索引,默认 1(A 列)
|
||||
header_row: 信号名称所在行号,默认 1
|
||||
type_row: 信号类型所在行号,默认 3
|
||||
data_start_row_offset: 相对于 source_row 的数据起始行偏移量,默认 1
|
||||
"""
|
||||
sheet_name: str = "Scenario1"
|
||||
source_header: str = "Source: Input"
|
||||
time_column: int = 1
|
||||
header_row: int = 1
|
||||
type_row: int = 3
|
||||
data_start_row_offset: int = 1
|
||||
|
||||
output_header: str = "Source: Output"
|
||||
block_path_row: int = 4
|
||||
|
||||
|
||||
|
||||
def read_excel_data(
|
||||
excel_path: str,
|
||||
return_object: bool = False,
|
||||
config: ExcelReaderConfig | None = None
|
||||
) -> dict[str, Any] | ExcelDataResult:
|
||||
"""读取 MIL 仿真 Excel 文件并解析信号数据
|
||||
|
||||
Args:
|
||||
excel_path: Excel 文件路径
|
||||
return_object: 是否返回 ExcelDataResult 对象(推荐),默认 False 保持向后兼容
|
||||
config: Excel 读取配置,默认使用 ExcelReaderConfig()
|
||||
|
||||
Returns:
|
||||
return_object=False 时:包含以下键的字典(向后兼容):
|
||||
- wb: Workbook 对象
|
||||
- sheet: Worksheet 对象
|
||||
- source_row: Source: Input 所在行号
|
||||
- <信号名>: 信号元数据(type, column, datalog)
|
||||
|
||||
return_object=True 时:ExcelDataResult 对象(推荐)
|
||||
- sheet_name: 工作表名称
|
||||
- source_row: Source: Input 所在行号
|
||||
- signals: 信号名称到信号数据的映射
|
||||
|
||||
Raises:
|
||||
ExcelReadError: 文件不存在或读取失败
|
||||
ExcelFormatError: Excel 格式不符合预期
|
||||
"""
|
||||
config = config or ExcelReaderConfig()
|
||||
|
||||
logger.info(f"开始读取文件: {excel_path}")
|
||||
try:
|
||||
wb = load_workbook(excel_path)
|
||||
logger.info(f"文件加载成功,Sheet 数量: {len(wb.sheetnames)}")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"文件不存在: {excel_path}")
|
||||
raise ExcelReadError(f"Excel文件 {excel_path} 不存在")
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件失败: {e}")
|
||||
raise ExcelReadError(f"读取Excel文件 {excel_path} 失败: {e}")
|
||||
|
||||
try:
|
||||
sheet = wb[config.sheet_name]
|
||||
except KeyError:
|
||||
logger.error(f"缺少 '{config.sheet_name}' 表")
|
||||
raise ExcelFormatError(f"Excel文件 {excel_path} 缺少 '{config.sheet_name}' 表")
|
||||
|
||||
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}")
|
||||
|
||||
signals: dict[str, SignalData] = {}
|
||||
column = 2
|
||||
current_header = None
|
||||
while True:
|
||||
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:
|
||||
name = sheet.cell(config.header_row, column).value
|
||||
if name is None:
|
||||
break
|
||||
if name == "time":
|
||||
column += 1
|
||||
continue
|
||||
if name == "Parameter:" or name == "Value" or name == "BlockPath":
|
||||
column += 1
|
||||
continue
|
||||
sig_type = sheet.cell(config.type_row, column).value
|
||||
|
||||
signals[name] = SignalData(
|
||||
signal_type=sig_type,
|
||||
column=column,
|
||||
datalog=get_data_log(sheet, source_row + config.data_start_row_offset, column, config.time_column)
|
||||
)
|
||||
logger.debug(f"解析信号: {name}, 数据点: {len(signals[name].datalog)}")
|
||||
# elif current_header == config.output_header:
|
||||
# column += 1
|
||||
# continue
|
||||
else:
|
||||
break
|
||||
column += 1
|
||||
|
||||
signal_count = len(signals)
|
||||
logger.info(f"解析完成,共 {signal_count} 个信号")
|
||||
|
||||
if return_object:
|
||||
return ExcelDataResult(
|
||||
sheet_name=config.sheet_name,
|
||||
source_row=source_row,
|
||||
signals=signals
|
||||
)
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"wb": wb,
|
||||
"sheet": sheet,
|
||||
"source_row": source_row
|
||||
}
|
||||
for name, signal in signals.items():
|
||||
data[name] = signal.to_dict()
|
||||
|
||||
return data
|
||||
|
||||
def get_data_log(
|
||||
sheet: Worksheet,
|
||||
row: int,
|
||||
column: int,
|
||||
time_column: int = 1
|
||||
) -> list[DataLog]:
|
||||
"""从指定位置读取时间-值数据对列表
|
||||
|
||||
Args:
|
||||
sheet: Worksheet 对象
|
||||
row: 起始行号
|
||||
column: 数据列号
|
||||
time_column: 时间列索引,默认 1
|
||||
|
||||
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:
|
||||
break
|
||||
data_log.append(DataLog(time, value))
|
||||
row += 1
|
||||
return data_log
|
||||
@@ -0,0 +1,47 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
@@ -0,0 +1,290 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from openpyxl import Workbook
|
||||
|
||||
from src.core.mil_case_excel import (
|
||||
read_excel_case,
|
||||
create_excel_case,
|
||||
__get_template_version,
|
||||
__analysis_action,
|
||||
__analysis_case,
|
||||
__analysis_step,
|
||||
__init_data_log
|
||||
)
|
||||
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)
|
||||
@@ -0,0 +1,155 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from openpyxl import Workbook
|
||||
|
||||
from src.core.mil_data_excel import read_excel_data, get_data_log, 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_get_data_log_empty_sheet():
|
||||
"""空 sheet 返回空列表"""
|
||||
wb = Workbook()
|
||||
sheet = wb.active
|
||||
datalog = get_data_log(sheet, row=1, column=1)
|
||||
assert len(datalog) == 0
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user