Skip to content

Commit 3e68a16

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. To pay the ABI break only once, TestNode's state is moved behind a PImpl, so future changes to it no longer alter the layout consumers link against (mirroring StatusChangeLogger). The new return_status_script field is appended at the end of TestNodeConfig, preserving the byte offsets of the existing fields. 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 3e68a16

7 files changed

Lines changed: 484 additions & 40 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: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
#pragma once
1515

1616
#include "behaviortree_cpp/action_node.h"
17-
#include "behaviortree_cpp/scripting/script_parser.hpp"
18-
#include "behaviortree_cpp/utils/timer_queue.h"
17+
18+
#include <chrono>
19+
#include <functional>
20+
#include <memory>
21+
#include <string>
1922

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

2832
/// script to execute when complete_func() returns SUCCESS
@@ -38,14 +42,27 @@ struct TestNodeConfig
3842
std::chrono::milliseconds async_delay = std::chrono::milliseconds(0);
3943

4044
/// Function invoked when the action is completed.
41-
/// If not specified, the node will return [return_status]
45+
/// If not specified, the node will use return_status_script when present,
46+
/// otherwise it will return [return_status].
4247
std::function<NodeStatus(void)> complete_func;
48+
49+
/// Optional script to compute the completion status dynamically.
50+
///
51+
/// This script is evaluated when the TestNode completes, after any
52+
/// async_delay has elapsed, using the current blackboard state.
53+
/// The result must resolve to the same set of statuses supported by
54+
/// return_status, except IDLE which is always rejected.
55+
/// When set, this takes precedence over return_status.
56+
///
57+
/// NOTE: kept last to preserve the byte offsets of the fields above.
58+
std::string return_status_script;
4359
};
4460

4561
/**
4662
* @brief The TestNode is a Node that can be configure to:
4763
*
4864
* 1. Return a specific status (SUCCESS / FAILURE)
65+
* 1.b Compute the returned status from a script evaluated at completion time
4966
* 2. Execute a post condition script (unless halted)
5067
* 3. Either complete immediately (synchronous action), or after a
5168
* given period of time (asynchronous action)
@@ -73,26 +90,32 @@ class TestNode : public BT::StatefulActionNode
7390
TestNode(const std::string& name, const NodeConfig& config,
7491
std::shared_ptr<TestNodeConfig> test_config);
7592

93+
~TestNode() override;
94+
95+
TestNode(const TestNode& other) = delete;
96+
TestNode& operator=(const TestNode& other) = delete;
97+
98+
TestNode(TestNode&& other) = delete;
99+
TestNode& operator=(TestNode&& other) = delete;
100+
76101
static PortsList providedPorts()
77102
{
78103
return {};
79104
}
80105

81106
protected:
82-
virtual NodeStatus onStart() override;
107+
NodeStatus onStart() override;
83108

84-
virtual NodeStatus onRunning() override;
109+
NodeStatus onRunning() override;
85110

86-
virtual void onHalted() override;
111+
void onHalted() override;
87112

88113
NodeStatus onCompleted();
89114

90-
std::shared_ptr<TestNodeConfig> _config;
91-
ScriptFunction _success_executor;
92-
ScriptFunction _failure_executor;
93-
ScriptFunction _post_executor;
94-
TimerQueue<> _timer;
95-
std::atomic_bool _completed = false;
115+
// All state lives behind this pointer, so that adding or changing it does not
116+
// alter the layout that consumers compile against.
117+
struct PImpl;
118+
std::unique_ptr<PImpl> _p;
96119
};
97120

98121
} // namespace BT

0 commit comments

Comments
 (0)