mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 10:29: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>
707 lines
25 KiB
Python
707 lines
25 KiB
Python
"""
|
||
全链路集成测试:ROS Goal 转换 → ResourceTreeSet → get_plr_nested_dict → 编译器 → 动作列表
|
||
|
||
模拟 workstation.py 中的完整路径:
|
||
1. host 返回 raw_data(模拟 resource_get 响应)
|
||
2. ResourceTreeSet.from_raw_dict_list(raw_data) 构建资源树
|
||
3. tree.root_node.get_plr_nested_dict() 生成嵌套 dict
|
||
4. protocol_kwargs 传给编译器
|
||
5. 编译器返回 action_list,验证结构和关键字段
|
||
"""
|
||
|
||
import copy
|
||
import json
|
||
import pytest
|
||
import networkx as nx
|
||
|
||
from unilabos.resources.resource_tracker import (
|
||
ResourceDictInstance,
|
||
ResourceTreeSet,
|
||
)
|
||
from unilabos.compile.utils.resource_helper import (
|
||
ensure_resource_instance,
|
||
resource_to_dict,
|
||
get_resource_id,
|
||
get_resource_data,
|
||
)
|
||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||
|
||
# ============ 构建模拟设备图 ============
|
||
|
||
def _build_test_graph():
|
||
"""构建一个包含常用设备节点的测试图"""
|
||
G = nx.DiGraph()
|
||
|
||
# 容器
|
||
G.add_node("reactor_01", **{
|
||
"id": "reactor_01",
|
||
"name": "reactor_01",
|
||
"type": "device",
|
||
"class": "virtual_stirrer",
|
||
"data": {},
|
||
"config": {},
|
||
})
|
||
|
||
# 搅拌设备
|
||
G.add_node("stirrer_1", **{
|
||
"id": "stirrer_1",
|
||
"name": "stirrer_1",
|
||
"type": "device",
|
||
"class": "virtual_stirrer",
|
||
"data": {},
|
||
"config": {},
|
||
})
|
||
G.add_edge("stirrer_1", "reactor_01")
|
||
|
||
# 加热设备
|
||
G.add_node("heatchill_1", **{
|
||
"id": "heatchill_1",
|
||
"name": "heatchill_1",
|
||
"type": "device",
|
||
"class": "virtual_heatchill",
|
||
"data": {},
|
||
"config": {},
|
||
})
|
||
G.add_edge("heatchill_1", "reactor_01")
|
||
|
||
# 试剂容器(液体)
|
||
G.add_node("flask_water", **{
|
||
"id": "flask_water",
|
||
"name": "flask_water",
|
||
"type": "container",
|
||
"class": "",
|
||
"data": {"reagent_name": "water", "liquid": [{"liquid_type": "water", "volume": 500.0}]},
|
||
"config": {"reagent": "water"},
|
||
})
|
||
|
||
# 固体加样器
|
||
G.add_node("solid_dispenser_1", **{
|
||
"id": "solid_dispenser_1",
|
||
"name": "solid_dispenser_1",
|
||
"type": "device",
|
||
"class": "solid_dispenser",
|
||
"data": {},
|
||
"config": {},
|
||
})
|
||
|
||
# 泵
|
||
G.add_node("pump_1", **{
|
||
"id": "pump_1",
|
||
"name": "pump_1",
|
||
"type": "device",
|
||
"class": "virtual_pump",
|
||
"data": {},
|
||
"config": {},
|
||
})
|
||
G.add_edge("flask_water", "pump_1")
|
||
G.add_edge("pump_1", "reactor_01")
|
||
|
||
return G
|
||
|
||
|
||
# ============ 构建模拟 host 返回数据 ============
|
||
|
||
def _make_raw_resource(
|
||
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,
|
||
):
|
||
"""模拟 host 返回的单个资源 dict(与 resource_get 服务响应一致)"""
|
||
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": 0.0, "y": 0.0, "z": 0.0},
|
||
}
|
||
|
||
|
||
def _simulate_workstation_resource_enrichment(raw_data_list, field_type="unilabos_msgs/Resource"):
|
||
"""
|
||
模拟 workstation.py 中 resource enrichment 的核心逻辑:
|
||
raw_data → ResourceTreeSet.from_raw_dict_list → get_plr_nested_dict → protocol_kwargs[k]
|
||
"""
|
||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data_list)
|
||
|
||
if field_type == "unilabos_msgs/Resource":
|
||
# 单个 Resource:取第一棵树的根节点
|
||
root_instance = tree_set.trees[0].root_node if tree_set.trees else None
|
||
return root_instance.get_plr_nested_dict() if root_instance else {}
|
||
else:
|
||
# sequence<Resource>:返回列表
|
||
return [tree.root_node.get_plr_nested_dict() for tree in tree_set.trees]
|
||
|
||
|
||
# ============ 全链路测试:Stir 协议 ============
|
||
|
||
class TestStirProtocolFullChain:
|
||
"""Stir 协议全链路:host raw_data → enriched dict → compiler → action_list"""
|
||
|
||
def test_stir_with_enriched_resource_dict(self):
|
||
"""单个 Resource 经过 enrichment 后传给 stir compiler"""
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
|
||
raw_data = [_make_raw_resource(
|
||
id="reactor_01", uuid="uuid-reactor-01",
|
||
klass="virtual_stirrer", type_="device",
|
||
)]
|
||
|
||
# 模拟 workstation enrichment
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
assert enriched_vessel["id"] == "reactor_01"
|
||
assert enriched_vessel["uuid"] == "uuid-reactor-01"
|
||
assert enriched_vessel["class"] == "virtual_stirrer"
|
||
|
||
# 传给编译器
|
||
G = _build_test_graph()
|
||
actions = generate_stir_protocol(
|
||
G=G,
|
||
vessel=enriched_vessel,
|
||
time="60",
|
||
stir_speed=300.0,
|
||
)
|
||
|
||
assert isinstance(actions, list)
|
||
assert len(actions) >= 1
|
||
action = actions[0]
|
||
assert action["device_id"] == "stirrer_1"
|
||
assert action["action_name"] == "stir"
|
||
assert "vessel" in action["action_kwargs"]
|
||
assert action["action_kwargs"]["vessel"]["id"] == "reactor_01"
|
||
|
||
def test_stir_with_resource_dict_instance(self):
|
||
"""直接用 ResourceDictInstance 传给 stir compiler(通过 get_plr_nested_dict 转换)"""
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||
inst = tree_set.trees[0].root_node
|
||
|
||
# 通过 resource_to_dict 转换(resource_helper 兼容层)
|
||
vessel_dict = resource_to_dict(inst)
|
||
assert isinstance(vessel_dict, dict)
|
||
assert vessel_dict["id"] == "reactor_01"
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_stir_protocol(G=G, vessel=vessel_dict, time="30")
|
||
|
||
assert len(actions) >= 1
|
||
assert actions[0]["action_name"] == "stir"
|
||
|
||
def test_stir_with_string_vessel(self):
|
||
"""兼容旧模式:直接传 vessel 字符串"""
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_stir_protocol(G=G, vessel="reactor_01", time="30")
|
||
|
||
assert len(actions) >= 1
|
||
assert actions[0]["device_id"] == "stirrer_1"
|
||
assert actions[0]["action_kwargs"]["vessel"]["id"] == "reactor_01"
|
||
|
||
|
||
# ============ 全链路测试:HeatChill 协议 ============
|
||
|
||
class TestHeatChillProtocolFullChain:
|
||
"""HeatChill 协议全链路"""
|
||
|
||
def test_heatchill_with_enriched_resource(self):
|
||
from unilabos.compile.heatchill_protocol import generate_heat_chill_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01", klass="virtual_stirrer")]
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_heat_chill_protocol(
|
||
G=G,
|
||
vessel=enriched_vessel,
|
||
temp=80.0,
|
||
time="300",
|
||
)
|
||
|
||
assert isinstance(actions, list)
|
||
assert len(actions) >= 1
|
||
action = actions[0]
|
||
assert action["device_id"] == "heatchill_1"
|
||
assert action["action_name"] == "heat_chill"
|
||
assert action["action_kwargs"]["temp"] == 80.0
|
||
|
||
def test_heatchill_start_with_enriched_resource(self):
|
||
from unilabos.compile.heatchill_protocol import generate_heat_chill_start_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_heat_chill_start_protocol(
|
||
G=G,
|
||
vessel=enriched_vessel,
|
||
temp=60.0,
|
||
)
|
||
|
||
assert len(actions) >= 1
|
||
assert actions[0]["action_name"] == "heat_chill_start"
|
||
assert actions[0]["action_kwargs"]["temp"] == 60.0
|
||
|
||
def test_heatchill_stop_with_enriched_resource(self):
|
||
from unilabos.compile.heatchill_protocol import generate_heat_chill_stop_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_heat_chill_stop_protocol(G=G, vessel=enriched_vessel)
|
||
|
||
assert len(actions) >= 1
|
||
assert actions[0]["action_name"] == "heat_chill_stop"
|
||
|
||
|
||
# ============ 全链路测试:Add 协议 ============
|
||
|
||
class TestAddProtocolFullChain:
|
||
"""Add 协议全链路:vessel enrichment + reagent 查找 + 泵传输"""
|
||
|
||
def test_add_solid_with_enriched_resource(self):
|
||
from unilabos.compile.add_protocol import generate_add_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_add_protocol(
|
||
G=G,
|
||
vessel=enriched_vessel,
|
||
reagent="NaCl",
|
||
mass="5 g",
|
||
)
|
||
|
||
assert isinstance(actions, list)
|
||
assert len(actions) >= 1
|
||
# 应该包含至少一个 add_solid 或 log_message 动作
|
||
action_names = [a.get("action_name", "") for a in actions]
|
||
assert any(name in ["add_solid", "log_message"] for name in action_names)
|
||
|
||
def test_add_liquid_with_enriched_resource(self):
|
||
from unilabos.compile.add_protocol import generate_add_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched_vessel = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_add_protocol(
|
||
G=G,
|
||
vessel=enriched_vessel,
|
||
reagent="water",
|
||
volume="10 mL",
|
||
)
|
||
|
||
assert isinstance(actions, list)
|
||
assert len(actions) >= 1
|
||
|
||
|
||
# ============ 全链路测试:ResourceDictInstance 兼容层 ============
|
||
|
||
class TestResourceDictInstanceCompatibility:
|
||
"""验证编译器兼容层对 ResourceDictInstance 的处理"""
|
||
|
||
def test_get_vessel_from_enriched_dict(self):
|
||
"""get_vessel 对 enriched dict 的处理"""
|
||
raw_data = [_make_raw_resource(
|
||
id="reactor_01",
|
||
data={"temperature": 25.0, "liquid": [{"liquid_type": "water", "volume": 10.0}]},
|
||
)]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
vessel_id, vessel_data = get_vessel(enriched)
|
||
assert vessel_id == "reactor_01"
|
||
assert vessel_data["temperature"] == 25.0
|
||
assert len(vessel_data["liquid"]) == 1
|
||
|
||
def test_get_vessel_from_resource_instance(self):
|
||
"""get_vessel 直接对 ResourceDictInstance 的处理"""
|
||
raw_data = [_make_raw_resource(
|
||
id="reactor_01",
|
||
data={"temperature": 25.0},
|
||
)]
|
||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||
inst = tree_set.trees[0].root_node
|
||
|
||
vessel_id, vessel_data = get_vessel(inst)
|
||
assert vessel_id == "reactor_01"
|
||
assert vessel_data["temperature"] == 25.0
|
||
|
||
def test_ensure_resource_instance_round_trip(self):
|
||
"""ensure_resource_instance → resource_to_dict 无损往返"""
|
||
raw_data = [_make_raw_resource(
|
||
id="reactor_01", uuid="uuid-r01", klass="virtual_stirrer",
|
||
data={"temp": 25.0},
|
||
)]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
# dict → ResourceDictInstance
|
||
inst = ensure_resource_instance(enriched)
|
||
assert isinstance(inst, ResourceDictInstance)
|
||
assert inst.res_content.id == "reactor_01"
|
||
assert inst.res_content.uuid == "uuid-r01"
|
||
|
||
# ResourceDictInstance → dict
|
||
d = resource_to_dict(inst)
|
||
assert isinstance(d, dict)
|
||
assert d["id"] == "reactor_01"
|
||
assert d["uuid"] == "uuid-r01"
|
||
assert d["class"] == "virtual_stirrer"
|
||
|
||
|
||
# ============ 全链路测试:带 children 的资源树 ============
|
||
|
||
class TestResourceTreeWithChildren:
|
||
"""测试带 children 结构的资源树通过编译器的路径"""
|
||
|
||
def _make_tree_with_children(self):
|
||
"""构建 StationA -> [Flask1, Flask2] 的资源树"""
|
||
return [
|
||
_make_raw_resource(
|
||
id="StationA", uuid="uuid-station-a",
|
||
name="StationA", klass="workstation", type_="device",
|
||
),
|
||
_make_raw_resource(
|
||
id="Flask1", uuid="uuid-flask-1",
|
||
name="Flask1", klass="", type_="resource",
|
||
parent="StationA", parent_uuid="uuid-station-a",
|
||
data={"liquid": [{"liquid_type": "water", "volume": 10.0}]},
|
||
),
|
||
_make_raw_resource(
|
||
id="Flask2", uuid="uuid-flask-2",
|
||
name="Flask2", klass="", type_="resource",
|
||
parent="StationA", parent_uuid="uuid-station-a",
|
||
data={"liquid": [{"liquid_type": "ethanol", "volume": 5.0}]},
|
||
),
|
||
]
|
||
|
||
def test_enrichment_preserves_children_structure(self):
|
||
"""验证 enrichment 后 children 为嵌套 dict"""
|
||
raw_data = self._make_tree_with_children()
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
assert enriched["id"] == "StationA"
|
||
assert "children" in enriched
|
||
assert isinstance(enriched["children"], dict)
|
||
assert "Flask1" in enriched["children"]
|
||
assert "Flask2" in enriched["children"]
|
||
|
||
def test_children_preserve_uuid_and_data(self):
|
||
"""验证 children 中的 uuid 和 data 被正确保留"""
|
||
raw_data = self._make_tree_with_children()
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
flask1 = enriched["children"]["Flask1"]
|
||
assert flask1["uuid"] == "uuid-flask-1"
|
||
assert flask1["data"]["liquid"][0]["liquid_type"] == "water"
|
||
assert flask1["data"]["liquid"][0]["volume"] == 10.0
|
||
|
||
flask2 = enriched["children"]["Flask2"]
|
||
assert flask2["uuid"] == "uuid-flask-2"
|
||
assert flask2["data"]["liquid"][0]["liquid_type"] == "ethanol"
|
||
|
||
def test_children_dict_can_be_popped(self):
|
||
"""模拟 batch_transfer_protocol 中 pop children 的操作"""
|
||
raw_data = self._make_tree_with_children()
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
# batch_transfer_protocol 中会 pop children
|
||
children = enriched["children"]
|
||
popped = children.pop("Flask1")
|
||
assert popped["id"] == "Flask1"
|
||
assert "Flask1" not in enriched["children"]
|
||
assert "Flask2" in enriched["children"]
|
||
|
||
def test_children_dict_usable_as_from_repo(self):
|
||
"""模拟 batch_transfer_protocol 中 from_repo 参数"""
|
||
raw_data = self._make_tree_with_children()
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
# 模拟编译器接收的 from_repo 格式
|
||
from_repo = {"StationA": enriched}
|
||
from_repo_ = list(from_repo.values())[0]
|
||
|
||
assert from_repo_["id"] == "StationA"
|
||
assert "Flask1" in from_repo_["children"]
|
||
assert from_repo_["children"]["Flask1"]["uuid"] == "uuid-flask-1"
|
||
|
||
def test_sequence_resource_enrichment(self):
|
||
"""sequence<Resource> 情况:多个独立资源树"""
|
||
raw_data1 = [_make_raw_resource(id="R1", uuid="uuid-r1")]
|
||
raw_data2 = [_make_raw_resource(id="R2", uuid="uuid-r2")]
|
||
|
||
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"
|
||
|
||
|
||
# ============ 全链路测试:动作列表结构验证 ============
|
||
|
||
class TestActionListStructure:
|
||
"""验证编译器返回的 action_list 结构符合 workstation 预期"""
|
||
|
||
def _validate_action(self, action):
|
||
"""验证单个 action dict 的结构"""
|
||
if action.get("action_name") == "wait":
|
||
# wait 伪动作不需要 device_id
|
||
assert "action_kwargs" in action
|
||
assert "time" in action["action_kwargs"]
|
||
return
|
||
|
||
if action.get("action_name") == "log_message":
|
||
# log 伪动作
|
||
assert "action_kwargs" in action
|
||
return
|
||
|
||
# 正常设备动作
|
||
assert "device_id" in action, f"action 缺少 device_id: {action}"
|
||
assert "action_name" in action, f"action 缺少 action_name: {action}"
|
||
assert "action_kwargs" in action, f"action 缺少 action_kwargs: {action}"
|
||
assert isinstance(action["action_kwargs"], dict)
|
||
|
||
def test_stir_action_list_structure(self):
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_stir_protocol(G=G, vessel=enriched, time="60")
|
||
|
||
for action in actions:
|
||
if isinstance(action, list):
|
||
# 并行动作
|
||
for sub_action in action:
|
||
self._validate_action(sub_action)
|
||
else:
|
||
self._validate_action(action)
|
||
|
||
def test_heatchill_action_list_structure(self):
|
||
from unilabos.compile.heatchill_protocol import generate_heat_chill_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_heat_chill_protocol(G=G, vessel=enriched, temp=80.0, time="60")
|
||
|
||
for action in actions:
|
||
if isinstance(action, list):
|
||
for sub_action in action:
|
||
self._validate_action(sub_action)
|
||
else:
|
||
self._validate_action(action)
|
||
|
||
def test_add_action_list_structure(self):
|
||
from unilabos.compile.add_protocol import generate_add_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
|
||
G = _build_test_graph()
|
||
actions = generate_add_protocol(G=G, vessel=enriched, reagent="NaCl", mass="5 g")
|
||
|
||
for action in actions:
|
||
if isinstance(action, list):
|
||
for sub_action in action:
|
||
self._validate_action(sub_action)
|
||
else:
|
||
self._validate_action(action)
|
||
|
||
|
||
# ============ 全链路测试:message_converter 到 enrichment ============
|
||
|
||
class TestMessageConverterToEnrichment:
|
||
"""模拟从 ROS 消息转换后的 dict 到 enrichment 的完整链路"""
|
||
|
||
def test_ros_goal_conversion_simulation(self):
|
||
"""
|
||
模拟 workstation.py 中的完整流程:
|
||
1. ROS goal 中的 vessel 字段被 convert_from_ros_msg 转换为浅层 dict
|
||
2. workstation 用 resource_id 请求 host 获取完整资源数据
|
||
3. ResourceTreeSet.from_raw_dict_list 构建资源树
|
||
4. get_plr_nested_dict 生成嵌套 dict 替换 protocol_kwargs[k]
|
||
"""
|
||
# 步骤1: 模拟 convert_from_ros_msg 的输出(浅层 dict,只有 id 等基本字段)
|
||
shallow_vessel = {
|
||
"id": "reactor_01",
|
||
"uuid": "uuid-reactor-01",
|
||
"name": "reactor_01",
|
||
"type": "device",
|
||
"category": "virtual_stirrer",
|
||
"children": [],
|
||
"parent": "",
|
||
"parent_uuid": "",
|
||
"config": {},
|
||
"data": {},
|
||
"extra": {},
|
||
"position": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||
}
|
||
|
||
protocol_kwargs = {
|
||
"vessel": shallow_vessel,
|
||
"time": "300",
|
||
"stir_speed": 300.0,
|
||
}
|
||
|
||
# 步骤2: 提取 resource_id
|
||
resource_id = protocol_kwargs["vessel"]["id"]
|
||
assert resource_id == "reactor_01"
|
||
|
||
# 步骤3: 模拟 host 返回完整数据(带 children)
|
||
host_response = [
|
||
_make_raw_resource(
|
||
id="reactor_01", uuid="uuid-reactor-01",
|
||
klass="virtual_stirrer", type_="device",
|
||
data={"temperature": 25.0, "pressure": 1.0},
|
||
config={"max_temp": 300.0},
|
||
),
|
||
]
|
||
|
||
# 步骤4: enrichment
|
||
enriched = _simulate_workstation_resource_enrichment(host_response)
|
||
protocol_kwargs["vessel"] = enriched
|
||
|
||
# 验证 enrichment 后的 protocol_kwargs
|
||
assert protocol_kwargs["vessel"]["id"] == "reactor_01"
|
||
assert protocol_kwargs["vessel"]["uuid"] == "uuid-reactor-01"
|
||
assert protocol_kwargs["vessel"]["class"] == "virtual_stirrer"
|
||
assert protocol_kwargs["vessel"]["data"]["temperature"] == 25.0
|
||
assert protocol_kwargs["vessel"]["config"]["max_temp"] == 300.0
|
||
|
||
# 步骤5: 传给编译器
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
G = _build_test_graph()
|
||
actions = generate_stir_protocol(G=G, **protocol_kwargs)
|
||
|
||
assert len(actions) >= 1
|
||
assert actions[0]["device_id"] == "stirrer_1"
|
||
assert actions[0]["action_name"] == "stir"
|
||
|
||
def test_ros_goal_with_children_enrichment(self):
|
||
"""ROS goal → enrichment 带 children 的场景(batch transfer)"""
|
||
# 模拟 host 返回带 children 的数据
|
||
host_response = [
|
||
_make_raw_resource(
|
||
id="StationA", uuid="uuid-sa", klass="workstation", type_="device",
|
||
config={"num_items_x": 4, "num_items_y": 2},
|
||
),
|
||
_make_raw_resource(
|
||
id="Plate1", uuid="uuid-p1", type_="resource",
|
||
parent="StationA", parent_uuid="uuid-sa",
|
||
data={"sample": "sample_A"},
|
||
),
|
||
_make_raw_resource(
|
||
id="Plate2", uuid="uuid-p2", type_="resource",
|
||
parent="StationA", parent_uuid="uuid-sa",
|
||
data={"sample": "sample_B"},
|
||
),
|
||
]
|
||
|
||
enriched = _simulate_workstation_resource_enrichment(host_response)
|
||
|
||
assert enriched["id"] == "StationA"
|
||
assert enriched["class"] == "workstation"
|
||
assert len(enriched["children"]) == 2
|
||
assert enriched["children"]["Plate1"]["data"]["sample"] == "sample_A"
|
||
assert enriched["children"]["Plate2"]["uuid"] == "uuid-p2"
|
||
|
||
# 模拟 batch_transfer 的 from_repo 格式
|
||
from_repo = {"StationA": enriched}
|
||
from_repo_ = list(from_repo.values())[0]
|
||
assert "Plate1" in from_repo_["children"]
|
||
assert from_repo_["children"]["Plate1"]["uuid"] == "uuid-p1"
|
||
|
||
|
||
# ============ 全链路测试:多协议连续调用 ============
|
||
|
||
class TestMultiProtocolChain:
|
||
"""模拟连续执行多个协议(如 add → stir → heatchill)"""
|
||
|
||
def test_sequential_protocol_execution(self):
|
||
"""模拟典型合成路径:add → stir → heatchill"""
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
from unilabos.compile.heatchill_protocol import generate_heat_chill_protocol
|
||
from unilabos.compile.add_protocol import generate_add_protocol
|
||
|
||
raw_data = [_make_raw_resource(
|
||
id="reactor_01", uuid="uuid-reactor-01",
|
||
klass="virtual_stirrer", type_="device",
|
||
)]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
G = _build_test_graph()
|
||
|
||
# 每次调用用 enriched 的副本,避免编译器修改原数据
|
||
all_actions = []
|
||
|
||
# 步骤1: 添加试剂
|
||
add_actions = generate_add_protocol(
|
||
G=G, vessel=copy.deepcopy(enriched),
|
||
reagent="NaCl", mass="5 g",
|
||
)
|
||
all_actions.extend(add_actions)
|
||
|
||
# 步骤2: 搅拌
|
||
stir_actions = generate_stir_protocol(
|
||
G=G, vessel=copy.deepcopy(enriched),
|
||
time="60", stir_speed=300.0,
|
||
)
|
||
all_actions.extend(stir_actions)
|
||
|
||
# 步骤3: 加热
|
||
heat_actions = generate_heat_chill_protocol(
|
||
G=G, vessel=copy.deepcopy(enriched),
|
||
temp=80.0, time="300",
|
||
)
|
||
all_actions.extend(heat_actions)
|
||
|
||
# 验证总动作列表
|
||
assert len(all_actions) >= 3
|
||
# 每个协议至少产生一个核心动作
|
||
action_names = [a.get("action_name", "") for a in all_actions if isinstance(a, dict)]
|
||
assert "stir" in action_names
|
||
assert "heat_chill" in action_names
|
||
|
||
def test_enriched_resource_not_mutated(self):
|
||
"""验证编译器不应修改传入的 enriched dict(如果需要修改应 deepcopy)"""
|
||
from unilabos.compile.stir_protocol import generate_stir_protocol
|
||
|
||
raw_data = [_make_raw_resource(id="reactor_01")]
|
||
enriched = _simulate_workstation_resource_enrichment(raw_data)
|
||
original_id = enriched["id"]
|
||
original_uuid = enriched["uuid"]
|
||
|
||
G = _build_test_graph()
|
||
generate_stir_protocol(G=G, vessel=enriched, time="60")
|
||
|
||
# 验证 enriched dict 核心字段未被修改
|
||
assert enriched["id"] == original_id
|
||
assert enriched["uuid"] == original_uuid
|