Skip to content

Commit 284e62c

Browse files
pepeRossRoboticsclaude
authored andcommitted
Add scripted return status support for TestNode mocks
Introduce `return_status_script` on TestNodeConfig, allowing a mock's completion status to be computed at runtime from blackboard state instead of being fixed. When set, it takes precedence over `return_status`; it may reference the NodeStatus names (SUCCESS/FAILURE/...) and is rejected if it resolves to IDLE. The NodeStatus enum names are injected only into the return_status_script environment. The pre-existing success_script/failure_script/post_script continue to evaluate against the node's own enums, so a blackboard entry named like a status (e.g. "SUCCESS") is not shadowed. Also add JSON validation rejecting a TestNodeConfig with neither a return_status nor a return_status_script, and a tutorial example (t15_nodes_mocking_strict_failure) demonstrating the strict-failure flow. Addresses #995. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ce95461 commit 284e62c

7 files changed

Lines changed: 433 additions & 15 deletions

File tree

‎examples/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ CompileExample("t12_default_ports")
3535
CompileExample("t13_access_by_ref")
3636
CompileExample("t14_subtree_model")
3737
CompileExample("t15_nodes_mocking")
38+
CompileExample("t15_nodes_mocking_strict_failure")
3839
CompileExample("t16_global_blackboard")
3940
CompileExample("t17_blackboard_backup")
4041
CompileExample("t18_waypoints")

‎examples/t15_nodes_mocking.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const char* xml_text = R"(
3939

4040
/**
4141
* @brief In this example we will see how we can substitute some nodes
42-
* in the Tree above with
42+
* in the Tree above with mocks.
4343
* @param argc
4444
* @param argv
4545
* @return
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#include "dummy_nodes.h"
2+
3+
#include "behaviortree_cpp/bt_factory.h"
4+
5+
// clang-format off
6+
namespace
7+
{
8+
const char* xml_text = R"(
9+
<root BTCPP_format="4">
10+
11+
<BehaviorTree ID="MainTree">
12+
<Sequence>
13+
<SaySomething name="talk" message="hello world"/>
14+
15+
<SubTree ID="MySub" name="mysub"/>
16+
17+
<Script name="set_mock_flag" code="mock_should_fail:= false"/>
18+
<Script name="set_message" code="msg:= 'the original message' "/>
19+
<SaySomething message="{msg}"/>
20+
21+
<Sequence name="counting">
22+
<SaySomething message="1"/>
23+
<SaySomething message="2"/>
24+
<SaySomething message="3"/>
25+
</Sequence>
26+
</Sequence>
27+
</BehaviorTree>
28+
29+
<BehaviorTree ID="MySub">
30+
<Sequence>
31+
<AlwaysSuccess name="action_subA"/>
32+
<AlwaysSuccess name="action_subB"/>
33+
</Sequence>
34+
</BehaviorTree>
35+
36+
</root>
37+
)";
38+
} // namespace
39+
// clang-format on
40+
41+
/**
42+
* @brief Companion to tutorial 15 that preserves strict failure propagation.
43+
*
44+
* If the substituted TestNode resolves to FAILURE, the enclosing Sequence stops
45+
* immediately. The final blackboard message is still printed by this executable,
46+
* but the tree itself does not log the failure branch through a fallback.
47+
*/
48+
49+
int main(int /*argc*/, char** /*argv*/)
50+
{
51+
using namespace DummyNodes;
52+
BT::BehaviorTreeFactory factory;
53+
factory.registerNodeType<SaySomething>("SaySomething");
54+
factory.registerBehaviorTreeFromText(xml_text);
55+
56+
{
57+
auto tree = factory.createTree("MainTree");
58+
59+
std::cout << "----- Nodes fullPath() -------\n";
60+
tree.applyVisitor(
61+
[](BT::TreeNode* node) { std::cout << node->fullPath() << std::endl; });
62+
63+
std::cout << "\n------ Output (original) ------\n";
64+
auto status = tree.tickWhileRunning();
65+
std::cout << "Original status: " << BT::toStr(status, false) << std::endl;
66+
}
67+
68+
factory.registerSimpleAction("DummyAction", [](BT::TreeNode& self) {
69+
std::cout << "DummyAction substituting node with fullPath(): " << self.fullPath()
70+
<< std::endl;
71+
return BT::NodeStatus::SUCCESS;
72+
});
73+
74+
factory.registerSimpleAction("DummySaySomething", [](BT::TreeNode& self) {
75+
auto msg = self.getInput<std::string>("message");
76+
std::cout << "DummySaySomething: " << msg.value() << std::endl;
77+
return BT::NodeStatus::SUCCESS;
78+
});
79+
80+
BT::TestNodeConfig test_config;
81+
test_config.return_status_script = "(mock_should_fail == true) ? FAILURE : SUCCESS";
82+
test_config.async_delay = std::chrono::milliseconds(2000);
83+
test_config.success_script = "msg := 'message SUBSTITUTED' ";
84+
test_config.failure_script = "msg := 'message FAILURE branch' ";
85+
86+
BT::TestNodeConfig counting_config;
87+
counting_config.return_status = BT::NodeStatus::SUCCESS;
88+
89+
factory.addSubstitutionRule("mysub/action_*", "DummyAction");
90+
factory.addSubstitutionRule("talk", "DummySaySomething");
91+
factory.addSubstitutionRule("set_message", test_config);
92+
factory.addSubstitutionRule("counting", counting_config);
93+
94+
auto blackboard = BT::Blackboard::create();
95+
auto tree = factory.createTree("MainTree", blackboard);
96+
std::cout << "\n------ Output (substituted, strict failure) ------\n";
97+
auto status = tree.tickWhileRunning();
98+
std::cout << "Substituted status: " << BT::toStr(status, false) << std::endl;
99+
if(blackboard->getEntry("msg"))
100+
{
101+
std::cout << "Substituted final msg: " << blackboard->get<std::string>("msg")
102+
<< std::endl;
103+
}
104+
105+
return 0;
106+
}
107+
108+
/* Expected output:
109+
110+
----- Nodes fullPath() -------
111+
Sequence::1
112+
talk
113+
mysub
114+
mysub/Sequence::4
115+
mysub/action_subA
116+
mysub/action_subB
117+
set_mock_flag
118+
set_message
119+
SaySomething::9
120+
counting
121+
SaySomething::11
122+
SaySomething::12
123+
SaySomething::13
124+
125+
------ Output (original) ------
126+
Robot says: hello world
127+
Robot says: the original message
128+
Robot says: 1
129+
Robot says: 2
130+
Robot says: 3
131+
Original status: SUCCESS
132+
133+
------ Output (substituted, strict failure) ------
134+
DummySaySomething: hello world
135+
DummyAction substituting node with fullPath(): mysub/action_subA
136+
DummyAction substituting node with fullPath(): mysub/action_subB
137+
Substituted status: SUCCESS
138+
Substituted final msg: message SUBSTITUTED
139+
140+
If you change mock_should_fail to true in the XML above, the substituted status
141+
becomes FAILURE and the final msg becomes "message FAILURE branch".
142+
143+
*/

‎include/behaviortree_cpp/actions/test_node.h‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,27 @@
1717
#include "behaviortree_cpp/scripting/script_parser.hpp"
1818
#include "behaviortree_cpp/utils/timer_queue.h"
1919

20+
#include <memory>
21+
#include <string>
22+
2023
namespace BT
2124
{
2225

2326
struct TestNodeConfig
2427
{
25-
/// status to return when the action is completed.
28+
/// Status to return when the action is completed.
29+
/// If return_status_script is also set, the script takes precedence.
2630
NodeStatus return_status = NodeStatus::SUCCESS;
2731

32+
/// Optional script to compute the completion status dynamically.
33+
///
34+
/// This script is evaluated when the TestNode completes, after any
35+
/// async_delay has elapsed, using the current blackboard state.
36+
/// The result must resolve to the same set of statuses supported by
37+
/// return_status, except IDLE which is always rejected.
38+
/// When set, this takes precedence over return_status.
39+
std::string return_status_script;
40+
2841
/// script to execute when complete_func() returns SUCCESS
2942
std::string success_script;
3043

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

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

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

8196
protected:
82-
virtual NodeStatus onStart() override;
97+
NodeStatus onStart() override;
8398

84-
virtual NodeStatus onRunning() override;
99+
NodeStatus onRunning() override;
85100

86-
virtual void onHalted() override;
101+
void onHalted() override;
87102

88103
NodeStatus onCompleted();
89104

90105
std::shared_ptr<TestNodeConfig> _config;
106+
ScriptFunction _return_status_executor;
91107
ScriptFunction _success_executor;
92108
ScriptFunction _failure_executor;
93109
ScriptFunction _post_executor;

‎src/actions/test_node.cpp‎

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,75 @@
33
namespace BT
44
{
55

6+
namespace
7+
{
8+
9+
bool isAllowedTestNodeStatus(NodeStatus status)
10+
{
11+
switch(status)
12+
{
13+
case NodeStatus::RUNNING:
14+
case NodeStatus::SUCCESS:
15+
case NodeStatus::FAILURE:
16+
case NodeStatus::SKIPPED:
17+
return true;
18+
19+
case NodeStatus::IDLE:
20+
return false;
21+
}
22+
return false;
23+
}
24+
25+
void validateTestNodeStatus(NodeStatus status, StringView source)
26+
{
27+
if(!isAllowedTestNodeStatus(status))
28+
{
29+
throw RuntimeError("TestNode ", std::string(source),
30+
" resolved to invalid status (integer value: ",
31+
std::to_string(static_cast<int>(status)), ")");
32+
}
33+
}
34+
35+
EnumsTablePtr createTestNodeEnums(const EnumsTablePtr& base_enums)
36+
{
37+
auto enums = base_enums ? std::make_shared<EnumsTable>(*base_enums) :
38+
std::make_shared<EnumsTable>();
39+
40+
(*enums)["IDLE"] = static_cast<int>(NodeStatus::IDLE);
41+
(*enums)["RUNNING"] = static_cast<int>(NodeStatus::RUNNING);
42+
(*enums)["SUCCESS"] = static_cast<int>(NodeStatus::SUCCESS);
43+
(*enums)["FAILURE"] = static_cast<int>(NodeStatus::FAILURE);
44+
(*enums)["SKIPPED"] = static_cast<int>(NodeStatus::SKIPPED);
45+
return enums;
46+
}
47+
48+
NodeStatus convertScriptResultToStatus(const Any& result)
49+
{
50+
if(result.empty())
51+
{
52+
throw RuntimeError("return_status_script returned an empty value");
53+
}
54+
55+
if(result.isString())
56+
{
57+
auto status = convertFromString<NodeStatus>(result.cast<std::string>());
58+
validateTestNodeStatus(status, "return_status_script");
59+
return status;
60+
}
61+
62+
if(auto status = result.tryCast<int>())
63+
{
64+
auto resolved = static_cast<NodeStatus>(status.value());
65+
validateTestNodeStatus(resolved, "return_status_script");
66+
return resolved;
67+
}
68+
69+
throw RuntimeError("return_status_script must evaluate to a NodeStatus-compatible "
70+
"value");
71+
}
72+
73+
} // namespace
74+
675
TestNode::TestNode(const std::string& name, const NodeConfig& config,
776
TestNodeConfig test_config)
877
: TestNode(name, config, std::make_shared<TestNodeConfig>(std::move(test_config)))
@@ -14,9 +83,9 @@ TestNode::TestNode(const std::string& name, const NodeConfig& config,
1483
{
1584
setRegistrationID("TestNode");
1685

17-
if(_config->return_status == NodeStatus::IDLE)
86+
if(_config->return_status_script.empty())
1887
{
19-
throw RuntimeError("TestNode can not return IDLE");
88+
validateTestNodeStatus(_config->return_status, "return_status");
2089
}
2190

2291
auto prepareScript = [](const std::string& script, auto& executor) {
@@ -30,6 +99,7 @@ TestNode::TestNode(const std::string& name, const NodeConfig& config,
3099
executor = result.value();
31100
}
32101
};
102+
prepareScript(_config->return_status_script, _return_status_executor);
33103
prepareScript(_config->success_script, _success_executor);
34104
prepareScript(_config->failure_script, _failure_executor);
35105
prepareScript(_config->post_script, _post_executor);
@@ -74,10 +144,31 @@ void TestNode::onHalted()
74144

75145
NodeStatus TestNode::onCompleted()
76146
{
77-
Ast::Environment env = { config().blackboard, config().enums };
147+
NodeStatus status = NodeStatus::IDLE;
148+
149+
if(_config->complete_func)
150+
{
151+
status = _config->complete_func();
152+
}
153+
else if(_return_status_executor)
154+
{
155+
// return_status_script may reference NodeStatus names (e.g. SUCCESS, FAILURE),
156+
// so it is evaluated with an enum table augmented with those values. The
157+
// post-execution scripts below deliberately do NOT see these names.
158+
auto status_enums = createTestNodeEnums(config().enums);
159+
Ast::Environment status_env = { config().blackboard, status_enums };
160+
status = convertScriptResultToStatus(_return_status_executor(status_env));
161+
}
162+
else
163+
{
164+
status = _config->return_status;
165+
}
78166

79-
auto status =
80-
(_config->complete_func) ? _config->complete_func() : _config->return_status;
167+
validateTestNodeStatus(status, "completion");
168+
169+
// success/failure/post scripts use only the node's own enums, so that a
170+
// blackboard entry named like a status (e.g. "SUCCESS") is not shadowed.
171+
Ast::Environment env = { config().blackboard, config().enums };
81172

82173
if(status == NodeStatus::SUCCESS && _success_executor)
83174
{

‎src/bt_factory.cpp‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,19 @@ void BehaviorTreeFactory::loadSubstitutionRuleFromJSON(const std::string& json_t
531531
for(auto const& [name, test_config] : test_configs.items())
532532
{
533533
auto& config = configs[name];
534+
bool has_return_status = false;
534535

535-
auto return_status = test_config.at("return_status").get<std::string>();
536-
config.return_status = convertFromString<NodeStatus>(return_status);
536+
if(test_config.contains("return_status"))
537+
{
538+
auto return_status = test_config.at("return_status").get<std::string>();
539+
config.return_status = convertFromString<NodeStatus>(return_status);
540+
has_return_status = true;
541+
}
542+
if(test_config.contains("return_status_script"))
543+
{
544+
config.return_status_script =
545+
test_config["return_status_script"].get<std::string>();
546+
}
537547
if(test_config.contains("async_delay"))
538548
{
539549
config.async_delay =
@@ -551,6 +561,12 @@ void BehaviorTreeFactory::loadSubstitutionRuleFromJSON(const std::string& json_t
551561
{
552562
config.failure_script = test_config["failure_script"].get<std::string>();
553563
}
564+
565+
if(!has_return_status && config.return_status_script.empty())
566+
{
567+
throw RuntimeError("TestNodeConfig [", name,
568+
"] must contain return_status or return_status_script");
569+
}
554570
}
555571
}
556572

0 commit comments

Comments
 (0)