|
#include <gtest/gtest.h> |
|
#include <gmock/gmock.h> |
|
#include "behaviortree_cpp/leaf_node.h" |
|
#include "behaviortree_cpp/bt_factory.h" |
|
|
|
namespace BT::test |
|
{ |
|
using ::testing::Assign; |
|
using ::testing::DoAll; |
|
using ::testing::Return; |
|
|
|
/// @brief Mocked @c BT::LeafNode that allows callers to trivially customize behavior of @c tick() and @c halt(). |
|
/// @tparam NodeTypeT The type of leaf node this class is. |
|
template <NodeType NodeTypeT> |
|
class MockLeafNode : public LeafNode |
|
{ |
|
public: |
|
MockLeafNode(const std::string& name) |
|
: LeafNode(name, {}) |
|
{ |
|
} |
|
|
|
// Made public for setting actions and expectations |
|
MOCK_METHOD(NodeStatus, tick, (), (override)); |
|
MOCK_METHOD(void, halt, (), (override)); |
|
|
|
NodeType type() const override |
|
{ |
|
return NodeTypeT; |
|
} |
|
}; |
|
|
|
TEST(PPA, EnsureWarm) |
|
{ |
|
// This test shows the basic structure of a PPA: a fallback of a postcondition and an action to make that |
|
// postcondition true. |
|
/* |
|
<BehaviorTree ID="EnsureWarm"> |
|
<ReactiveFallback> |
|
<IsWarm /> |
|
<ReactiveSequence> |
|
<IsHoldingJacket /> |
|
<WearJacket /> |
|
</ReactiveSequence> |
|
</ReactiveFallback> |
|
</BehaviorTree> |
|
*/ |
|
|
|
// The final condition of the PPA; the thing that make_warm achieves. For this example, we're only warm after |
|
// WearJacket returns success. |
|
MockLeafNode<NodeType::CONDITION> is_warm("IsWarm"); |
|
NodeStatus is_warm_return = NodeStatus::FAILURE; |
|
EXPECT_CALL(is_warm, tick()) |
|
.WillRepeatedly([&is_warm_return]() { return is_warm_return; }); |
|
|
|
// For this example, we already have a jacket |
|
MockLeafNode<NodeType::CONDITION> is_holding_jacket("IsHoldingJacket"); |
|
EXPECT_CALL(is_holding_jacket, tick()) |
|
.Times(2) |
|
.WillRepeatedly(Return(NodeStatus::SUCCESS)); |
|
|
|
// Putting the jacket on takes two ticks, and updates the return value of IsWarm |
|
MockLeafNode<NodeType::ACTION> wear_jacket("WearJacket"); |
|
EXPECT_CALL(wear_jacket, tick()) |
|
.WillOnce(Return(NodeStatus::RUNNING)) |
|
.WillOnce(DoAll(Assign(&is_warm_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS))); |
|
|
|
ReactiveSequence make_warm("MakeWarm"); |
|
make_warm.addChild(&is_holding_jacket); |
|
make_warm.addChild(&wear_jacket); |
|
|
|
ReactiveFallback ensure_warm("EnsureWarm"); |
|
ensure_warm.addChild(&is_warm); |
|
ensure_warm.addChild(&make_warm); |
|
|
|
|
|
// first tick: not warm, have a jacket: start wearing it |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING); |
|
|
|
// second tick: warm (wearing succeeded) |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS); |
|
|
|
// third tick: still warm (just the postcondition ticked) |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS); |
|
} |
|
|
|
TEST(PPA, EnsureWarmWithEnsureHoldingHacket) |
|
{ |
|
// This test backchains on HoldingHacket => EnsureHoldingHacket to iteratively add reactivity and functionality to the tree. |
|
// The general structure of the PPA remains the same. |
|
/* |
|
<BehaviorTree ID="EnsureWarm"> |
|
<ReactiveFallback> |
|
<IsWarm /> |
|
<ReactiveSequence> |
|
<SubTree ID="EnsureHoldingJacket" /> |
|
<WearJacket /> |
|
</ReactiveSequence> |
|
</ReactiveFallback> |
|
</BehaviorTree> |
|
|
|
<BehaviorTree ID="EnsureHoldingJacket"> |
|
<ReactiveFallback> |
|
<IsHoldingJacket /> |
|
<ReactiveSequence> |
|
<IsNearCloset /> |
|
<GrabJacket /> |
|
</ReactiveSequence> |
|
</ReactiveFallback> |
|
</BehaviorTree> |
|
*/ |
|
|
|
// Same postcondition as first test. |
|
MockLeafNode<NodeType::CONDITION> is_warm("IsWarm"); |
|
NodeStatus is_warm_return = NodeStatus::FAILURE; // initially: not warm |
|
EXPECT_CALL(is_warm, tick()) |
|
.WillRepeatedly([&is_warm_return]() { return is_warm_return; }); |
|
|
|
// The new PPA's postcondition is the same condition that we're backchaining on. |
|
MockLeafNode<NodeType::CONDITION> is_holding_jacket("IsHoldingJacket"); |
|
NodeStatus is_holding_jacket_return = NodeStatus::FAILURE; // initially: not holding a jacket |
|
EXPECT_CALL(is_holding_jacket, tick()) |
|
.WillRepeatedly([&is_holding_jacket_return]() { return is_holding_jacket_return; }); |
|
|
|
// For this test, we're already near a closet. This condition could be backchained! |
|
MockLeafNode<NodeType::CONDITION> is_near_closet("IsNearCloset"); |
|
EXPECT_CALL(is_near_closet, tick()) |
|
.Times(2) |
|
.WillRepeatedly(Return(NodeStatus::SUCCESS)); |
|
|
|
// Same as first test's action: running once, then success, updating this PPA's postcondition. |
|
MockLeafNode<NodeType::ACTION> grab_jacket("GrabJacket"); |
|
EXPECT_CALL(grab_jacket, tick()) |
|
.WillOnce(Return(NodeStatus::RUNNING)) |
|
.WillRepeatedly(DoAll(Assign(&is_holding_jacket_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS))); |
|
|
|
// Same as first test. |
|
MockLeafNode<NodeType::ACTION> wear_jacket("WearJacket"); |
|
EXPECT_CALL(wear_jacket, tick()) |
|
.WillOnce(Return(NodeStatus::RUNNING)) |
|
.WillOnce(DoAll(Assign(&is_warm_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS))); |
|
|
|
// The new PPA's precondition-action (PA) is the same form as the root PPA's PA. Similar to MakeWarm, this PA ticks |
|
// running twice and updates the postcondition afterwards. |
|
ReactiveSequence grab_jacket_from_closet("GrabJacketFromCloset"); |
|
grab_jacket_from_closet.addChild(&is_near_closet); |
|
grab_jacket_from_closet.addChild(&grab_jacket); |
|
|
|
// For this example, our precondition is another PPA: it short circuits if we already have a jacket, otherwise it |
|
// fetches one asynchronously. |
|
ReactiveFallback ensure_holding_jacket("EnsureHoldingJacket"); |
|
ensure_holding_jacket.addChild(&is_holding_jacket); |
|
ensure_holding_jacket.addChild(&grab_jacket_from_closet); |
|
|
|
ReactiveSequence make_warm("MakeWarm"); |
|
make_warm.addChild(&ensure_holding_jacket); // Use the new PPA within PA, not the original precondition. |
|
make_warm.addChild(&wear_jacket); |
|
|
|
ReactiveFallback ensure_warm("EnsureWarm"); |
|
ensure_warm.addChild(&is_warm); |
|
ensure_warm.addChild(&make_warm); |
|
|
|
// BUG(755): need to disable exception throwing on reactive control nodes to support backchaining. |
|
ReactiveSequence::EnableException(false); |
|
ReactiveFallback::EnableException(false); |
|
|
|
// first tick: not warm, no jacket, near a closet: start grabbing a jacket |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING); |
|
|
|
// first tick: not warm, has a jacket (grabbing succeeded), not wearing: start wearing |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING); |
|
|
|
// third tick: warm (wearing succeeded) |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS); |
|
|
|
// fourth tick: still warm (just the postcondition ticked) |
|
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS); |
|
} |
|
} |