Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ CompileExample("t12_default_ports")
CompileExample("t13_access_by_ref")
CompileExample("t14_subtree_model")
CompileExample("t15_nodes_mocking")
CompileExample("t15_nodes_mocking_strict_failure")
CompileExample("t16_global_blackboard")
CompileExample("t17_blackboard_backup")
CompileExample("t18_waypoints")
Expand Down
2 changes: 1 addition & 1 deletion examples/t15_nodes_mocking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const char* xml_text = R"(

/**
* @brief In this example we will see how we can substitute some nodes
* in the Tree above with
* in the Tree above with mocks.
* @param argc
* @param argv
* @return
Expand Down
143 changes: 143 additions & 0 deletions examples/t15_nodes_mocking_strict_failure.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#include "dummy_nodes.h"

#include "behaviortree_cpp/bt_factory.h"

// clang-format off
namespace
{
const char* xml_text = R"(
<root BTCPP_format="4">

<BehaviorTree ID="MainTree">
<Sequence>
<SaySomething name="talk" message="hello world"/>

<SubTree ID="MySub" name="mysub"/>

<Script name="set_mock_flag" code="mock_should_fail:= false"/>
<Script name="set_message" code="msg:= 'the original message' "/>
<SaySomething message="{msg}"/>

<Sequence name="counting">
<SaySomething message="1"/>
<SaySomething message="2"/>
<SaySomething message="3"/>
</Sequence>
</Sequence>
</BehaviorTree>

<BehaviorTree ID="MySub">
<Sequence>
<AlwaysSuccess name="action_subA"/>
<AlwaysSuccess name="action_subB"/>
</Sequence>
</BehaviorTree>

</root>
)";
} // namespace
// clang-format on

/**
* @brief Companion to tutorial 15 that preserves strict failure propagation.
*
* If the substituted TestNode resolves to FAILURE, the enclosing Sequence stops
* immediately. The final blackboard message is still printed by this executable,
* but the tree itself does not log the failure branch through a fallback.
*/

int main(int /*argc*/, char** /*argv*/)
{
using namespace DummyNodes;
BT::BehaviorTreeFactory factory;
factory.registerNodeType<SaySomething>("SaySomething");
factory.registerBehaviorTreeFromText(xml_text);

{
auto tree = factory.createTree("MainTree");

std::cout << "----- Nodes fullPath() -------\n";
tree.applyVisitor(
[](BT::TreeNode* node) { std::cout << node->fullPath() << std::endl; });

std::cout << "\n------ Output (original) ------\n";
auto status = tree.tickWhileRunning();
std::cout << "Original status: " << BT::toStr(status, false) << std::endl;
}

factory.registerSimpleAction("DummyAction", [](BT::TreeNode& self) {
std::cout << "DummyAction substituting node with fullPath(): " << self.fullPath()
<< std::endl;
return BT::NodeStatus::SUCCESS;
});

factory.registerSimpleAction("DummySaySomething", [](BT::TreeNode& self) {
auto msg = self.getInput<std::string>("message");
std::cout << "DummySaySomething: " << msg.value() << std::endl;
return BT::NodeStatus::SUCCESS;
});

BT::TestNodeConfig test_config;
test_config.return_status_script = "(mock_should_fail == true) ? FAILURE : SUCCESS";
test_config.async_delay = std::chrono::milliseconds(2000);
test_config.success_script = "msg := 'message SUBSTITUTED' ";
test_config.failure_script = "msg := 'message FAILURE branch' ";

BT::TestNodeConfig counting_config;
counting_config.return_status = BT::NodeStatus::SUCCESS;

factory.addSubstitutionRule("mysub/action_*", "DummyAction");
factory.addSubstitutionRule("talk", "DummySaySomething");
factory.addSubstitutionRule("set_message", test_config);
factory.addSubstitutionRule("counting", counting_config);

auto blackboard = BT::Blackboard::create();
auto tree = factory.createTree("MainTree", blackboard);
std::cout << "\n------ Output (substituted, strict failure) ------\n";
auto status = tree.tickWhileRunning();
std::cout << "Substituted status: " << BT::toStr(status, false) << std::endl;
if(blackboard->getEntry("msg"))
{
std::cout << "Substituted final msg: " << blackboard->get<std::string>("msg")
<< std::endl;
}

return 0;
}

/* Expected output:

----- Nodes fullPath() -------
Sequence::1
talk
mysub
mysub/Sequence::4
mysub/action_subA
mysub/action_subB
set_mock_flag
set_message
SaySomething::9
counting
SaySomething::11
SaySomething::12
SaySomething::13

------ Output (original) ------
Robot says: hello world
Robot says: the original message
Robot says: 1
Robot says: 2
Robot says: 3
Original status: SUCCESS

------ Output (substituted, strict failure) ------
DummySaySomething: hello world
DummyAction substituting node with fullPath(): mysub/action_subA
DummyAction substituting node with fullPath(): mysub/action_subB
Substituted status: SUCCESS
Substituted final msg: message SUBSTITUTED

If you change mock_should_fail to true in the XML above, the substituted status
becomes FAILURE and the final msg becomes "message FAILURE branch".

*/
27 changes: 22 additions & 5 deletions include/behaviortree_cpp/actions/test_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@
#include "behaviortree_cpp/scripting/script_parser.hpp"
#include "behaviortree_cpp/utils/timer_queue.h"

#include <memory>
#include <string>

namespace BT
{

struct TestNodeConfig
{
/// status to return when the action is completed.
/// Status to return when the action is completed.
/// If return_status_script is also set, the script takes precedence.
NodeStatus return_status = NodeStatus::SUCCESS;

/// Optional script to compute the completion status dynamically.
///
/// This script is evaluated when the TestNode completes, after any
/// async_delay has elapsed, using the current blackboard state.
/// The result must resolve to the same set of statuses supported by
/// return_status, except IDLE which is always rejected.
/// When set, this takes precedence over return_status.
std::string return_status_script;

/// script to execute when complete_func() returns SUCCESS
std::string success_script;

Expand All @@ -38,14 +51,16 @@ struct TestNodeConfig
std::chrono::milliseconds async_delay = std::chrono::milliseconds(0);

/// Function invoked when the action is completed.
/// If not specified, the node will return [return_status]
/// If not specified, the node will use return_status_script when present,
/// otherwise it will return [return_status].
std::function<NodeStatus(void)> complete_func;
};

/**
* @brief The TestNode is a Node that can be configure to:
*
* 1. Return a specific status (SUCCESS / FAILURE)
* 1.b Compute the returned status from a script evaluated at completion time
* 2. Execute a post condition script (unless halted)
* 3. Either complete immediately (synchronous action), or after a
* given period of time (asynchronous action)
Expand Down Expand Up @@ -79,15 +94,17 @@ class TestNode : public BT::StatefulActionNode
}

protected:
virtual NodeStatus onStart() override;
NodeStatus onStart() override;

virtual NodeStatus onRunning() override;
NodeStatus onRunning() override;

virtual void onHalted() override;
void onHalted() override;

NodeStatus onCompleted();

std::shared_ptr<TestNodeConfig> _config;
EnumsTablePtr _script_enums;
ScriptFunction _return_status_executor;
ScriptFunction _success_executor;
ScriptFunction _failure_executor;
ScriptFunction _post_executor;
Expand Down
96 changes: 91 additions & 5 deletions src/actions/test_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,75 @@
namespace BT
{

namespace
{

bool isAllowedTestNodeStatus(NodeStatus status)
{
switch(status)
{
case NodeStatus::RUNNING:
case NodeStatus::SUCCESS:
case NodeStatus::FAILURE:
case NodeStatus::SKIPPED:
return true;

case NodeStatus::IDLE:
return false;
}
return false;
}

void validateTestNodeStatus(NodeStatus status, StringView source)
{
if(!isAllowedTestNodeStatus(status))
{
throw RuntimeError("TestNode ", std::string(source),
" resolved to invalid status (integer value: ",
std::to_string(static_cast<int>(status)), ")");
}
}

EnumsTablePtr createTestNodeEnums(const EnumsTablePtr& base_enums)
{
auto enums = base_enums ? std::make_shared<EnumsTable>(*base_enums) :
std::make_shared<EnumsTable>();

(*enums)["IDLE"] = static_cast<int>(NodeStatus::IDLE);
(*enums)["RUNNING"] = static_cast<int>(NodeStatus::RUNNING);
(*enums)["SUCCESS"] = static_cast<int>(NodeStatus::SUCCESS);
(*enums)["FAILURE"] = static_cast<int>(NodeStatus::FAILURE);
(*enums)["SKIPPED"] = static_cast<int>(NodeStatus::SKIPPED);
return enums;
}

NodeStatus convertScriptResultToStatus(const Any& result)
{
if(result.empty())
{
throw RuntimeError("return_status_script returned an empty value");
}

if(result.isString())
{
auto status = convertFromString<NodeStatus>(result.cast<std::string>());
validateTestNodeStatus(status, "return_status_script");
return status;
}

if(auto status = result.tryCast<int>())
{
auto resolved = static_cast<NodeStatus>(status.value());
validateTestNodeStatus(resolved, "return_status_script");
return resolved;
}

throw RuntimeError("return_status_script must evaluate to a NodeStatus-compatible "
"value");
}

} // namespace

TestNode::TestNode(const std::string& name, const NodeConfig& config,
TestNodeConfig test_config)
: TestNode(name, config, std::make_shared<TestNodeConfig>(std::move(test_config)))
Expand All @@ -14,11 +83,13 @@ TestNode::TestNode(const std::string& name, const NodeConfig& config,
{
setRegistrationID("TestNode");

if(_config->return_status == NodeStatus::IDLE)
if(_config->return_status_script.empty())
{
throw RuntimeError("TestNode can not return IDLE");
validateTestNodeStatus(_config->return_status, "return_status");
}

_script_enums = createTestNodeEnums(config.enums);

auto prepareScript = [](const std::string& script, auto& executor) {
if(!script.empty())
{
Expand All @@ -30,6 +101,7 @@ TestNode::TestNode(const std::string& name, const NodeConfig& config,
executor = result.value();
}
};
prepareScript(_config->return_status_script, _return_status_executor);
prepareScript(_config->success_script, _success_executor);
prepareScript(_config->failure_script, _failure_executor);
prepareScript(_config->post_script, _post_executor);
Expand Down Expand Up @@ -74,10 +146,24 @@ void TestNode::onHalted()

NodeStatus TestNode::onCompleted()
{
Ast::Environment env = { config().blackboard, config().enums };
Ast::Environment env = { config().blackboard, _script_enums };

NodeStatus status = NodeStatus::IDLE;

if(_config->complete_func)
{
status = _config->complete_func();
}
else if(_return_status_executor)
{
status = convertScriptResultToStatus(_return_status_executor(env));
}
else
{
status = _config->return_status;
}

auto status =
(_config->complete_func) ? _config->complete_func() : _config->return_status;
validateTestNodeStatus(status, "completion");

if(status == NodeStatus::SUCCESS && _success_executor)
{
Expand Down
20 changes: 18 additions & 2 deletions src/bt_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,19 @@ void BehaviorTreeFactory::loadSubstitutionRuleFromJSON(const std::string& json_t
for(auto const& [name, test_config] : test_configs.items())
{
auto& config = configs[name];
bool has_return_status = false;

auto return_status = test_config.at("return_status").get<std::string>();
config.return_status = convertFromString<NodeStatus>(return_status);
if(test_config.contains("return_status"))
{
auto return_status = test_config.at("return_status").get<std::string>();
config.return_status = convertFromString<NodeStatus>(return_status);
has_return_status = true;
}
if(test_config.contains("return_status_script"))
{
config.return_status_script =
test_config["return_status_script"].get<std::string>();
}
if(test_config.contains("async_delay"))
{
config.async_delay =
Expand All @@ -551,6 +561,12 @@ void BehaviorTreeFactory::loadSubstitutionRuleFromJSON(const std::string& json_t
{
config.failure_script = test_config["failure_script"].get<std::string>();
}

if(!has_return_status && config.return_status_script.empty())
{
throw RuntimeError("TestNodeConfig [", name,
"] must contain return_status or return_status_script");
}
}
}

Expand Down
Loading
Loading