Files
Uni-Lab-OS/tests/compile/test_resource_conversion_path.py
Junhan Chang 80272d691d 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>
2026-03-25 13:12:27 +08:00

325 lines
11 KiB
Python
Raw 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.
"""
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"