mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 23:16:30 +00:00
test: 新增编译器全链路测试和资源转换测试
- 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>
This commit is contained in:
324
tests/compile/test_resource_conversion_path.py
Normal file
324
tests/compile/test_resource_conversion_path.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user