mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 09:59:16 +00:00
- test_pump_separate_full_chain: PumpTransfer和Separate全链路测试, 验证bug修复后separate不再crash - test_full_chain_conversion_to_compile: HeatChill/Add协议结构验证 - test_resource_conversion_path: ResourceDictInstance转换路径测试 - test_batch_transfer_protocol: AGV批量转运编译器测试 - test_agv_transport_station: AGV工作站设备测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
11 KiB
Python
325 lines
11 KiB
Python
"""
|
||
ROS Goal → Resource 转换 → 编译器路径的集成测试
|
||
|
||
覆盖:
|
||
1. Resource.msg 新字段(uuid, klass, extra)的往返转换
|
||
2. dict → ROS Resource → dict 往返无损
|
||
3. ResourceTreeSet → get_plr_nested_dict 保留 children 结构
|
||
4. resource_helper 兼容 dict / ResourceDictInstance
|
||
5. vessel_parser.get_vessel 兼容 ResourceDictInstance
|
||
"""
|
||
|
||
import json
|
||
import pytest
|
||
|
||
# 不依赖 ROS 的测试 —— 直接测试 resource 处理路径
|
||
from unilabos.resources.resource_tracker import (
|
||
ResourceDict,
|
||
ResourceDictInstance,
|
||
ResourceTreeInstance,
|
||
ResourceTreeSet,
|
||
)
|
||
from unilabos.compile.utils.resource_helper import (
|
||
ensure_resource_instance,
|
||
resource_to_dict,
|
||
get_resource_id,
|
||
get_resource_data,
|
||
get_resource_display_info,
|
||
get_resource_liquid_volume,
|
||
)
|
||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||
|
||
|
||
# ============ 构建测试数据 ============
|
||
|
||
|
||
def _make_resource_dict(
|
||
id="reactor_01",
|
||
uuid="uuid-reactor-01",
|
||
name="reactor_01",
|
||
klass="virtual_stirrer",
|
||
type_="device",
|
||
parent=None,
|
||
parent_uuid=None,
|
||
data=None,
|
||
config=None,
|
||
extra=None,
|
||
):
|
||
return {
|
||
"id": id,
|
||
"uuid": uuid,
|
||
"name": name,
|
||
"class": klass,
|
||
"type": type_,
|
||
"parent": parent,
|
||
"parent_uuid": parent_uuid or "",
|
||
"description": "",
|
||
"config": config or {},
|
||
"data": data or {},
|
||
"extra": extra or {},
|
||
"position": {"x": 1.0, "y": 2.0, "z": 3.0},
|
||
}
|
||
|
||
|
||
def _make_resource_instance(id="reactor_01", **kwargs):
|
||
d = _make_resource_dict(id=id, **kwargs)
|
||
return ResourceDictInstance.get_resource_instance_from_dict(d)
|
||
|
||
|
||
def _make_tree_with_children():
|
||
"""构建 StationA -> [R1, R2] 的资源树"""
|
||
raw_data = [
|
||
_make_resource_dict(
|
||
id="StationA",
|
||
uuid="uuid-station-a",
|
||
name="StationA",
|
||
klass="workstation",
|
||
type_="device",
|
||
),
|
||
_make_resource_dict(
|
||
id="R1",
|
||
uuid="uuid-r1",
|
||
name="R1",
|
||
klass="",
|
||
type_="resource",
|
||
parent="StationA",
|
||
parent_uuid="uuid-station-a",
|
||
data={"liquid": [{"liquid_type": "water", "volume": 10.0}]},
|
||
),
|
||
_make_resource_dict(
|
||
id="R2",
|
||
uuid="uuid-r2",
|
||
name="R2",
|
||
klass="",
|
||
type_="resource",
|
||
parent="StationA",
|
||
parent_uuid="uuid-station-a",
|
||
data={"liquid": [{"liquid_type": "ethanol", "volume": 5.0}]},
|
||
),
|
||
]
|
||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||
return tree_set
|
||
|
||
|
||
# ============ resource_helper 测试 ============
|
||
|
||
|
||
class TestResourceHelper:
|
||
"""测试 resource_helper 对 dict / ResourceDictInstance 的兼容性"""
|
||
|
||
def test_ensure_resource_instance_from_dict(self):
|
||
d = _make_resource_dict()
|
||
inst = ensure_resource_instance(d)
|
||
assert isinstance(inst, ResourceDictInstance)
|
||
assert inst.res_content.id == "reactor_01"
|
||
assert inst.res_content.uuid == "uuid-reactor-01"
|
||
|
||
def test_ensure_resource_instance_passthrough(self):
|
||
inst = _make_resource_instance()
|
||
result = ensure_resource_instance(inst)
|
||
assert result is inst # 同一个对象,不复制
|
||
|
||
def test_ensure_resource_instance_none(self):
|
||
assert ensure_resource_instance(None) is None
|
||
|
||
def test_get_resource_id_from_dict(self):
|
||
d = _make_resource_dict(id="my_device")
|
||
assert get_resource_id(d) == "my_device"
|
||
|
||
def test_get_resource_id_from_instance(self):
|
||
inst = _make_resource_instance(id="my_device")
|
||
assert get_resource_id(inst) == "my_device"
|
||
|
||
def test_get_resource_id_from_string(self):
|
||
assert get_resource_id("my_device") == "my_device"
|
||
|
||
def test_get_resource_id_from_wrapped_dict(self):
|
||
"""兼容 {station_id: {...}} 格式"""
|
||
d = {"StationA": {"id": "StationA", "name": "StationA"}}
|
||
assert get_resource_id(d) == "StationA"
|
||
|
||
def test_get_resource_data_from_dict(self):
|
||
d = _make_resource_dict(data={"temperature": 25.0})
|
||
assert get_resource_data(d) == {"temperature": 25.0}
|
||
|
||
def test_get_resource_data_from_instance(self):
|
||
inst = _make_resource_instance(data={"temperature": 25.0})
|
||
data = get_resource_data(inst)
|
||
assert data["temperature"] == 25.0
|
||
|
||
def test_get_resource_display_info_from_dict(self):
|
||
d = _make_resource_dict(id="reactor_01", name="Reactor #1")
|
||
info = get_resource_display_info(d)
|
||
assert "reactor_01" in info
|
||
assert "Reactor #1" in info
|
||
|
||
def test_get_resource_display_info_from_instance(self):
|
||
inst = _make_resource_instance(id="reactor_01", name="Reactor #1")
|
||
info = get_resource_display_info(inst)
|
||
assert "reactor_01" in info
|
||
|
||
def test_get_resource_display_info_from_string(self):
|
||
assert get_resource_display_info("reactor_01") == "reactor_01"
|
||
|
||
def test_get_resource_liquid_volume(self):
|
||
d = _make_resource_dict(data={"liquid": [{"liquid_type": "water", "volume": 15.5}]})
|
||
assert get_resource_liquid_volume(d) == pytest.approx(15.5)
|
||
|
||
def test_resource_to_dict_from_instance(self):
|
||
inst = _make_resource_instance(id="reactor_01", klass="virtual_stirrer")
|
||
d = resource_to_dict(inst)
|
||
assert isinstance(d, dict)
|
||
assert d["id"] == "reactor_01"
|
||
assert d["class"] == "virtual_stirrer"
|
||
|
||
def test_resource_to_dict_passthrough(self):
|
||
d = _make_resource_dict()
|
||
result = resource_to_dict(d)
|
||
assert result is d # 同一个 dict
|
||
|
||
|
||
# ============ vessel_parser 兼容性测试 ============
|
||
|
||
|
||
class TestVesselParser:
|
||
"""测试 vessel_parser.get_vessel 对 ResourceDictInstance 的兼容"""
|
||
|
||
def test_get_vessel_from_dict(self):
|
||
d = _make_resource_dict(id="reactor_01", data={"temperature": 25.0})
|
||
vessel_id, vessel_data = get_vessel(d)
|
||
assert vessel_id == "reactor_01"
|
||
assert vessel_data["temperature"] == 25.0
|
||
|
||
def test_get_vessel_from_string(self):
|
||
vessel_id, vessel_data = get_vessel("reactor_01")
|
||
assert vessel_id == "reactor_01"
|
||
assert vessel_data == {}
|
||
|
||
def test_get_vessel_from_resource_instance(self):
|
||
inst = _make_resource_instance(id="reactor_01", data={"temperature": 25.0})
|
||
vessel_id, vessel_data = get_vessel(inst)
|
||
assert vessel_id == "reactor_01"
|
||
assert vessel_data["temperature"] == 25.0
|
||
|
||
def test_get_vessel_from_wrapped_dict(self):
|
||
"""兼容 {station_id: {id: ..., data: {...}}} 格式"""
|
||
d = {"StationA": {"id": "StationA", "data": {"vol": 100}}}
|
||
vessel_id, vessel_data = get_vessel(d)
|
||
assert vessel_id == "StationA"
|
||
|
||
|
||
# ============ ResourceTreeSet → get_plr_nested_dict 测试 ============
|
||
|
||
|
||
class TestResourceTreeRoundTrip:
|
||
"""测试 ResourceTreeSet → get_plr_nested_dict 保留树结构和关键字段"""
|
||
|
||
def test_tree_preserves_children(self):
|
||
tree_set = _make_tree_with_children()
|
||
assert len(tree_set.trees) == 1
|
||
root = tree_set.trees[0].root_node
|
||
assert root.res_content.id == "StationA"
|
||
assert len(root.children) == 2
|
||
|
||
def test_plr_nested_dict_has_children(self):
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
assert isinstance(nested, dict)
|
||
assert "children" in nested
|
||
assert isinstance(nested["children"], dict)
|
||
assert "R1" in nested["children"]
|
||
assert "R2" in nested["children"]
|
||
|
||
def test_plr_nested_dict_preserves_uuid(self):
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
assert nested["uuid"] == "uuid-station-a"
|
||
assert nested["children"]["R1"]["uuid"] == "uuid-r1"
|
||
|
||
def test_plr_nested_dict_preserves_klass(self):
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
assert nested["class"] == "workstation"
|
||
|
||
def test_plr_nested_dict_preserves_data(self):
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
r1_data = nested["children"]["R1"]["data"]
|
||
assert "liquid" in r1_data
|
||
assert r1_data["liquid"][0]["volume"] == 10.0
|
||
|
||
def test_plr_nested_dict_usable_by_get_vessel(self):
|
||
"""get_plr_nested_dict 的结果可以直接传给 get_vessel"""
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
vessel_id, vessel_data = get_vessel(nested)
|
||
assert vessel_id == "StationA"
|
||
|
||
def test_dump_vs_plr_nested_dict(self):
|
||
"""dump() 是扁平化的,get_plr_nested_dict 保留树结构"""
|
||
tree_set = _make_tree_with_children()
|
||
# dump 返回扁平列表
|
||
dumped = tree_set.dump()
|
||
assert isinstance(dumped[0], list)
|
||
assert len(dumped[0]) == 3 # StationA + R1 + R2,全部扁平
|
||
|
||
# get_plr_nested_dict 保留嵌套
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
assert isinstance(nested["children"], dict)
|
||
assert len(nested["children"]) == 2 # 嵌套的 children
|
||
|
||
|
||
# ============ 模拟 workstation 路径测试 ============
|
||
|
||
|
||
class TestWorkstationPath:
|
||
"""模拟 workstation.py 中的关键路径:
|
||
raw_data → ResourceTreeSet.from_raw_dict_list → get_plr_nested_dict → compiler
|
||
"""
|
||
|
||
def test_single_resource_path(self):
|
||
"""单个 Resource: 取第一棵树的根节点"""
|
||
raw_data = [
|
||
_make_resource_dict(id="reactor_01", uuid="uuid-r01", klass="virtual_stirrer"),
|
||
]
|
||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||
root = tree_set.trees[0].root_node
|
||
result = root.get_plr_nested_dict()
|
||
assert result["id"] == "reactor_01"
|
||
assert result["uuid"] == "uuid-r01"
|
||
assert result["class"] == "virtual_stirrer"
|
||
|
||
def test_resource_with_children_path(self):
|
||
"""Resource 带 children: AGV/batch transfer 场景"""
|
||
tree_set = _make_tree_with_children()
|
||
root = tree_set.trees[0].root_node
|
||
nested = root.get_plr_nested_dict()
|
||
|
||
# 模拟编译器接收到的参数
|
||
from_repo = {"StationA": nested}
|
||
assert "A01" not in from_repo["StationA"]["children"] # children 按 id 索引
|
||
assert "R1" in from_repo["StationA"]["children"]
|
||
assert from_repo["StationA"]["children"]["R1"]["uuid"] == "uuid-r1"
|
||
|
||
def test_multiple_resource_path(self):
|
||
"""多个 Resource: 每棵树取根节点"""
|
||
raw_data1 = [_make_resource_dict(id="R1", uuid="uuid-r1")]
|
||
raw_data2 = [_make_resource_dict(id="R2", uuid="uuid-r2")]
|
||
# 模拟 host 返回多棵树
|
||
tree_set1 = ResourceTreeSet.from_raw_dict_list(raw_data1)
|
||
tree_set2 = ResourceTreeSet.from_raw_dict_list(raw_data2)
|
||
results = [
|
||
tree.root_node.get_plr_nested_dict()
|
||
for ts in [tree_set1, tree_set2]
|
||
for tree in ts.trees
|
||
]
|
||
assert len(results) == 2
|
||
assert results[0]["id"] == "R1"
|
||
assert results[1]["id"] == "R2"
|