Compare commits
4
Commits
38610e97eb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f188055228 | ||
|
|
91bb61444e | ||
|
|
45e4f1ecf2 | ||
|
|
4c5fc718c1 |
+8
-106
@@ -28,8 +28,6 @@ share/python-wheels/
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
@@ -52,82 +50,6 @@ coverage.xml
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
@@ -137,42 +59,22 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
# Ruff
|
||||
.ruff_cache/
|
||||
|
||||
# UV
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
__pypackages__/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
*.xlsx
|
||||
*.md
|
||||
|
||||
/plugins
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
"""通过 Nuitka 把 mil 包及其纯 Python 伴生包编译为 .pyd,并完成导出、打包、部署、验证全流程。
|
||||
|
||||
策略说明:
|
||||
- **mil 业务包**(mil/ 整个包:core + ui)→ 编 pyd;
|
||||
- **Qt 桥接代码**(mil/ui/*_ui.py)→ 保留为源码;
|
||||
- **纯 Python 伴生包**(COMPANIONS 列表)→ 逐个编 pyd,产物随 mil 一起分发;
|
||||
- **Qt .ui 资源文件** → 运行时由 mil 包内的 uic.loadUiType 直接读源码路径;
|
||||
- **PySide6** → --nofollow-import-to,运行时走宿主 pip 安装的二进制。
|
||||
|
||||
前置条件:
|
||||
- openpyxl 3.x 的 C 扩展依赖 et_xmlfile 必须升到 >= 2.0(纯 Python 实现),
|
||||
否则 Nuitka --module 模式会丢弃 _xmlfile.pyd 导致运行期 ImportError。
|
||||
pip install --upgrade --force-reinstall "et_xmlfile>=2.0"
|
||||
pip install --upgrade --force-reinstall openpyxl
|
||||
|
||||
用法(在工程根 F:\MyProject\mil_sdk 下):
|
||||
python build_pyd.py # 编译 + 导出 + 打包 + 部署(默认全流程)
|
||||
python build_pyd.py --clean # 清理 build/ 后走全流程
|
||||
python build_pyd.py --no-progress # 关闭进度条(CI 友好)
|
||||
python build_pyd.py --only mil # 只编指定包(逗号分隔)
|
||||
python build_pyd.py --no-deploy # 不复制 zip 到网络盘
|
||||
python build_pyd.py --verify # 单独验证伴生包加载链路
|
||||
python build_pyd.py --verify --fallback # 验证时允许回退到 site-packages
|
||||
|
||||
全流程四步:
|
||||
1. Nuitka 编译 .pyd → build/
|
||||
2. export 聚合 → plugins/
|
||||
3. pack 打包 → dist/<tool>-<ver>-<tag>.zip
|
||||
4. 复制 zip → Y:/SE/xufeifei/plugins/(--no-deploy 跳过)
|
||||
|
||||
产物布局:
|
||||
build/
|
||||
mil.cp311-win_amd64.pyd ← 业务包
|
||||
plugins/ ← 中间产物
|
||||
dist/
|
||||
mil-<version>-cp311-win_amd64.zip ← 最终交付物
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BUILD_DIR = ROOT / "build"
|
||||
PLUGINS_DIR = ROOT / "plugins"
|
||||
DIST_DIR = ROOT / "dist"
|
||||
REMOTE_DEPLOY_DIR = Path("Y:/SE/xufeifei/plugins")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 共享工具函数
|
||||
# ============================================================
|
||||
|
||||
def _python_tag() -> str:
|
||||
"""返回 Python ABI tag,如 cp311-win_amd64。"""
|
||||
impl = "cp" + str(sys.version_info.major) + str(sys.version_info.minor)
|
||||
plat = "win_amd64" if platform.system() == "Windows" else platform.machine()
|
||||
return f"{impl}-{plat}"
|
||||
|
||||
|
||||
def _read_plugin_meta() -> tuple[str, str, str]:
|
||||
"""从 mil/__init__.py 源码提取插件名/版本/描述(绕过 PySide6 import)。"""
|
||||
src = (ROOT / "mil" / "__init__.py").read_text(encoding="utf-8")
|
||||
name = re.search(r'NMAE\s*=\s*"([^"]+)"', src).group(1)
|
||||
major = re.search(r"__MAJOR_VER:\s*int\s*=\s*(\d+)", src).group(1)
|
||||
minor = re.search(r"__MINOR_VER:\s*int\s*=\s*(\d+)", src).group(1)
|
||||
micro = re.search(r"__MICRO_VER:\s*int\s*=\s*(\d+)", src).group(1)
|
||||
desc_match = re.search(r'DESCRIPITION\s*=\s*"""([\s\S]*?)"""', src)
|
||||
desc = desc_match.group(1).strip() if desc_match else ""
|
||||
return name, f"{major}.{minor}.{micro}", desc
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 伴生包清单(从 manifest.py 加载,绕过 mil/__init__.py 的 PySide6 依赖)
|
||||
# ============================================================
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"mil_manifest", ROOT / "mil" / "core" / "manifest.py"
|
||||
)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
COMPANIONS: list[str] = _mod.COMPANION_PACKAGES_PYD
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Nuitka 编译
|
||||
# ============================================================
|
||||
|
||||
def _resolve_entry(pkg: str) -> Path:
|
||||
"""定位包的入口 __init__.py。
|
||||
|
||||
业务包(mil)从工程根找;第三方包从当前 Python 的 site-packages 找。
|
||||
"""
|
||||
local = ROOT / pkg / "__init__.py"
|
||||
if local.is_file():
|
||||
return local
|
||||
|
||||
try:
|
||||
spec = importlib.util.find_spec(pkg)
|
||||
if spec is not None and spec.origin:
|
||||
origin = Path(spec.origin)
|
||||
if origin.is_file():
|
||||
return origin
|
||||
except (ImportError, ValueError):
|
||||
pass
|
||||
|
||||
raise FileNotFoundError(f"找不到包 {pkg} 的入口 __init__.py")
|
||||
|
||||
|
||||
def build_one(pkg: str, no_progress: bool) -> Path:
|
||||
"""对单个包跑 Nuitka --module,产出 <pkg>.<python-tag>.pyd。
|
||||
|
||||
关键约束:--module 模式下要编"包"必须传"包目录路径"(末尾带 /),
|
||||
不能传 __init__.py。Nuitka 看到目录就会自动按包模式处理。
|
||||
"""
|
||||
entry = _resolve_entry(pkg)
|
||||
pkg_dir = entry.parent
|
||||
pkg_name = pkg_dir.name
|
||||
is_local = pkg_dir.parent == ROOT
|
||||
|
||||
module_arg = str(pkg_dir) + "/"
|
||||
|
||||
cmd: list[str] = [
|
||||
sys.executable, "-m", "nuitka",
|
||||
"--module",
|
||||
module_arg,
|
||||
f"--include-package={pkg_name}",
|
||||
f"--output-dir={BUILD_DIR}",
|
||||
]
|
||||
|
||||
if not is_local:
|
||||
cmd += [
|
||||
"--nofollow-import-to=xml",
|
||||
"--nofollow-import-to=xml.etree",
|
||||
"--nofollow-import-to=xml.etree.ElementTree",
|
||||
"--nofollow-import-to=zipfile",
|
||||
"--nofollow-import-to=tempfile",
|
||||
"--nofollow-import-to=datetime",
|
||||
"--nofollow-import-to=decimal",
|
||||
"--nofollow-import-to=re",
|
||||
"--nofollow-import-to=json",
|
||||
"--nofollow-import-to=argparse",
|
||||
"--nofollow-import-to=logging",
|
||||
]
|
||||
|
||||
if is_local:
|
||||
for pkg in _mod.COMPANION_PACKAGES_INLINE:
|
||||
cmd.append(f"--include-package={pkg}")
|
||||
for mod in _mod.COMPANION_STDLIB_INLINE:
|
||||
cmd.append(f"--include-module={mod}")
|
||||
for ext_pkgs in _mod.COMPANION_C_EXTENSIONS.values():
|
||||
for ext in ext_pkgs:
|
||||
cmd.append(f"--nofollow-import-to={ext}")
|
||||
cmd.append("--nofollow-import-to=pyexpat")
|
||||
|
||||
cmd += ["--no-deployment-flag=frame-useless-set-trace"]
|
||||
if no_progress:
|
||||
cmd.append("--no-progress")
|
||||
|
||||
subprocess.run(cmd, cwd=str(pkg_dir.parent), check=True)
|
||||
|
||||
candidates = list(BUILD_DIR.glob(f"{pkg_name}.*.pyd"))
|
||||
if not candidates:
|
||||
candidates = list((BUILD_DIR / pkg_name).glob(f"{pkg_name}.*.pyd"))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(
|
||||
f"Nuitka 未产出预期的 {pkg_name}.*.pyd(已扫遍 build/)"
|
||||
)
|
||||
return candidates[0]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 导出 plugins/ 分发包(原 tools/export_runtime.py)
|
||||
# ============================================================
|
||||
|
||||
def _copy_pyd_files() -> list[str]:
|
||||
"""把 build/ 下的 .pyd 拷到 plugins/,返回已拷贝的包名列表。"""
|
||||
copied: list[str] = []
|
||||
for pkg in _mod.COMPANION_PACKAGES + ["mil"]:
|
||||
matches = list(BUILD_DIR.glob(f"{pkg}.*.pyd"))
|
||||
if not matches:
|
||||
matches = list((BUILD_DIR / pkg).glob(f"{pkg}.*.pyd"))
|
||||
if not matches:
|
||||
print(f"[export] WARN: build/ 下未找到 {pkg}.*.pyd,跳过")
|
||||
continue
|
||||
|
||||
dst = PLUGINS_DIR / matches[0].name
|
||||
shutil.copy2(matches[0], dst)
|
||||
copied.append(pkg)
|
||||
print(f"[export] {pkg}: {matches[0].name} -> {dst}")
|
||||
|
||||
return copied
|
||||
|
||||
|
||||
def _copy_c_extensions() -> list[str]:
|
||||
"""从 Python 安装目录拷贝 C 扩展 .pyd 到 plugins/。"""
|
||||
copied: list[str] = []
|
||||
py_root = Path(sys.base_prefix)
|
||||
search_dirs = [py_root, py_root / "DLLs"]
|
||||
|
||||
for stdlib_pkg, ext_names in _mod.COMPANION_C_EXTENSIONS.items():
|
||||
for ext_name in ext_names:
|
||||
candidates: list[Path] = []
|
||||
for d in search_dirs:
|
||||
candidates = list(d.glob(f"{ext_name}.*.pyd"))
|
||||
if candidates:
|
||||
break
|
||||
candidates = list(d.glob(f"{ext_name}*.pyd"))
|
||||
if candidates:
|
||||
break
|
||||
if not candidates:
|
||||
print(f"[export] WARN: 找不到 C 扩展 {ext_name}.pyd"
|
||||
f"(已查 {', '.join(str(d) for d in search_dirs)})")
|
||||
continue
|
||||
|
||||
dst = PLUGINS_DIR / candidates[0].name
|
||||
shutil.copy2(candidates[0], dst)
|
||||
copied.append(ext_name)
|
||||
print(f"[export] C 扩展 {ext_name}: {candidates[0]} -> {dst}")
|
||||
|
||||
return copied
|
||||
|
||||
|
||||
def _write_manifest(copied_packages: list[str], stdlib_packages: list[str],
|
||||
c_extensions: list[str]) -> None:
|
||||
"""生成 plugins/manifest.json,供宿主 runtime hook 读取并配置 sys.path。"""
|
||||
name, version, desc = _read_plugin_meta()
|
||||
manifest = {
|
||||
"python_tag": _python_tag(),
|
||||
"python_version": platform.python_version(),
|
||||
"packages": copied_packages,
|
||||
"stdlib": stdlib_packages,
|
||||
"c_extensions": c_extensions,
|
||||
"plugin_name": name,
|
||||
"plugin_version": version,
|
||||
"plugin_description": desc,
|
||||
"tool_name": "mil",
|
||||
}
|
||||
dst = PLUGINS_DIR / "manifest.json"
|
||||
dst.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"[export] manifest -> {dst}")
|
||||
|
||||
|
||||
def _export_runtime() -> None:
|
||||
"""导出插件分发包到 plugins/ 目录。"""
|
||||
if not BUILD_DIR.exists():
|
||||
print("[export] ERROR: build/ 不存在,请先运行 python build_pyd.py", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
if PLUGINS_DIR.exists():
|
||||
shutil.rmtree(PLUGINS_DIR)
|
||||
PLUGINS_DIR.mkdir(parents=True)
|
||||
|
||||
copied_pyd = _copy_pyd_files()
|
||||
copied_cext = _copy_c_extensions()
|
||||
_write_manifest(copied_pyd, [], copied_cext)
|
||||
|
||||
print(f"\n[export] 完成!plugins/ 目录已就绪:{PLUGINS_DIR}")
|
||||
print(f"[export] .pyd 文件: {len(copied_pyd)} 个(含已编入 mil.pyd 的 openpyxl/et_xmlfile)")
|
||||
print(f"[export] C 扩展: {len(copied_cext)} 个(运行时从插件目录加载)")
|
||||
print(f"[export] Python 版本: {platform.python_version()}")
|
||||
print(f"[export] 下一步: 把 plugins/ 目录放到宿主 exe 旁边")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 打包 zip 分发包(原 tools/pack_zip.py)
|
||||
# ============================================================
|
||||
|
||||
def _pack_zip() -> None:
|
||||
"""把 plugins/ 目录打包成带版本号的 zip 分发包。"""
|
||||
if not PLUGINS_DIR.exists() or not any(PLUGINS_DIR.iterdir()):
|
||||
print("[pack] ERROR: plugins/ 不存在或为空,请先执行导出步骤", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
name, version, _ = _read_plugin_meta()
|
||||
manifest = json.loads((PLUGINS_DIR / "manifest.json").read_text(encoding="utf-8"))
|
||||
tool_name = manifest["tool_name"]
|
||||
|
||||
zip_name = f"{tool_name}-{version}-{_python_tag()}.zip"
|
||||
DIST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
zip_path = DIST_DIR / zip_name
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for f in PLUGINS_DIR.rglob("*"):
|
||||
if f.is_file():
|
||||
arcname = f.relative_to(PLUGINS_DIR).as_posix()
|
||||
zf.write(f, arcname)
|
||||
|
||||
file_count = len(zipfile.ZipFile(zip_path).namelist())
|
||||
print(f"[pack] 打包完成: {zip_path}")
|
||||
print(f"[pack] 插件: {name} v{version}")
|
||||
print(f"[pack] 文件数: {file_count}")
|
||||
print(f"[pack] 下一步: 把 zip 复制到远程 {REMOTE_DEPLOY_DIR}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 部署
|
||||
# ============================================================
|
||||
|
||||
def _deploy_zip() -> None:
|
||||
"""把 dist/ 下最新生成的 zip 复制到网络盘远程分发目录。"""
|
||||
zips = sorted(DIST_DIR.glob("*.zip"), key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
if not zips:
|
||||
print("[build_pyd] WARN: dist/ 下无 zip 文件,跳过部署", file=sys.stderr)
|
||||
return
|
||||
src = zips[0]
|
||||
if not REMOTE_DEPLOY_DIR.exists():
|
||||
print(f"[build_pyd] WARN: 远程目录不可达: {REMOTE_DEPLOY_DIR},跳过部署",
|
||||
file=sys.stderr)
|
||||
return
|
||||
dst = REMOTE_DEPLOY_DIR / src.name
|
||||
shutil.copy2(src, dst)
|
||||
print(f"[build_pyd] 步骤 4/4: 已部署 {src.name} -> {dst}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 验证伴生包加载链路(原 tools/verify_companions.py)
|
||||
# ============================================================
|
||||
|
||||
def verify_companions(fallback: bool = False) -> int:
|
||||
"""验证 build/ 下伴生包 .pyd 的 openpyxl 读写闭环是否可用。
|
||||
|
||||
直接 import mil.core.companion_loader,不经过 mil/__init__.py,
|
||||
避免在无 PySide6 的开发机上因 UI 桥接而炸。
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"mil_companion_loader", ROOT / "mil" / "core" / "companion_loader.py"
|
||||
)
|
||||
loader_mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(loader_mod)
|
||||
load_companions = loader_mod.load_companions
|
||||
|
||||
packages = list(_mod.COMPANION_PACKAGES)
|
||||
loaded = load_companions(packages)
|
||||
print(f"[verify] 加载器返回: {loaded}")
|
||||
|
||||
if not loaded and not fallback:
|
||||
print(
|
||||
"[verify] FAIL: 没有挂载到任何伴生包(是否忘了 python build_pyd.py?)\n"
|
||||
" 调试期可加 --fallback 走 site-packages 自测加载器逻辑。",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
if not loaded and fallback:
|
||||
import importlib as _il
|
||||
for pkg in packages:
|
||||
_il.import_module(pkg)
|
||||
loaded.append(pkg)
|
||||
print(f"[verify] fallback 后挂载: {loaded}")
|
||||
|
||||
import openpyxl # noqa: WPS433
|
||||
print(f"[verify] openpyxl 来自: {openpyxl.__file__}")
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
xlsx_path = Path(td) / "smoke.xlsx"
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws["A1"] = "time"
|
||||
ws["B1"] = "sig1"
|
||||
ws["A2"] = 0.0
|
||||
ws["B2"] = 1
|
||||
ws["A3"] = 1.0
|
||||
ws["B3"] = 2
|
||||
wb.save(xlsx_path)
|
||||
|
||||
wb2 = openpyxl.load_workbook(xlsx_path)
|
||||
ws2 = wb2.active
|
||||
assert ws2["A1"].value == "time", ws2["A1"].value
|
||||
assert ws2["B1"].value == "sig1", ws2["B1"].value
|
||||
assert float(ws2["A3"].value) == 1.0, ws2["A3"].value
|
||||
assert int(ws2["B3"].value) == 2, ws2["B3"].value
|
||||
|
||||
print("[verify] PASS: openpyxl 写读闭环 ✓")
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主入口
|
||||
# ============================================================
|
||||
|
||||
def main() -> None:
|
||||
"""编译 mil 包及其伴生依赖为 .pyd,导出为 plugins/ 目录,打包为 zip 并部署至网络盘。
|
||||
|
||||
通过 --verify 可跳过编译,仅验证已编译产物的 openpyxl 读写闭环。
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="编译 mil + 伴生包为 .pyd,并打包成 zip")
|
||||
parser.add_argument("--clean", action="store_true", help="清理 build/ 与上次产物")
|
||||
parser.add_argument("--no-progress", action="store_true", help="关闭进度条(CI 友好)")
|
||||
parser.add_argument("--no-deploy", action="store_true", help="不复制 zip 到网络盘")
|
||||
parser.add_argument("--only", type=str, default="",
|
||||
help="只编指定包(逗号分隔),如 --only mil,openpyxl;默认全编")
|
||||
parser.add_argument("--verify", action="store_true", help="验证伴生包 .pyd 加载链路(不编译)")
|
||||
parser.add_argument("--fallback", action="store_true",
|
||||
help="验证时,未发现产物则回退到 site-packages(开发自测用)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verify:
|
||||
sys.exit(verify_companions(args.fallback))
|
||||
|
||||
if args.clean and BUILD_DIR.exists():
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
|
||||
targets = ["mil"]
|
||||
if args.only:
|
||||
targets = [p.strip() for p in args.only.split(",") if p.strip()]
|
||||
else:
|
||||
targets += COMPANIONS
|
||||
|
||||
print("[build_pyd] 步骤 1/4: Nuitka 编译 .pyd")
|
||||
for pkg in targets:
|
||||
print(f"[build_pyd] 正在编译: {pkg}")
|
||||
out = build_one(pkg, args.no_progress)
|
||||
print(f"[build_pyd] {pkg} -> {out}")
|
||||
|
||||
try:
|
||||
print("[build_pyd] 步骤 2/4: 导出 plugins/ 分发包")
|
||||
_export_runtime()
|
||||
|
||||
print("[build_pyd] 步骤 3/4: 打包 zip")
|
||||
_pack_zip()
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(f"[build_pyd] 打包流程中止(退出码 {e.code})", file=sys.stderr)
|
||||
sys.exit(e.code)
|
||||
|
||||
if not args.no_deploy:
|
||||
_deploy_zip()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,25 +0,0 @@
|
||||
"""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,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""MIL SDK 示例程序主函数"""
|
||||
setup_logging()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"DataPath": "",
|
||||
"FilePath": "",
|
||||
"AddTimeEn": true,
|
||||
"GeratePath": true,
|
||||
"CurrProject": "ABC",
|
||||
"ItemConfigs": {
|
||||
"吉利E211": {
|
||||
"DataPath": "C:/Users/xff1atk/Downloads/模型检查单.xlsx",
|
||||
"FilePath": "C:/Users/xff1atk/Downloads/Geely_E22H_Signallist.xlsx"
|
||||
},
|
||||
"ABC": {
|
||||
"DataPath": "C:/Users/xff1atk/Downloads/Geely_E22H_Signallist.xlsx",
|
||||
"FilePath": "C:/Users/xff1atk/Downloads/合肥模型团队-MIL测试模版V0.3.xlsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import re
|
||||
from .ui.mil_tool import FrameMILTool
|
||||
from .core.companion_loader import load_companions
|
||||
from .core.manifest import COMPANION_PACKAGES
|
||||
|
||||
# 工具名称
|
||||
NMAE = "MIL Tool"
|
||||
|
||||
# 工具版本
|
||||
__MAJOR_VER: int = 0 # 主版本号
|
||||
__MINOR_VER: int = 0 # 次版本号
|
||||
__MICRO_VER: int = 6 # 修订版本号
|
||||
|
||||
VERSION:str = f"{__MAJOR_VER}.{__MINOR_VER}.{__MICRO_VER}"
|
||||
|
||||
# 工具描述
|
||||
DESCRIPITION = """
|
||||
主要用于生成Simulink Test Case。
|
||||
"""
|
||||
|
||||
SVG = """
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 512 512"><path fill="#1de9b6" d="M495.855 367.604c-28.98-73.177-53.037-148.231-80.725-221.893c-12.23-31.362-24.198-62.986-40.868-92.33c-6.62-10.535-12.956-22.119-23.814-28.86c-2.739-1.74-5.828-2.479-8.88-2.42c-5.084.096-10.14 2.37-14.025 5.65c-14.571 11.543-23.209 28.334-32.896 43.793c-17.114 28.698-35.156 58.748-63.773 77.396c-13.44 9.485-31.039 10.514-43.995 20.686c-17.72 13.36-29.767 32.31-44.298 48.737c-3.31 3.956-8.436 5.53-12.916 7.67C86.315 243.71 42.945 261.35 0 279.916c36.367 28.132 75.115 53.157 112.208 80.321c10.172-2.018 20.383-6.196 30.877-4.339c16.63 5.207 26.377 21.15 34.006 35.721c15.5 31.765 26.7 65.307 39.253 98.283c20.988-1.493 39.878-12.31 56.104-25.025c31.24-25.307 55.034-58.001 81.23-88.192c12.552-13.199 25.347-28.92 44.197-32.593c19.05-4.601 39.454 2.22 53.581 15.338c19.818 17.719 37.376 38.203 60.544 51.765c-3.108-15.338-10.575-29.101-16.145-43.592M171.844 316.04c-18.284 10.817-37.699 19.717-56.71 29.162c-29.828-19.98-58.889-41.15-88.192-61.856c39.515-17.315 79.635-33.4 119.675-49.545c18.97 14.813 38.547 28.88 57.517 43.693c-8.92 14.248-19.657 27.487-32.29 38.546m40.665-49.646c-18.85-14.167-37.94-27.951-56.508-42.482c11.907-16.226 24.945-31.906 40.565-44.701c10.333-6.62 22.966-8.456 33.703-14.43c25.469-12.714 42.845-36.044 59.434-58.324c-21.271 55.216-43.975 110.654-77.194 159.937"/></svg>
|
||||
"""
|
||||
|
||||
# 伴生包清单从 manifest 导入(唯一事实源),此处保留引用以兼容旧调用方
|
||||
# 加新伴生包:改 mil/core/manifest.py 的 COMPANION_PACKAGES,不要改这里
|
||||
|
||||
def create_plugin(workspace, parent=None):
|
||||
# 先挂伴生包,再构造 UI——mil 包内任何 import openpyxl 都将命中 build/ 下的 pyd
|
||||
load_companions(COMPANION_PACKAGES)
|
||||
return FrameMILTool(workspace, parent)
|
||||
|
||||
def create_plugin_svg(file:str):
|
||||
with open(file,'w', encoding='utf-8') as f:
|
||||
f.write(SVG.strip())
|
||||
f.close()
|
||||
|
||||
def read_plugin_name():
|
||||
return NMAE
|
||||
|
||||
def read_plugin_version():
|
||||
return VERSION
|
||||
|
||||
def read_plugin_description():
|
||||
return re.sub(r'^[\r\n]+',"",DESCRIPITION)
|
||||
|
||||
__all__ = [
|
||||
"create_plugin",
|
||||
"create_plugin_svg",
|
||||
"read_plugin_name",
|
||||
"read_plugin_version",
|
||||
"read_plugin_description",
|
||||
]
|
||||
@@ -3,14 +3,12 @@
|
||||
提供 MIL 仿真数据读取功能
|
||||
|
||||
使用示例:
|
||||
from src.core import setup_logging, read_excel_data
|
||||
from mil.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"
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
"""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:
|
||||
path: 配置文件路径(JSON 文件)
|
||||
DataPath: 仿真数据目录
|
||||
FilePath: 当前打开的 Excel 文件路径
|
||||
AddTimeEn: 用例步骤时间是否按累加方式记录
|
||||
GeratePath: 是否为生成的用例另存新文件
|
||||
CurrProject: 当前工程名称
|
||||
ItemConfigs: 各模块/用例项的细粒度配置
|
||||
"""
|
||||
path:str
|
||||
DataPath:str = ""
|
||||
FilePath:str = ""
|
||||
AddTimeEn:bool = True
|
||||
GeratePath:bool = True
|
||||
CurrProject:str = ""
|
||||
# dict 是可变类型,必须用 default_factory 显式实例化,避免多个 Config 共享同一对象
|
||||
ItemConfigs: dict = field(default_factory=dict)
|
||||
|
||||
def check_path(self):
|
||||
return os.path.exists(self.path)
|
||||
|
||||
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":
|
||||
"""从 self.path 指定的 JSON 文件读取配置并填充到当前实例的各个字段。
|
||||
|
||||
解析规则:
|
||||
- JSON 中存在的字段会被回写到 Config 的对应字段;
|
||||
- JSON 中缺失的字段保持当前 Config 实例的默认值;
|
||||
- JSON 中多余字段被忽略(向前兼容:增加字段不会破坏老配置)。
|
||||
|
||||
Returns:
|
||||
self:填充后的 Config 实例,便于链式调用。
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 配置文件不存在。
|
||||
json.JSONDecodeError: 文件内容不是合法 JSON。
|
||||
"""
|
||||
try:
|
||||
# 以 UTF-8 读取 JSON,避免 Windows 默认编码带来的乱码问题
|
||||
with open(self.path, 'r', encoding='utf-8') as f:
|
||||
raw = json.load(f)
|
||||
except FileNotFoundError:
|
||||
# 显式记录并重抛,遵循"错误必须显式处理,禁止静默失败"
|
||||
logger.error(f"配置文件{self.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):
|
||||
"""将当前配置以 JSON 格式写入 self.path 指定的文件。
|
||||
|
||||
Raises:
|
||||
Exception: 写入失败时记录日志并原样抛出异常。
|
||||
"""
|
||||
try:
|
||||
with open(self.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"配置文件{self.path}写入失败: {e}")
|
||||
raise
|
||||
|
||||
@dataclass
|
||||
class DataLog:
|
||||
"""仿真数据日志记录
|
||||
|
||||
Attributes:
|
||||
time: 时间戳(秒)
|
||||
value: 信号值(字符串形式,写入 Excel 时按内容推断 int/str 类型)
|
||||
"""
|
||||
time: float = 0.0
|
||||
value: str = ""
|
||||
|
||||
@dataclass
|
||||
class SignalData:
|
||||
"""信号数据封装
|
||||
|
||||
描述 Excel 中某一列信号的完整信息:
|
||||
- 所在列号(column)
|
||||
- 列上方的属性行(attributes,例如 Signal Name / BlockPath 等)
|
||||
- 时间-值采样序列(datalog)
|
||||
|
||||
Attributes:
|
||||
column: 信号在 Excel 工作表中的列索引(1-based)
|
||||
attributes: 列上方各属性行(行号 -> 文本)的字典映射
|
||||
datalog: 时间戳-数值采样点列表
|
||||
"""
|
||||
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())
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""伴生包加载器:把纯 Python 伴生包从产物目录挂到 sys.modules。
|
||||
|
||||
设计目标:宿主 exe 一次打包后,mil 的伴生依赖(openpyxl / et_xmlfile / ...)
|
||||
可通过"覆盖 build/<pkg>/ 下的 .pyd 或 mil_runtime/<pkg>/ 下的源码"增量更新,
|
||||
宿主不需要重打。
|
||||
|
||||
加载顺序(命中即停):
|
||||
1. sys.modules 已存在(不动)
|
||||
2. mil_runtime/<pkg>/ 下的纯 Python 源码(数据化部署)
|
||||
3. build/<pkg>/ 下的预编译 .pyd(本轮主路径)
|
||||
4. 都不命中 → 跳过并 warning,由调用方决定是否兜底
|
||||
|
||||
约定:
|
||||
- mil 包的父目录下存在两个候选目录:mil_runtime/(源码)和 build/(.pyd)。
|
||||
- 解析顺序可通过环境变量 MIL_RUNTIME_DIR / MIL_BUILD_DIR 覆盖。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_RUNTIME_DIRNAME = "mil_runtime"
|
||||
DEFAULT_BUILD_DIRNAME = "build"
|
||||
|
||||
|
||||
def _mil_root() -> Path:
|
||||
"""获取 mil 包的父目录(即 mil_runtime/ 与 build/ 所在的工程根)。
|
||||
|
||||
不通过 `import mil` 解析位置,避免触发 mil 包完整初始化
|
||||
(宿主侧必装 PySide6,开发侧不一定有)。
|
||||
"""
|
||||
# 本文件位于 <root>/mil/core/companion_loader.py,往上两级就是 <root>
|
||||
return Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def _load_from_source(runtime_dir: Path, pkg: str) -> ModuleType | None:
|
||||
"""尝试从 mil_runtime/<pkg>/ 加载纯 Python 源码包。"""
|
||||
pkg_dir = runtime_dir / pkg
|
||||
if not (pkg_dir / "__init__.py").is_file():
|
||||
return None
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
pkg,
|
||||
pkg_dir / "__init__.py",
|
||||
submodule_search_locations=[str(pkg_dir)],
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning(f"无法为 {pkg} 构造 spec(runtime 源码),跳过")
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[pkg] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_from_pyd(build_dir: Path, pkg: str) -> ModuleType | None:
|
||||
"""尝试从 build/<pkg>/<pkg>.<python-tag>.pyd 加载预编译包。"""
|
||||
candidates = list(build_dir.glob(f"{pkg}.*.pyd"))
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
pyd_path = candidates[0]
|
||||
spec = importlib.util.spec_from_file_location(pkg, pyd_path)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning(f"无法为 {pkg} 构造 spec(pyd={pyd_path}),跳过")
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[pkg] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def load_companions(packages: list[str]) -> list[str]:
|
||||
"""按上述顺序把每个伴生包挂到 sys.modules,返回成功挂载的包名列表。"""
|
||||
root = _mil_root()
|
||||
runtime_dir = Path(os.environ.get("MIL_RUNTIME_DIR", root / DEFAULT_RUNTIME_DIRNAME))
|
||||
build_dir = Path(os.environ.get("MIL_BUILD_DIR", root / DEFAULT_BUILD_DIRNAME))
|
||||
|
||||
loaded: list[str] = []
|
||||
for pkg in packages:
|
||||
if pkg in sys.modules:
|
||||
loaded.append(pkg)
|
||||
continue
|
||||
|
||||
mod = (
|
||||
_load_from_source(runtime_dir, pkg)
|
||||
or _load_from_pyd(build_dir, pkg)
|
||||
)
|
||||
if mod is not None:
|
||||
loaded.append(pkg)
|
||||
logger.info(f"伴生包已挂载: {pkg}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"伴生包未找到: {pkg}(runtime={runtime_dir} / build={build_dir} 均无)"
|
||||
)
|
||||
return loaded
|
||||
@@ -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
|
||||
@@ -0,0 +1,41 @@
|
||||
"""插件运行期依赖清单。
|
||||
|
||||
这是唯一的事实源:
|
||||
- build_pyd.py 读 COMPANION_PACKAGES_PYD 决定要编哪些伴生包;
|
||||
- build_pyd.py 编译 mil 时用 --include-package 把 COMPANION_PACKAGES_INLINE 编进 mil.pyd;
|
||||
- build_pyd.py 导出步骤读 COMPANION_C_EXTENSIONS 决定要拷哪些 C 扩展。
|
||||
|
||||
加新伴生包时只动这里,其它脚本自动跟随。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# 需 Nuitka 编译成独立 .pyd 的伴生包(业务代码需保护)
|
||||
COMPANION_PACKAGES_PYD: list[str] = []
|
||||
|
||||
# 编译 mil 时用 --include-package 编进 mil.pyd 的纯 Python 包
|
||||
# 这些包的 .py 代码编进 mil.pyd,运行时内部 hard-import,不查 sys.path
|
||||
COMPANION_PACKAGES_INLINE: list[str] = [
|
||||
"openpyxl",
|
||||
"et_xmlfile",
|
||||
]
|
||||
|
||||
# 编译 mil 时用 --include-module 编进 mil.pyd 的 stdlib 子模块
|
||||
# Nuitka 默认不编译 stdlib(--nofollow-stdlib),所以必须显式 include。
|
||||
# 这些是 openpyxl/et_xmlfile 间接依赖的纯 Python stdlib,编进 mil.pyd 后
|
||||
# 运行时 hard-import,不查 sys.path。只有 C 扩展(_elementtree)留给运行时。
|
||||
COMPANION_STDLIB_INLINE: list[str] = [
|
||||
"xml.etree",
|
||||
"xml.etree.ElementTree",
|
||||
"xml.etree.ElementPath",
|
||||
"xml.etree.ElementInclude",
|
||||
]
|
||||
|
||||
# 兼容旧接口:所有伴生包
|
||||
COMPANION_PACKAGES: list[str] = COMPANION_PACKAGES_PYD + COMPANION_PACKAGES_INLINE
|
||||
|
||||
# 插件依赖的 C 扩展模块(Nuitka --module 编不进去,运行时从插件目录加载)
|
||||
# key = 所属 stdlib 包名, value = C 扩展模块名列表
|
||||
COMPANION_C_EXTENSIONS: dict[str, list[str]] = {
|
||||
"xml": ["_elementtree"], # xml.etree.ElementTree 内部 from _elementtree import *
|
||||
}
|
||||
@@ -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():
|
||||
@@ -120,8 +135,10 @@ def _analysis_case(data_dict: dict, sheets_dict: dict) -> CaseResultDict:
|
||||
if sheet_dict[case]["enable"]:
|
||||
continue
|
||||
except KeyError:
|
||||
logger.warning(f"用例 '{case}' 缺少 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 +146,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 +190,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 +205,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,63 +51,80 @@ 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)}")
|
||||
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)}")
|
||||
|
||||
# 版本表 Atech-Hefei 必须存在;解析后将其从工作簿移除,避免进入后续遍历
|
||||
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: 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 +133,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 +141,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 +156,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 +173,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 +198,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 +229,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():
|
||||
@@ -32,20 +32,14 @@ class ExcelReaderConfig:
|
||||
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
|
||||
interp_row: int = 6
|
||||
data_start_row_offset: int = 1
|
||||
|
||||
output_header: str = "Source: Output"
|
||||
block_path_row: int = 4
|
||||
|
||||
|
||||
def read_excel_data(
|
||||
excel_path: str,
|
||||
@@ -75,72 +69,72 @@ 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)}")
|
||||
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}")
|
||||
|
||||
# 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:
|
||||
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
|
||||
attributes = __get_signal_attributes(sheet, column, source_row)
|
||||
if attributes is None:
|
||||
break
|
||||
signals[name] = SignalData(
|
||||
column=column,
|
||||
attributes=attributes,
|
||||
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:
|
||||
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
|
||||
signals[name] = SignalData(
|
||||
column=column,
|
||||
attributes=attributes,
|
||||
datalog=__get_data_log(sheet, source_row + config.data_start_row_offset, column, config.time_column)
|
||||
)
|
||||
logger.debug(f"解析信号: {name}, 数据点: {len(signals[name].datalog)}")
|
||||
column += 1
|
||||
|
||||
signal_count = len(signals)
|
||||
logger.info(f"解析完成,共 {signal_count} 个信号")
|
||||
|
||||
# 新接口:返回强类型 ExcelDataResult(推荐)
|
||||
if return_object:
|
||||
return ExcelDataResult(
|
||||
sheet_name=config.sheet_name,
|
||||
@@ -148,6 +142,7 @@ def read_excel_data(
|
||||
signals=signals
|
||||
)
|
||||
|
||||
# 旧接口:保持返回 dict 以便调用方继续拿到 Workbook / Worksheet 引用
|
||||
data: DataDict = {
|
||||
"wb": wb,
|
||||
"sheet": sheet,
|
||||
@@ -164,15 +159,15 @@ def __get_signal_attributes(
|
||||
column: int,
|
||||
max_row: int
|
||||
) -> dict[int, str]:
|
||||
"""从指定位置读取信号属性字典
|
||||
"""读取指定列在 [1, max_row) 范围内各属性行的内容。
|
||||
|
||||
Args:
|
||||
sheet: Worksheet 对象
|
||||
column: 列号
|
||||
max_row: 最大行号
|
||||
max_row: Source: Input 标记行(不包含),即属性区域的上界
|
||||
|
||||
Returns:
|
||||
信号属性字典,键为行号,值为单元格值
|
||||
信号属性字典,键为行号(int),值为该单元格文本(str)
|
||||
"""
|
||||
attributes: dict[int, str] = {}
|
||||
for row in range(1, max_row):
|
||||
@@ -185,19 +180,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: 时间列索引,默认 1(A 列)
|
||||
|
||||
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')
|
||||
@@ -37,28 +44,34 @@ def update_case_excel(filename: str, old_data: dict, new_data: dict) -> None:
|
||||
new_source_row = new_data.pop('source_row')
|
||||
|
||||
if not isinstance(old_wb, Workbook) or not isinstance(old_sheet, Worksheet):
|
||||
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 +88,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 +106,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 +124,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'])
|
||||
return 0
|
||||
if not old_data:
|
||||
return 0
|
||||
# 取首项的 datalog 长度即可,无需遍历整张字典
|
||||
first_name = next(iter(old_data))
|
||||
return len(old_data[first_name]['datalog'])
|
||||
@@ -0,0 +1,124 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .mil_project_ui import Ui_FrameMILProject
|
||||
from mil.core import Config
|
||||
|
||||
class MILProject(QFrame, Ui_FrameMILProject):
|
||||
update_item_name_signal = Signal()
|
||||
update_item_data_path_signal = Signal(str,str)
|
||||
update_item_file_path_signal = Signal(str,str)
|
||||
|
||||
def __init__(self, config, name = None,parent = None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.initUI(config,name)
|
||||
|
||||
def initUI(self,config:Config, name:str):
|
||||
self.lineEditDataPath.setReadOnly(True)
|
||||
self.lineEditFilePath.setReadOnly(True)
|
||||
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.ItemConfigs = config.ItemConfigs
|
||||
|
||||
if self.name is not None and self.name in self.ItemConfigs.keys():
|
||||
self.lineEditName.setText(name)
|
||||
self.lineEditDataPath.setText(self.ItemConfigs[self.name]["DataPath"])
|
||||
self.lineEditFilePath.setText(self.ItemConfigs[self.name]["FilePath"])
|
||||
else:
|
||||
self.pushButtonDataPath.setEnabled(False)
|
||||
self.pushButtonFilePath.setEnabled(False)
|
||||
self.lineEditName.setPlaceholderText("必填项!!!")
|
||||
self.lineEditDataPath.setPlaceholderText("点击【选择数据文件】按钮,选择路径!")
|
||||
self.lineEditFilePath.setPlaceholderText("点击【选择测试用例】按钮,选择路径!")
|
||||
|
||||
self.lineEditName.editingFinished.connect(self.on_name_editing_finished_event)
|
||||
self.lineEditDataPath.editingFinished.connect(self.on_datapath_editing_finished_event)
|
||||
self.lineEditFilePath.editingFinished.connect(self.on_filepath_editing_finished_event)
|
||||
|
||||
self.pushButtonDataPath.clicked.connect(self.on_data_path_event)
|
||||
self.pushButtonFilePath.clicked.connect(self.on_file_path_event)
|
||||
|
||||
def on_name_editing_finished_event(self):
|
||||
name = self.lineEditName.text()
|
||||
|
||||
if name.replace(" ", "") == "":
|
||||
self.lineEditName.setText(self.name)
|
||||
logger.warning("项目名称是必填项!!!")
|
||||
return
|
||||
if name == self.name:
|
||||
return
|
||||
|
||||
if self.name is None:
|
||||
self.name = name
|
||||
self.ItemConfigs[name] = dict()
|
||||
self.ItemConfigs[name]["DataPath"] = ""
|
||||
self.ItemConfigs[name]["FilePath"] = ""
|
||||
else:
|
||||
self.ItemConfigs[name] = self.ItemConfigs.pop(self.name)
|
||||
if self.name == self.config.CurrProject:
|
||||
self.config.CurrProject = name
|
||||
self.name = name
|
||||
|
||||
self.config.save_config()
|
||||
self.update_item_name_signal.emit()
|
||||
self.pushButtonDataPath.setEnabled(True)
|
||||
self.pushButtonFilePath.setEnabled(True)
|
||||
|
||||
def on_datapath_editing_finished_event(self):
|
||||
name = self.lineEditName.text()
|
||||
data_path = self.lineEditDataPath.text()
|
||||
|
||||
if data_path.replace(" ","") == "":
|
||||
logger.warning("数据文件路径无效!")
|
||||
return
|
||||
|
||||
self.ItemConfigs[name]["DataPath"] = data_path
|
||||
self.config.save_config()
|
||||
|
||||
self.update_item_data_path_signal.emit(name,data_path)
|
||||
|
||||
def on_filepath_editing_finished_event(self):
|
||||
name = self.lineEditName.text()
|
||||
file_path = self.lineEditFilePath.text()
|
||||
|
||||
if file_path.replace(" ","") == "":
|
||||
logger.warning("测试用例路径无效!")
|
||||
return
|
||||
|
||||
self.ItemConfigs[name]["FilePath"] = file_path
|
||||
self.config.save_config()
|
||||
|
||||
self.update_item_file_path_signal.emit(name,file_path)
|
||||
|
||||
def on_data_path_event(self):
|
||||
dataPath = self.lineEditDataPath.text()
|
||||
if os.path.exists(dataPath):
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", dataPath,"Excel工作簿(*.xlsx)")
|
||||
else:
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", "./","Excel工作簿(*.xlsx)")
|
||||
if not os.path.exists(path):
|
||||
logger.debug(f"数据文件选择已取消: {dataPath}")
|
||||
return
|
||||
self.lineEditDataPath.setText(path)
|
||||
self.on_datapath_editing_finished_event()
|
||||
|
||||
def on_file_path_event(self):
|
||||
filePath = self.lineEditFilePath.text()
|
||||
if os.path.exists(filePath):
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", filePath,"Excel工作簿(*.xlsx)")
|
||||
else:
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", "./","Excel工作簿(*.xlsx)")
|
||||
if not os.path.exists(path):
|
||||
logger.debug(f"测试用例文件选择已取消: {filePath}")
|
||||
return
|
||||
self.lineEditFilePath.setText(path)
|
||||
self.on_filepath_editing_finished_event()
|
||||
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>FrameMILProject</class>
|
||||
<widget class="QFrame" name="FrameMILProject">
|
||||
<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>
|
||||
@@ -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_FrameMILProject(object):
|
||||
def setupUi(self, FrameMILProject):
|
||||
if not FrameMILProject.objectName():
|
||||
FrameMILProject.setObjectName(u"FrameMILProject")
|
||||
FrameMILProject.resize(740, 95)
|
||||
self.gridLayout_4 = QGridLayout(FrameMILProject)
|
||||
self.gridLayout_4.setObjectName(u"gridLayout_4")
|
||||
self.gridLayout = QGridLayout()
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
self.label = QLabel(FrameMILProject)
|
||||
self.label.setObjectName(u"label")
|
||||
|
||||
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
|
||||
|
||||
self.label_2 = QLabel(FrameMILProject)
|
||||
self.label_2.setObjectName(u"label_2")
|
||||
|
||||
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
|
||||
|
||||
self.label_3 = QLabel(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
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(FrameMILProject)
|
||||
|
||||
QMetaObject.connectSlotsByName(FrameMILProject)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, FrameMILProject):
|
||||
FrameMILProject.setWindowTitle(QCoreApplication.translate("FrameMILProject", u"Frame", None))
|
||||
self.label.setText(QCoreApplication.translate("FrameMILProject", u"\u6a21\u578b\u9879\u76ee\uff1a", None))
|
||||
self.label_2.setText(QCoreApplication.translate("FrameMILProject", u"\u6570\u636e\u6587\u4ef6\uff1a", None))
|
||||
self.label_3.setText(QCoreApplication.translate("FrameMILProject", u"\u6d4b\u8bd5\u7528\u4f8b\uff1a", None))
|
||||
self.lineEditName.setText("")
|
||||
self.pushButtonDataPath.setText(QCoreApplication.translate("FrameMILProject", u"\u9009\u62e9\u6570\u636e\u6587\u4ef6", None))
|
||||
self.pushButtonFilePath.setText(QCoreApplication.translate("FrameMILProject", u"\u9009\u62e9\u6d4b\u8bd5\u7528\u4f8b", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from .mil_tool_ui import Ui_FrameMILTool
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from .mil_project import MILProject
|
||||
|
||||
from mil.core import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONF = "mil.json"
|
||||
|
||||
class FrameMILTool(QFrame,Ui_FrameMILTool):
|
||||
def __init__(self,workspace:Path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.initUI(workspace)
|
||||
|
||||
|
||||
def initUI(self, workspace:Path):
|
||||
self.workspace = workspace
|
||||
if not self.workspace.exists():
|
||||
os.mkdir(self.workspace)
|
||||
self.conf = self.workspace.joinpath(CONF)
|
||||
|
||||
self.config = Config(self.conf)
|
||||
if self.config.check_path():
|
||||
self.config.load_config()
|
||||
self.checkBox.setChecked(self.config.AddTimeEn)
|
||||
if self.config.GeratePath:
|
||||
self.radioButtonData.setChecked(True)
|
||||
else:
|
||||
self.radioButtonFile.setChecked(True)
|
||||
else:
|
||||
self.config.save_config()
|
||||
self.update_config()
|
||||
|
||||
for key in self.config.ItemConfigs.keys():
|
||||
self.add_pro_event(key)
|
||||
|
||||
self.menu = QMenu(self.listWidget)
|
||||
self.new_pro_acction = QAction("新建项目", self)
|
||||
self.new_pro_acction.triggered.connect(self.on_new_pro_event)
|
||||
|
||||
self.del_pro_acction = QAction("删除项目", self)
|
||||
self.del_pro_acction.triggered.connect(self.on_del_pro_event)
|
||||
self.menu.addAction(self.new_pro_acction)
|
||||
self.menu.addAction(self.del_pro_acction)
|
||||
|
||||
self.DataPath.setReadOnly(True)
|
||||
self.FilePath.setReadOnly(True)
|
||||
|
||||
self.DataButton.clicked.connect(self.on_load_data_path)
|
||||
self.FileButton.clicked.connect(self.on_load_file_path)
|
||||
|
||||
self.checkBox.toggled.connect(self.on_select_add_time_en)
|
||||
self.radioButtonData.toggled.connect(self.on_select_gerate_path)
|
||||
self.comboBox.currentTextChanged.connect(self.on_text_changed_event)
|
||||
self.listWidget.customContextMenuRequested.connect(self.on_ContextMenuRequested)
|
||||
|
||||
def update_config(self):
|
||||
self.comboBox.clear()
|
||||
if self.config.ItemConfigs.keys().__len__() == 0:
|
||||
return
|
||||
|
||||
self.lock_event = True
|
||||
for key in self.config.ItemConfigs.keys():
|
||||
self.comboBox.addItem(key)
|
||||
self.lock_event = False
|
||||
|
||||
|
||||
if self.config.CurrProject == "":
|
||||
self.config.CurrProject = self.comboBox.currentText()
|
||||
self.config.save_config()
|
||||
|
||||
self.DataPath.setText(self.config.ItemConfigs[self.comboBox.currentText()]["DataPath"])
|
||||
self.FilePath.setText(self.config.ItemConfigs[self.comboBox.currentText()]["FilePath"])
|
||||
else:
|
||||
self.comboBox.setCurrentText(self.config.CurrProject)
|
||||
self.DataPath.setText(self.config.ItemConfigs[self.config.CurrProject]["DataPath"])
|
||||
self.FilePath.setText(self.config.ItemConfigs[self.config.CurrProject]["FilePath"])
|
||||
|
||||
def on_update_data_path_event(self,name,data_path):
|
||||
if name == self.comboBox.currentText():
|
||||
self.DataPath.setText(data_path)
|
||||
|
||||
def on_update_file_path_event(self,name,file_path):
|
||||
if name == self.comboBox.currentText():
|
||||
self.FilePath.setText(file_path)
|
||||
|
||||
def on_select_gerate_path(self,checked):
|
||||
self.config.GeratePath = checked
|
||||
self.config.save_config()
|
||||
|
||||
def on_select_add_time_en(self,checked):
|
||||
self.config.AddTimeEn = checked
|
||||
self.config.save_config()
|
||||
|
||||
def on_ContextMenuRequested(self,point:QPoint):
|
||||
self.menu.exec_(QCursor.pos())
|
||||
|
||||
def on_text_changed_event(self,text:str):
|
||||
if text == "" or self.lock_event:
|
||||
return
|
||||
|
||||
self.config.CurrProject = text
|
||||
self.config.save_config()
|
||||
|
||||
self.DataPath.setText(self.config.ItemConfigs[self.config.CurrProject]["DataPath"])
|
||||
self.FilePath.setText(self.config.ItemConfigs[self.config.CurrProject]["FilePath"])
|
||||
|
||||
def on_new_pro_event(self):
|
||||
item = QListWidgetItem()
|
||||
mil_project = MILProject(self.config)
|
||||
mil_project.update_item_name_signal.connect(self.update_config)
|
||||
mil_project.update_item_data_path_signal.connect(self.on_update_data_path_event)
|
||||
mil_project.update_item_file_path_signal.connect(self.on_update_file_path_event)
|
||||
|
||||
item.setSizeHint(QSize(mil_project.sizeHint().width(), 150))
|
||||
self.listWidget.addItem(item)
|
||||
self.listWidget.setCurrentItem(item)
|
||||
self.listWidget.setItemWidget(item, mil_project)
|
||||
|
||||
def add_pro_event(self, name):
|
||||
item = QListWidgetItem()
|
||||
mil_project = MILProject(self.config, name=name)
|
||||
mil_project.update_item_name_signal.connect(self.update_config)
|
||||
mil_project.update_item_data_path_signal.connect(self.on_update_data_path_event)
|
||||
mil_project.update_item_file_path_signal.connect(self.on_update_file_path_event)
|
||||
|
||||
item.setSizeHint(QSize(mil_project.sizeHint().width(), 150))
|
||||
self.listWidget.addItem(item)
|
||||
self.listWidget.setCurrentItem(item)
|
||||
self.listWidget.setItemWidget(item, mil_project)
|
||||
|
||||
def on_del_pro_event(self):
|
||||
mil_project = self.listWidget.itemWidget(self.listWidget.currentItem())
|
||||
if not isinstance(mil_project, MILProject):
|
||||
logger.error("on_del_pro_event: 当前项不是 MILProject 实例")
|
||||
return
|
||||
|
||||
if self.config.CurrProject == mil_project.name:
|
||||
self.config.CurrProject = ""
|
||||
|
||||
self.listWidget.takeItem(self.listWidget.currentRow())
|
||||
self.config.ItemConfigs.pop(mil_project.name)
|
||||
self.config.save_config()
|
||||
|
||||
self.update_config()
|
||||
|
||||
def search_mil_pro_item(self, name):
|
||||
for mil_project in self.listWidget.findChildren(MILProject):
|
||||
if not isinstance(mil_project,MILProject):
|
||||
return
|
||||
if mil_project.name == name:
|
||||
return mil_project
|
||||
return None
|
||||
|
||||
def on_load_data_path(self):
|
||||
data_path = self.DataPath.text()
|
||||
if os.path.exists(data_path):
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", data_path,"Excel工作簿(*.xlsx)")
|
||||
else:
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", os.getcwd(),"Excel工作簿(*.xlsx)")
|
||||
self.DataPath.setText(path)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self.config.ItemConfigs[self.config.CurrProject]["DataPath"] = path
|
||||
mil_project = self.search_mil_pro_item(self.config.CurrProject)
|
||||
mil_project.lineEditDataPath.setText(path)
|
||||
self.config.save_config()
|
||||
|
||||
|
||||
def on_load_file_path(self):
|
||||
file_path = self.FilePath.text()
|
||||
if os.path.exists(file_path):
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", file_path,"Excel工作簿(*.xlsx)")
|
||||
else:
|
||||
path,type = QFileDialog.getOpenFileName(self, "选择文件", os.getcwd(),"Excel工作簿(*.xlsx)")
|
||||
self.FilePath.setText(path)
|
||||
if not path:
|
||||
return
|
||||
|
||||
self.config.ItemConfigs[self.config.CurrProject]["FilePath"] = path
|
||||
mil_project = self.search_mil_pro_item(self.config.CurrProject)
|
||||
mil_project.lineEditFilePath.setText(path)
|
||||
self.config.save_config()
|
||||
@@ -0,0 +1,371 @@
|
||||
<?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::North</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">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,317 @@
|
||||
# -*- 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.North)
|
||||
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.listWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
|
||||
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
|
||||
|
||||
@@ -5,3 +5,4 @@ openpyxl>=3.0.0
|
||||
|
||||
# 开发依赖
|
||||
pytest>=7.0.0
|
||||
nuitka
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user