443 lines
14 KiB
Python
443 lines
14 KiB
Python
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
|