diff --git a/conanfile.txt b/conanfile.txt index 56dd97006..7b81d1d6d 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,5 @@ [requires] -gtest/1.12.1 +gtest/1.14.0 zeromq/4.3.4 sqlite3/3.40.1 diff --git a/src/controls/reactive_fallback.cpp b/src/controls/reactive_fallback.cpp index 92809410d..9b1c4adec 100644 --- a/src/controls/reactive_fallback.cpp +++ b/src/controls/reactive_fallback.cpp @@ -15,7 +15,7 @@ namespace BT { -bool ReactiveFallback::throw_if_multiple_running = true; +bool ReactiveFallback::throw_if_multiple_running = false; void ReactiveFallback::EnableException(bool enable) { diff --git a/src/controls/reactive_sequence.cpp b/src/controls/reactive_sequence.cpp index 8e1f529d5..ac76a9ee6 100644 --- a/src/controls/reactive_sequence.cpp +++ b/src/controls/reactive_sequence.cpp @@ -15,7 +15,7 @@ namespace BT { -bool ReactiveSequence::throw_if_multiple_running = true; +bool ReactiveSequence::throw_if_multiple_running = false; void ReactiveSequence::EnableException(bool enable) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4cdd9b30c..114f2c55d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,7 @@ set(BT_TESTS gtest_match.cpp gtest_json.cpp gtest_reactive.cpp + gtest_reactive_backchaining.cpp gtest_sequence.cpp gtest_skipping.cpp gtest_substitution.cpp @@ -55,12 +56,11 @@ elseif(catkin_FOUND AND CATKIN_ENABLE_TESTING) else() - find_package(GTest) + find_package(GTest REQUIRED) enable_testing() - add_executable(${BTCPP_LIBRARY}_test ${BT_TESTS} - gtest_enums.cpp - gtest_any.cpp) + add_executable(${BTCPP_LIBRARY}_test ${BT_TESTS}) + target_link_libraries(${PROJECT_NAME}_test ${TEST_DEPENDECIES} Threads::Threads diff --git a/tests/gtest_reactive_backchaining.cpp b/tests/gtest_reactive_backchaining.cpp new file mode 100644 index 000000000..ee92d2238 --- /dev/null +++ b/tests/gtest_reactive_backchaining.cpp @@ -0,0 +1,184 @@ +#include +#include "behaviortree_cpp/loggers/bt_observer.h" +#include "behaviortree_cpp/bt_factory.h" + +namespace BT::test +{ + +class SimpleCondition : public BT::ConditionNode +{ +private: + std::string port_name_; +public: + SimpleCondition(const std::string& name, const BT::NodeConfig& config, + std::string port_name) : + BT::ConditionNode(name, config), + port_name_(port_name) + {} + static BT::PortsList providedPorts() { + return {}; + } + BT::NodeStatus tick() override + { + auto val = config().blackboard->get(port_name_); + return (val) ? NodeStatus::SUCCESS : NodeStatus::FAILURE; + } +}; + +//-------------------------- +class AsyncTestAction : public BT::StatefulActionNode +{ + int counter_ = 0; + std::string port_name_; +public: + AsyncTestAction(const std::string& name, const BT::NodeConfig& config, + std::string port_name) : + BT::StatefulActionNode(name, config), + port_name_(port_name) + {} + + static BT::PortsList providedPorts(){ return {}; } + + NodeStatus onStart() override { + counter_ = 0; + return NodeStatus::RUNNING; + } + + NodeStatus onRunning() override { + if(++counter_ == 2) { + config().blackboard->set(port_name_, true); + return NodeStatus::SUCCESS; + } + return NodeStatus::RUNNING; + } + void onHalted() override {} +}; +//-------------------------- + +TEST(ReactiveBackchaining, EnsureWarm) +{ + // This test shows the basic structure of a PPA: a fallback + // of a postcondition and an action to make that + // postcondition true. + static const char* xml_text = R"( + + + + + + + + + + + + )"; + + // The final condition of the PPA; the thing that make_warm achieves. + // For this example, we're only warm after WearJacket returns success. + BehaviorTreeFactory factory; + factory.registerNodeType("IsWarm", "is_warm"); + factory.registerNodeType("IsHoldingJacket", "holding_jacket"); + factory.registerNodeType("WearJacket", "is_warm"); + + Tree tree = factory.createTreeFromText(xml_text); + BT::TreeObserver observer(tree); + + auto& blackboard = tree.subtrees.front()->blackboard; + blackboard->set("is_warm", false); + blackboard->set("holding_jacket", true); + + // first tick: not warm, have a jacket: start wearing it + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + EXPECT_FALSE(blackboard->get("is_warm")); + + // second tick: not warm (still wearing) + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + EXPECT_FALSE(blackboard->get("is_warm")); + + // third tick: warm (wearing succeeded) + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::SUCCESS); + EXPECT_TRUE(blackboard->get("is_warm")); + + // fourth tick: still warm (just the condition ticked) + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::SUCCESS); + + EXPECT_EQ(observer.getStatistics("warm").failure_count, 3); + EXPECT_EQ(observer.getStatistics("warm").success_count, 1); + + EXPECT_EQ(observer.getStatistics("jacket").transitions_count, 3); + EXPECT_EQ(observer.getStatistics("jacket").success_count, 3); + + EXPECT_EQ(observer.getStatistics("wear").success_count, 1); +} + + +TEST(ReactiveBackchaining, 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. + static const char* xml_text = R"( + + + + + + + + + + + + + + + + + + + + + + )"; + + BehaviorTreeFactory factory; + factory.registerNodeType("IsWarm", "is_warm"); + factory.registerNodeType("IsHoldingJacket", "holding_jacket"); + factory.registerNodeType("IsNearCloset", "near_closet"); + factory.registerNodeType("WearJacket", "is_warm"); + factory.registerNodeType("GrabJacket", "holding_jacket"); + + factory.registerBehaviorTreeFromText(xml_text); + Tree tree = factory.createTree("EnsureWarm"); + BT::TreeObserver observer(tree); + + tree.subtrees[0]->blackboard->set("is_warm", false); + tree.subtrees[1]->blackboard->set("holding_jacket", false); + tree.subtrees[1]->blackboard->set("near_closet", true); + + // first tick: not warm, no jacket, start GrabJacket + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + EXPECT_FALSE(tree.subtrees[0]->blackboard->get("is_warm")); + EXPECT_FALSE(tree.subtrees[1]->blackboard->get("holding_jacket")); + EXPECT_TRUE(tree.subtrees[1]->blackboard->get("near_closet")); + + // second tick: still GrabJacket + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + + // third tick: GrabJacket succeeded, start wearing + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + EXPECT_FALSE(tree.subtrees[0]->blackboard->get("is_warm")); + EXPECT_TRUE(tree.subtrees[1]->blackboard->get("holding_jacket")); + + // fourth tick: still WearingJacket + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::RUNNING); + + // fifth tick: warm (WearingJacket succeeded) + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::SUCCESS); + EXPECT_TRUE(tree.subtrees[0]->blackboard->get("is_warm")); + + // sixr tick: still warm (just the condition ticked) + EXPECT_EQ(tree.tickExactlyOnce(), NodeStatus::SUCCESS); +} + +}