alkaid_release_platform/tests/test_flow.py

443 lines
14 KiB
Python
Raw Normal View History

from pathlib import Path
import pytest
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from thirdparty.py_trees.trees import BehaviourTree
from core.behavior_tree import ReleaseFlowActionDecorator, ReleaseFlowBuilder, ReleaseFlowAction
from thirdparty.py_trees.common import Status, Access
from core.logger import get_logger
log = get_logger()
class ActionTest(ReleaseFlowAction):
"""测试用的Action节点"""
def __init__(self, name: str, return_status: Status, description: str = ""):
super().__init__(name, description)
self.return_status = return_status
self.process_called = False
self.setup_called = False
self.shutdown_called = False
def setup(self):
self.setup_called = True
log.info(f"setup: {self.name}")
def process(self):
self.process_called = True
self.status = self.return_status
log.info(f"process: {self.name}, status: {self.status}")
def update(self):
return self.status
def shutdown(self):
self.shutdown_called = True
class ActionTestBlackboard(ReleaseFlowAction):
"""测试Blackboard功能的Action节点"""
def __init__(self, name: str, description: str = ""):
super().__init__(name, description)
self.blackboard.register_key("test_key", access=Access.WRITE)
self.blackboard.register_key("read_only_key", access=Access.READ)
def process(self):
self.status = Status.SUCCESS
log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
def update(self):
return self.status
class ActionTestBlackboard1(ReleaseFlowAction):
"""测试Blackboard功能的Action节点"""
def __init__(self, name: str, description: str = ""):
super().__init__(name, description)
self.blackboard.require_key("test_key")
def process(self):
self.status = Status.SUCCESS
log.info(f"process: {self.name}, status: {self.status}, test_key: {self.blackboard.test_key}")
def update(self):
return self.status
@pytest.fixture
def builder():
"""创建ReleaseFlowBuilder实例的fixture"""
return ReleaseFlowBuilder()
def test_single_action():
"""测试单个Action节点的执行"""
action = ActionTest("test_action", Status.SUCCESS)
action.setup()
assert action.setup_called
action.tick_once()
assert action.process_called
assert action.status == Status.SUCCESS
action.shutdown()
assert action.shutdown_called
def test_sequence(builder):
"""测试Sequence节点的执行"""
success_action = ActionTest("success_action", Status.SUCCESS)
failure_action = ActionTest("failure_action", Status.FAILURE)
with builder.sequence("test_sequence") as b:
b.action(success_action)
b.action(failure_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert failure_action.process_called
assert flow.status == Status.FAILURE
assert success_action.process_called
def test_selector(builder):
"""测试Selector节点的执行"""
failure_action = ActionTest("failure_action", Status.FAILURE)
success_action = ActionTest("success_action", Status.SUCCESS)
with builder.selector("test_selector") as b:
b.action(failure_action)
b.action(success_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.SUCCESS
assert failure_action.process_called
assert success_action.process_called
def test_blackboard():
"""测试Blackboard功能"""
action = ActionTestBlackboard("test_blackboard")
action1 = ActionTestBlackboard1("test_blackboard_read")
# 测试写入权限
action.blackboard.test_key = "test_value"
# 测试共享
assert action1.blackboard.test_key == "test_value"
# 测试只读权限
with pytest.raises(AttributeError):
action.blackboard.read_only_key = "new_value"
# 测试必需key
with pytest.raises(AttributeError):
_ = action.blackboard.required_key
def test_parallel(builder):
"""测试Parallel节点的执行"""
success_action1 = ActionTest("success_action1", Status.SUCCESS)
success_action2 = ActionTest("success_action2", Status.SUCCESS)
failure_action = ActionTest("failure_action", Status.FAILURE)
with builder.parallel("test_parallel") as b:
b.action(success_action1)
b.action(success_action2)
b.action(failure_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.FAILURE
assert success_action1.process_called
assert success_action2.process_called
assert failure_action.process_called
def test_nested_behavior_tree(builder):
"""测试嵌套的行为树结构"""
success_action1 = ActionTest("success_action1", Status.SUCCESS)
success_action2 = ActionTest("success_action2", Status.SUCCESS)
failure_action = ActionTest("failure_action", Status.FAILURE)
with builder.sequence("root_sequence") as b:
with b.selector("nested_selector") as s:
s.action(failure_action)
s.action(success_action1)
b.action(success_action2)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.SUCCESS
assert failure_action.process_called
assert success_action1.process_called
assert success_action2.process_called
def test_parallel_success_on_all(builder):
"""测试并行节点SuccessOnAll策略"""
success_action1 = ActionTest("success_action1", Status.SUCCESS)
success_action2 = ActionTest("success_action2", Status.SUCCESS)
success_action3 = ActionTest("success_action3", Status.SUCCESS)
with builder.parallel("test_parallel_success_on_all") as b:
b.action(success_action1)
b.action(success_action2)
b.action(success_action3)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.SUCCESS
assert success_action1.process_called
assert success_action2.process_called
assert success_action3.process_called
def test_parallel_success_on_one(builder):
"""测试并行节点SuccessOnOne策略"""
from thirdparty.py_trees.common import ParallelPolicy
running_action1 = ActionTest("running_action1", Status.RUNNING)
success_action = ActionTest("success_action", Status.SUCCESS)
running_action2 = ActionTest("running_action2", Status.RUNNING)
with builder.parallel("test_parallel_success_on_one",
policy=ParallelPolicy.SuccessOnOne()) as b:
b.action(running_action1)
b.action(success_action)
b.action(running_action2)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.SUCCESS
assert running_action1.process_called
assert success_action.process_called
assert running_action2.process_called
def test_multiple_executions(builder):
"""测试行为树的多次执行"""
action1 = ActionTest("action1", Status.SUCCESS)
action2 = ActionTest("action2", Status.SUCCESS)
with builder.sequence("test_multiple_executions") as b:
b.action(action1)
b.action(action2)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
# 第一次执行
tree.tick()
assert flow.status == Status.SUCCESS
assert action1.process_called
assert action2.process_called
# 重置状态
action1.process_called = False
action2.process_called = False
# 第二次执行
tree.tick()
assert flow.status == Status.SUCCESS
assert action1.process_called
assert action2.process_called
def test_error_handling(builder):
"""测试行为树的错误处理"""
# 测试空的行为树
with pytest.raises(Exception) as excinfo:
builder.build()
assert "No nodes added to behavior tree" in str(excinfo.value)
# 测试未闭合的节点
with pytest.raises(Exception) as excinfo:
with builder.sequence("unclosed_sequence") as b:
b.action(ActionTest("action", Status.SUCCESS))
b.action(ActionTest("action", Status.SUCCESS))
# 不调用end()或使用with语句的__exit__
builder.build()
assert "Unclosed nodes" in str(excinfo.value)
class ActionWithError(ReleaseFlowAction):
"""测试异常处理的Action节点"""
def __init__(self, name: str, description: str = ""):
super().__init__(name, description)
self.process_called = False
def process(self):
self.process_called = True
raise Exception("测试异常")
def update(self):
return Status.FAILURE
def test_action_exception_handling(builder):
"""测试Action节点异常处理"""
error_action = ActionWithError("error_action")
with builder.sequence("test_error_handling") as b:
b.action(error_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
# 执行时应该捕获异常并返回FAILURE状态
tree.tick()
assert error_action.process_called
assert flow.status == Status.FAILURE
def test_decorator():
"""测试ReleaseFlowActionDecorator装饰器"""
@ReleaseFlowActionDecorator
class DecoratedAction(ReleaseFlowAction):
def process(self):
self.status = Status.SUCCESS
def update(self):
return self.status
# 测试自动生成名称
action1 = DecoratedAction()
assert action1.name == f"{Path(__file__).stem}.DecoratedAction"
# 测试自定义名称
action2 = DecoratedAction(name="custom_name")
assert action2.name == "custom_name"
# 测试功能
action2.setup()
action2.tick_once()
assert action2.status == Status.SUCCESS
def test_complex_behavior_tree(builder):
"""测试复杂的行为树结构"""
success_action1 = ActionTest("success_action1", Status.SUCCESS)
success_action2 = ActionTest("success_action2", Status.SUCCESS)
failure_action1 = ActionTest("failure_action1", Status.FAILURE)
success_action3 = ActionTest("success_action3", Status.SUCCESS)
with builder.sequence("root") as b:
with b.parallel("parallel_group") as p:
p.action(success_action1)
p.action(success_action2)
with b.selector("selector_group") as s:
s.action(failure_action1)
s.action(success_action3)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
tree.tick()
assert flow.status == Status.SUCCESS
assert success_action1.process_called
assert success_action2.process_called
assert failure_action1.process_called
assert success_action3.process_called
def test_blackboard_advanced():
"""测试Blackboard的高级功能"""
class ActionWrite(ReleaseFlowAction):
def __init__(self, name: str, description: str = ""):
super().__init__(name, description)
self.blackboard.register_key("counter", access=Access.WRITE)
self.blackboard.counter = 0
def process(self):
self.blackboard.counter += 1
self.status = Status.SUCCESS
def update(self):
return self.status
class ActionRead(ReleaseFlowAction):
def __init__(self, name: str, description: str = ""):
super().__init__(name, description)
self.blackboard.register_key("counter", access=Access.READ)
self.value = 0
def process(self):
self.value = self.blackboard.counter
self.status = Status.SUCCESS
def update(self):
return self.status
# 创建动作节点
write_action = ActionWrite("write_action")
read_action = ActionRead("read_action")
# 设置初始值
write_action.blackboard.counter = 10
# 测试读取
assert read_action.blackboard.counter == 10
# 测试写入
write_action.blackboard.counter = 20
assert read_action.blackboard.counter == 20
# 测试只读权限
with pytest.raises(AttributeError):
read_action.blackboard.counter = 30
def test_action_exception_handling_with_sequence(builder):
"""测试Action节点异常处理后行为树继续执行的能力"""
error_action = ActionWithError("error_action")
success_action = ActionTest("success_action", Status.SUCCESS)
with builder.sequence("test_error_handling_sequence") as b:
b.action(error_action)
b.action(success_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
# 执行时应该捕获异常并返回FAILURE状态但不会中断行为树的执行
tree.tick()
# 验证error_action被标记为已处理
assert error_action.process_called
# 验证序列节点状态为FAILURE因为第一个节点失败
assert flow.status == Status.FAILURE
# 验证success_action没有被执行因为序列节点在第一个节点失败后停止
assert not success_action.process_called
def test_action_exception_handling_with_selector(builder):
"""测试Action节点异常处理后选择器继续尝试其他节点"""
error_action = ActionWithError("error_action")
success_action = ActionTest("success_action", Status.SUCCESS)
with builder.selector("test_error_handling_selector") as b:
b.action(error_action)
b.action(success_action)
flow = builder.build()
tree = BehaviourTree(root=flow)
tree.setup()
# 执行时应该捕获异常并继续尝试下一个节点
tree.tick()
# 验证error_action被标记为已处理
assert error_action.process_called
# 验证success_action被执行
assert success_action.process_called
# 验证选择器节点状态为SUCCESS因为第二个节点成功
assert flow.status == Status.SUCCESS