Files
geMoldInsight/tests/test_occ_process_pool.py
cjw e728dcd226 批次4后续专项完成:D11清偿 + OCC方案B实施 + 部署参数 + D2诚实标注 + CI门禁
① D11 HTML 报告 RustFS 单源化(TECH_DEBT P2 清偿):可视化产物写任务临时目录后
   裸传报告键 html/reports/{filename}(文件名寻址),/html StaticFiles 挂载删除,
   新增 html_report_router 根路径代理(报告键→遗留 JSON 包装→本地卷兜底→404,
   防穿越);URL 形状 /html/{filename} 不变,持久化引用零迁移;celery 摘除
   html_data 卷,镜像不再烤入陈旧报告;顺带删除 get_stp_file_with_data 死数据块
② OCC 方案 B(D10 清偿):run_occ(op_name, payload) 契约 + 常驻工作进程池
   (occ_process_pool + occ_worker 操作注册表),超时/崩溃 terminate 换新补位、
   任务级超时 recover 整体重建,残留线程泄漏根治;TopoDS 不跨进程(generate_cavity
   分模 + 方案 STEP 持久化全在子进程内,返回 export_manifest);删除内存形状缓存链、
   CADExporter.export_mold_results、shape_loader(→ stp_materializer)
③ OCC 方案 A 部署参数:CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进
   Dockerfile.celery + compose + .env.example
④ D2 诚实标注:铝价响应带 source: "simulated",前端按来源渲染标注(原硬编码
   "上海期货交易所"属虚假声明),死代码 getAluminumPrice 删除
⑤ CI 门禁:.gitea/workflows/ci.yml 三 job(pytest / 前端构建含 vue-tsc /
   openapi 漂移检测)

接口变更三件套随批完成(openapi 76→77 paths + gen:api + 前端构建通过;方案 B
接口面零变化)。测试基线 143 passed, 0 skipped(新增 16 项)。文档六处同步。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 17:22:01 +08:00

117 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OCC 方案 B(常驻进程池)契约测试,见 docs/topics/performance/OCC_THROUGHPUT.md。
覆盖:spawn 子进程 + 管道往返、操作错误回传、超时换新补位、真实 OCC 解析
(盒体 STP 端到端)。OCC(pythonocc)仅 conda 环境提供,无 OCC 环境自动跳过。
"""
import os
from pathlib import Path
import pytest
pytest.importorskip("OCC", reason="需要 pythonocc 运行 OCC 进程池测试")
from moldinsight.services.occ_process_pool import OccProcessPool
@pytest.fixture(scope="module", autouse=True)
def _child_pythonpath():
"""spawn 子进程不继承 pytest 的 sys.path,需经 PYTHONPATH 传递 src 目录。"""
src_dir = str(Path(__file__).resolve().parent.parent / "src")
prev = os.environ.get("PYTHONPATH", "")
os.environ["PYTHONPATH"] = src_dir if not prev else f"{src_dir}{os.pathsep}{prev}"
yield
if prev:
os.environ["PYTHONPATH"] = prev
else:
os.environ.pop("PYTHONPATH", None)
@pytest.fixture
async def pool():
_pool = OccProcessPool()
try:
yield _pool
finally:
await _pool.shutdown()
async def test_ping_roundtrip(pool):
result = await pool.run("ping", {})
assert result == {"pong": True}
async def test_unknown_op_raises(pool):
with pytest.raises(RuntimeError, match="未知 OCC 操作"):
await pool.run("no_such_op", {})
async def test_child_error_surfaces(pool):
# parse_stp 指向不存在的文件:子进程内抛错,父进程应收到 RuntimeError
with pytest.raises(RuntimeError):
await pool.run("parse_stp", {"stp_path": "D:/no/such/file.stp"})
async def test_timeout_replaces_worker(pool):
# 子进程挂 15s,父进程 1s 超时 → 换新补位后池仍可用
with pytest.raises(Exception, match=".*"):
await pool.run("sleep", {"seconds": 15}, timeout=1)
# 新 worker 上 ping 正常
result = await pool.run("ping", {})
assert result == {"pong": True}
def _write_box_stp(path: Path) -> Path:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.IFSelect import IFSelect_RetDone
from OCC.Core.STEPControl import STEPControl_AsIs, STEPControl_Writer
from OCC.Core.gp import gp_Pnt
box = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 100.0, 60.0, 40.0).Shape()
writer = STEPControl_Writer()
writer.Transfer(box, STEPControl_AsIs)
assert writer.Write(str(path)) == IFSelect_RetDone
return path
async def test_parse_stp_end_to_end(pool, tmp_path):
stp_path = _write_box_stp(tmp_path / "box.stp")
geometry = await pool.run("parse_stp", {"stp_path": str(stp_path)})
assert geometry["volume"] == pytest.approx(100 * 60 * 40, rel=0.01)
assert "bounding_box" in geometry
async def test_generate_cavity_persists_export_steps(pool, tmp_path):
"""最复杂的迁移点:分模 + 方案形状 STEP 导出全部在子进程内完成,
TopoDS 不跨进程传输,export_manifest 携带落盘文件清单。"""
from moldinsight.services.material_service import MaterialService
stp_path = _write_box_stp(tmp_path / "box.stp")
export_dir = tmp_path / "exports"
result = await pool.run(
"generate_cavity",
{
"stp_path": str(stp_path),
"task_id": "test-task",
"material": dict(MaterialService.get_material("ABS")),
"is_foam_material": False,
"process_params": None,
"export_out_dir": str(export_dir),
},
timeout=120,
)
plan_result = result["plan_result"]
schemes = plan_result["candidate_schemes"]
assert 1 <= len(schemes) <= 3
assert plan_result["best_scheme_id"] == schemes[0]["scheme_id"]
# TopoDS 形状不得跨进程(子进程已 pop 掉 _export_shapes)
assert "_export_shapes" not in plan_result
manifest = result["export_manifest"]
assert manifest is not None
assert manifest["schemes"]
best = manifest["schemes"][plan_result["best_scheme_id"]]
assert best["total_files"] >= 1
# 落盘文件真实存在于子进程写入的 export_out_dir
assert any((export_dir / f.get("relative_path", "")).exists() for f in best["files"])