use crate::arora_generated::behavior_tree::status::Status;
use crate::load_behavior_tree_yaml;
use crate::nodes;
use crate::tree_node::TreeNode;
use crate::{run_behavior_tree, BehaviorTree, BehaviorTreeRuntime, ModuleFunction};
use anyhow::Result;
use arora_types::call::{Call, CallBridge, CallError, CallResult, Callable, CallableId};
use arora_types::record::module::frozen::Function;
use arora_types::record::ty::{FrozenTy, PrimitiveKind};
use arora_types::value::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use uuid::Uuid;
#[test]
pub fn load_parse_error() -> Result<()> {
let tree_yaml = "I'm singing in the rain...";
assert!(load_behavior_tree_yaml(tree_yaml).is_err());
Ok(())
}
#[test]
pub fn load_simple_tree() -> Result<()> {
let tree_yaml = &crate::schema::tests::SIMPLE_TREE_YAML;
load_behavior_tree_yaml(tree_yaml)?;
Ok(())
}
#[test]
fn shared_variable_id_resolves_to_one_cell() {
use crate::schema::{Expression, NodeParameterId};
use crate::variable::VariableCell;
let var_id = Uuid::new_v4();
let mut variables: HashMap<Uuid, VariableCell> = HashMap::new();
let mut node_parameters: HashMap<NodeParameterId, VariableCell> = HashMap::new();
let param_a = NodeParameterId {
node: Uuid::new_v4(),
parameter: Uuid::new_v4(),
};
let param_b = NodeParameterId {
node: Uuid::new_v4(),
parameter: Uuid::new_v4(),
};
let cell_a = crate::setup_node_parameter_variable(
¶m_a,
&Expression::VariableId(var_id),
&mut variables,
&mut node_parameters,
&|_| None,
&HashMap::new(),
)
.unwrap();
let cell_b = crate::setup_node_parameter_variable(
¶m_b,
&Expression::VariableId(var_id),
&mut variables,
&mut node_parameters,
&|_| None,
&HashMap::new(),
)
.unwrap();
cell_a.set(Value::Boolean(true));
assert_eq!(
cell_b.get_or_unit(),
Value::Boolean(true),
"two parameters bound to the same variable id must share one cell"
);
}
#[test]
fn resolved_variable_is_store_backed() {
use crate::schema::{Expression, NodeParameterId};
use crate::variable::VariableCell;
use arora_simple_data_store::SimpleDataStore;
use arora_types::data::{DataStore, Key, StateChange};
let store = SimpleDataStore::new();
let resolver = {
let store = store.clone();
move |name: &str| Some(store.slot(&Key::from(name)))
};
let var_id = Uuid::new_v4();
let names = HashMap::from([(var_id, "battery.level".to_string())]);
let mut variables: HashMap<Uuid, VariableCell> = HashMap::new();
let mut node_parameters: HashMap<NodeParameterId, VariableCell> = HashMap::new();
let param = NodeParameterId {
node: Uuid::new_v4(),
parameter: Uuid::new_v4(),
};
let cell = crate::setup_node_parameter_variable(
¶m,
&Expression::VariableId(var_id),
&mut variables,
&mut node_parameters,
&resolver,
&names,
)
.unwrap();
cell.set(Value::from(1.0_f64));
assert_eq!(
store.read(&[Key::from("battery.level")]),
vec![Some(Value::from(1.0_f64))],
"a write through the resolved cell must reach the store key"
);
store
.write(StateChange::set("battery.level", Value::from(2.0_f64)))
.unwrap();
assert_eq!(
cell.get(),
Some(Value::from(2.0_f64)),
"a write through the store key must be visible through the resolved cell"
);
}
type LeafStatuses = Rc<RefCell<HashMap<Uuid, Status>>>;
type LeafTicks = Rc<RefCell<HashMap<Uuid, u32>>>;
struct TestBridge {
registered: HashMap<u64, Rc<dyn Callable>>,
next_id: u64,
leaf_statuses: LeafStatuses,
leaf_ticks: LeafTicks,
}
impl TestBridge {
fn new(leaf_statuses: LeafStatuses, leaf_ticks: LeafTicks) -> Self {
Self {
registered: HashMap::new(),
next_id: 0,
leaf_statuses,
leaf_ticks,
}
}
fn empty() -> Self {
Self::new(
Rc::new(RefCell::new(HashMap::new())),
Rc::new(RefCell::new(HashMap::new())),
)
}
}
impl CallBridge for TestBridge {
fn arora_call(&mut self, call: Call) -> Result<CallResult, CallError> {
*self.leaf_ticks.borrow_mut().entry(call.id).or_insert(0) += 1;
let status = self
.leaf_statuses
.borrow()
.get(&call.id)
.cloned()
.ok_or(CallError::FunctionNotFound { id: call.id })?;
Ok(CallResult {
ret: status.into(),
mutated: Vec::new(),
})
}
fn arora_register_callable(&mut self, callable: Rc<dyn Callable>) -> CallableId {
let id = self.next_id;
self.next_id += 1;
self.registered.insert(id, callable);
CallableId { id }
}
fn arora_unregister_callable(&mut self, callable_id: &CallableId) {
self.registered.remove(&callable_id.id);
}
fn arora_call_indirect(&mut self, callable_id: &CallableId) -> Result<Value, CallError> {
let callable = self
.registered
.get(&callable_id.id)
.cloned()
.ok_or(CallError::Generic {
message: format!("unknown callable {}", callable_id.id),
})?;
callable.call(self)
}
}
fn scripted_leaf(function: Uuid) -> TreeNode {
TreeNode {
function,
children: None,
parameters: HashMap::new(),
}
}
fn scripted_leaf_function(function: Uuid) -> ModuleFunction {
ModuleFunction {
module_id: Uuid::nil(),
function_id: function,
function_name: "scripted_leaf".to_string(),
function: Function {
parameters: HashMap::new(),
parameter_ordering: Vec::new(),
return_ty: FrozenTy::from(PrimitiveKind::U8),
},
}
}
pub(crate) fn build(node: TreeNode) -> BehaviorTree {
node.try_into().expect("tree builds")
}
fn tick_once(tree: &BehaviorTree) -> Status {
let mut bridge = TestBridge::empty();
let mut runtime = BehaviorTreeRuntime::setup(tree, Rc::new(HashMap::new()), &mut bridge, false)
.expect("runtime sets up");
runtime.tick().expect("tick succeeds")
}
fn run(tree: &BehaviorTree) -> Status {
let mut bridge = TestBridge::empty();
run_behavior_tree(tree, Rc::new(HashMap::new()), &mut bridge, false).expect("run succeeds")
}
#[test]
fn seq_all_success_is_success() {
let tree = build(nodes::seq(vec![nodes::succeed(), nodes::succeed()]));
assert_eq!(run(&tree), Status::Success);
}
#[test]
fn seq_first_failure_is_failure() {
let later = Uuid::from_u128(0xA1);
let tree = build(nodes::seq(vec![nodes::fail(), scripted_leaf(later)]));
assert_eq!(tick_once(&tree), Status::Failure);
}
#[test]
fn seq_running_child_is_running() {
let tree = build(nodes::seq(vec![nodes::succeed(), nodes::run()]));
assert_eq!(tick_once(&tree), Status::Running);
}
#[test]
fn fallback_first_success_is_success() {
let tree = build(nodes::fallback(vec![nodes::succeed(), nodes::fail()]));
assert_eq!(run(&tree), Status::Success);
}
#[test]
fn fallback_all_failure_is_failure() {
let tree = build(nodes::fallback(vec![nodes::fail(), nodes::fail()]));
assert_eq!(run(&tree), Status::Failure);
}
#[test]
fn fallback_empty_is_success() {
let tree = build(nodes::fallback(vec![]));
assert_eq!(run(&tree), Status::Success);
}
#[test]
fn fallback_running_child_is_running() {
let tree = build(nodes::fallback(vec![nodes::run(), nodes::succeed()]));
assert_eq!(tick_once(&tree), Status::Running);
}
#[test]
fn parallel_all_success_is_success() {
let tree = build(nodes::parallel(vec![nodes::succeed(), nodes::succeed()]));
assert_eq!(run(&tree), Status::Success);
}
#[test]
fn parallel_any_failure_is_failure() {
let tree = build(nodes::parallel(vec![
nodes::succeed(),
nodes::fail(),
nodes::succeed(),
]));
assert_eq!(run(&tree), Status::Failure);
}
#[test]
fn parallel_mixed_running_is_running() {
let tree = build(nodes::parallel(vec![nodes::succeed(), nodes::run()]));
assert_eq!(tick_once(&tree), Status::Running);
}
#[test]
fn concurrent_runs_advance_independently() {
let a = Uuid::from_u128(0xA);
let b = Uuid::from_u128(0xB);
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([
(a, Status::Running),
(b, Status::Running),
])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
let function_index = Rc::new(HashMap::from([
(a, scripted_leaf_function(a)),
(b, scripted_leaf_function(b)),
]));
let tree = build(nodes::parallel(vec![scripted_leaf(a), scripted_leaf(b)]));
let mut bridge = TestBridge::new(statuses.clone(), ticks.clone());
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Running);
let _ = runtime;
statuses.borrow_mut().insert(a, Status::Success);
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Running);
let _ = runtime;
statuses.borrow_mut().insert(b, Status::Success);
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Success);
}
#[test]
fn seq_star_resumes_and_resets() {
let first = Uuid::from_u128(0x1);
let second = Uuid::from_u128(0x2);
let third = Uuid::from_u128(0x3);
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([
(first, Status::Success),
(second, Status::Running),
(third, Status::Success),
])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
let function_index = Rc::new(HashMap::from([
(first, scripted_leaf_function(first)),
(second, scripted_leaf_function(second)),
(third, scripted_leaf_function(third)),
]));
let tree = build(nodes::seq_star(vec![
scripted_leaf(first),
scripted_leaf(second),
scripted_leaf(third),
]));
let mut bridge = TestBridge::new(statuses.clone(), ticks.clone());
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Running);
let _ = runtime;
assert_eq!(*ticks.borrow().get(&first).unwrap_or(&0), 1);
assert_eq!(*ticks.borrow().get(&second).unwrap_or(&0), 1);
assert_eq!(*ticks.borrow().get(&third).unwrap_or(&0), 0);
statuses.borrow_mut().insert(second, Status::Success);
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Success);
let _ = runtime;
assert_eq!(
*ticks.borrow().get(&first).unwrap_or(&0),
1,
"first not re-ticked"
);
assert_eq!(*ticks.borrow().get(&second).unwrap_or(&0), 2);
assert_eq!(*ticks.borrow().get(&third).unwrap_or(&0), 1);
let mut runtime = BehaviorTreeRuntime::setup(&tree, function_index.clone(), &mut bridge, false)
.expect("setup");
assert_eq!(runtime.tick().expect("tick"), Status::Success);
let _ = runtime;
assert_eq!(
*ticks.borrow().get(&first).unwrap_or(&0),
2,
"first re-ticked after reset"
);
}
#[test]
fn a_fresh_interpreter_accepts_edits_and_idles_while_empty() {
use crate::behavior::BehaviorTreeInterpreter;
use arora_behavior::graph::GraphDiff;
use arora_behavior::{BehaviorContext, BehaviorInterpreter, BehaviorStatus};
use arora_simple_data_store::SimpleDataStore;
let mut interpreter = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
assert!(interpreter.graph().root.is_some(), "the runner is the root");
assert_eq!(interpreter.graph().nodes.len(), 1, "just the runner");
interpreter
.apply(GraphDiff::default())
.expect("a fresh interpreter accepts a diff");
let store = SimpleDataStore::new();
let (statuses, ticks) = (LeafStatuses::default(), LeafTicks::default());
let mut bridge = TestBridge::new(statuses, ticks);
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
let status = interpreter.tick(&mut ctx).expect("the bare scaffold idles");
assert_eq!(status, BehaviorStatus::Running);
}
mod task_runs {
use super::{LeafStatuses, LeafTicks, TestBridge};
use crate::arora_generated::behavior_tree::status::Status;
use crate::behavior::{graph_type, BehaviorTreeInterpreter};
use crate::nodes;
use arora_behavior::{BehaviorContext, BehaviorInterpreter, BehaviorStatus, RunPolicy, TaskId};
use arora_simple_data_store::SimpleDataStore;
use arora_types::call::Call;
use arora_types::data::DataStore;
use arora_types::value::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use uuid::Uuid;
fn scripted(leaf: Uuid, status: Status) -> (TestBridge, LeafStatuses, LeafTicks) {
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([(leaf, status)])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
(
TestBridge::new(statuses.clone(), ticks.clone()),
statuses,
ticks,
)
}
fn call_to(module: u128, leaf: Uuid) -> Call {
Call {
module_id: Some(Uuid::from_u128(module)),
id: leaf,
args: Vec::new(),
}
}
#[test]
fn a_spawned_run_advances_and_writes_its_status_key() {
let leaf = Uuid::from_u128(0xA1);
let (mut bridge, statuses, ticks) = scripted(leaf, Status::Running);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let handle = interp
.spawn(call_to(0xB0, leaf), RunPolicy::Concurrent)
.expect("the tree interpreter hosts runs");
assert!(
handle.status.path.starts_with("arora/tasks/"),
"{}",
handle.status.path
);
assert!(
handle.status.path.ends_with("/status"),
"{}",
handle.status.path
);
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
assert_eq!(store.read(std::slice::from_ref(&handle.status)), vec![None]);
assert_eq!(interp.tick(&mut ctx).unwrap(), BehaviorStatus::Running);
let running: Value = Status::Running.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(running)],
"the run's status key holds Running"
);
assert_eq!(*ticks.borrow().get(&leaf).unwrap(), 1);
statuses.borrow_mut().insert(leaf, Status::Success);
assert_eq!(interp.tick(&mut ctx).unwrap(), BehaviorStatus::Running);
let success: Value = Status::Success.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(success)]
);
let before = *ticks.borrow().get(&leaf).unwrap();
interp.tick(&mut ctx).unwrap();
assert_eq!(
*ticks.borrow().get(&leaf).unwrap(),
before,
"a finished run is not re-ticked"
);
}
#[test]
fn halting_a_run_terminates_it_next_tick() {
let leaf = Uuid::from_u128(0xC2);
let (mut bridge, _statuses, ticks) = scripted(leaf, Status::Running);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let handle = interp
.spawn(call_to(0xB0, leaf), RunPolicy::Concurrent)
.unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
assert_eq!(interp.tick(&mut ctx).unwrap(), BehaviorStatus::Running);
let running: Value = Status::Running.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(running)]
);
interp.halt(handle.id).unwrap();
let ticks_before = *ticks.borrow().get(&leaf).unwrap();
assert_eq!(interp.tick(&mut ctx).unwrap(), BehaviorStatus::Running);
let failure: Value = Status::Failure.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(failure)],
"a halted run ends Failure"
);
assert_eq!(
*ticks.borrow().get(&leaf).unwrap(),
ticks_before,
"a halted run is not invoked"
);
interp
.halt(handle.id)
.expect("halting a finished run is a no-op");
interp
.halt(TaskId(Uuid::from_u128(0xDEAD)))
.expect("halting an unknown run is a no-op");
}
#[test]
fn a_loaded_behavior_and_a_run_tick_together() {
use arora_behavior::graph::{Graph, Node as GraphNode};
let main_leaf = Uuid::from_u128(0xE3);
let run_leaf = Uuid::from_u128(0xE4);
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([
(main_leaf, Status::Success),
(run_leaf, Status::Running),
])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
let mut bridge = TestBridge::new(statuses.clone(), ticks.clone());
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let main_node = Uuid::from_u128(0x111);
let mut main = Graph::empty();
main.root = Some(main_node);
main.nodes.insert(
main_node,
GraphNode {
id: main_node,
function: crate::nodes::RUN_CALL_FUNCTION_ID,
inputs: vec![arora_behavior::graph::Io::new(
crate::nodes::RUN_CALL_PARAM_ID,
)],
..GraphNode::default()
},
);
main.links.push(arora_behavior::graph::Link::new(
arora_behavior::graph::Port::new(main_node, crate::nodes::RUN_CALL_PARAM_ID),
arora_behavior::graph::LinkSource::Literal(
arora_types::value_serde::to_value(&call_to(0xB0, main_leaf)).unwrap(),
),
));
interp.load(main).expect("the behavior loads");
interp
.spawn(call_to(0xB0, run_leaf), RunPolicy::Concurrent)
.unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
for round in 1..=3u32 {
assert_eq!(
interp.tick(&mut ctx).unwrap(),
BehaviorStatus::Running,
"the interpreter is a standing policy (never Done)"
);
assert_eq!(
*ticks.borrow().get(&main_leaf).unwrap(),
round,
"the main behavior re-evaluates every tick"
);
assert_eq!(
*ticks.borrow().get(&run_leaf).unwrap(),
round,
"the run advances every tick"
);
}
}
#[test]
fn a_spawned_run_is_visible_in_the_graph_until_halted() {
let leaf = Uuid::from_u128(0xE5);
let (mut bridge, _statuses, _ticks) = scripted(leaf, Status::Running);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let handle = interp
.spawn(call_to(0xB0, leaf), RunPolicy::Concurrent)
.unwrap();
let graph = interp.graph();
let decorator = graph
.nodes
.values()
.find(|n| n.function == crate::nodes::RUN_STATUS_FUNCTION_ID)
.expect("the run's status decorator is in the graph");
assert_eq!(
decorator.outputs[0].predetermined_key.as_deref(),
Some(handle.status.path.as_str()),
"the decorator's status output is predetermined to the run's key"
);
let call_node = decorator.children.as_ref().unwrap()[0];
assert_eq!(
graph.nodes[&call_node].function,
crate::nodes::RUN_CALL_FUNCTION_ID,
"the decorator wraps the run-call leaf"
);
let root = graph.root.expect("the runner roots the scaffold");
assert!(
graph.nodes[&root]
.children
.as_ref()
.unwrap()
.contains(&decorator.id),
"the fragment hangs off the runner"
);
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
interp.halt(handle.id).unwrap();
interp.tick(&mut ctx).unwrap();
assert_eq!(
interp.graph().nodes.len(),
1,
"only the runner remains after the halt pruned the fragment"
);
}
#[test]
fn concurrent_runs_get_independent_status_keys() {
let a = Uuid::from_u128(0xAA);
let b = Uuid::from_u128(0xBB);
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([
(a, Status::Running),
(b, Status::Running),
])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
let mut bridge = TestBridge::new(statuses.clone(), ticks.clone());
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let ha = interp
.spawn(call_to(0xB0, a), RunPolicy::Concurrent)
.unwrap();
let hb = interp
.spawn(call_to(0xB0, b), RunPolicy::Concurrent)
.unwrap();
assert_ne!(
ha.status.path, hb.status.path,
"each run gets its own status key"
);
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
let running: Value = Status::Running.into();
assert_eq!(
store.read(std::slice::from_ref(&ha.status)),
vec![Some(running.clone())]
);
assert_eq!(
store.read(std::slice::from_ref(&hb.status)),
vec![Some(running.clone())]
);
interp.halt(ha.id).unwrap();
interp.tick(&mut ctx).unwrap();
let failure: Value = Status::Failure.into();
assert_eq!(
store.read(std::slice::from_ref(&ha.status)),
vec![Some(failure)],
"the halted run ended"
);
assert_eq!(
store.read(std::slice::from_ref(&hb.status)),
vec![Some(running)],
"the other run is unaffected"
);
}
fn stepping_graph(ids: [u128; 3], leaf: Uuid) -> arora_behavior::graph::Graph {
use arora_behavior::graph::{Graph, Io, Link, LinkSource, Node as GraphNode, Port};
let [parallel, call_node, run] = ids.map(Uuid::from_u128);
let mut graph = Graph::empty();
graph.root = Some(parallel);
graph.nodes.insert(
parallel,
GraphNode {
id: parallel,
function: nodes::PARALLEL_FUNCTION_ID,
children: Some(vec![call_node, run]),
..GraphNode::default()
},
);
graph.nodes.insert(
call_node,
GraphNode {
id: call_node,
function: nodes::RUN_CALL_FUNCTION_ID,
inputs: vec![Io::new(nodes::RUN_CALL_PARAM_ID)],
..GraphNode::default()
},
);
graph.nodes.insert(
run,
GraphNode {
id: run,
function: nodes::RUN_FUNCTION_ID,
..GraphNode::default()
},
);
graph.links.push(Link::new(
Port::new(call_node, nodes::RUN_CALL_PARAM_ID),
LinkSource::Literal(arora_types::value_serde::to_value(&call_to(0xB0, leaf)).unwrap()),
));
graph
}
#[test]
fn a_spawned_graph_may_call_a_core_string_node() {
use arora_behavior::graph::{Graph, Io, Link, LinkSource, Node as GraphNode, Port};
let node = Uuid::from_u128(0x30);
let mut graph = Graph::empty();
graph.root = Some(node);
graph.nodes.insert(
node,
GraphNode {
id: node,
function: nodes::IS_STR_SET_FUNCTION_ID,
inputs: vec![Io::new(nodes::IS_STR_SET_VALUE_PARAM_ID)],
..GraphNode::default()
},
);
graph.links.push(Link::new(
Port::new(node, nodes::IS_STR_SET_VALUE_PARAM_ID),
LinkSource::Literal(Value::String("set".to_string())),
));
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let handle = interp
.spawn_graph(&graph_type(), graph, RunPolicy::Concurrent)
.expect("a core node needs no module");
let store = SimpleDataStore::new();
let mut bridge = TestBridge::new(LeafStatuses::default(), LeafTicks::default());
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
let success: Value = Status::Success.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(success)],
"the run ticked the core node"
);
}
#[test]
fn a_spawned_behavior_ticks_every_step_until_halted() {
let leaf = Uuid::from_u128(0xF1);
let (mut bridge, _statuses, ticks) = scripted(leaf, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let handle = interp
.spawn_graph(
&graph_type(),
stepping_graph([0x10, 0x11, 0x12], leaf),
RunPolicy::Concurrent,
)
.expect("the tree interpreter hosts a graph as a run");
assert_eq!(
handle.status.path,
format!(
"arora/tasks/{}/{}/{}/status",
arora_behavior::interpreter_module::ID,
arora_behavior::interpreter_module::SPAWN_GRAPH,
handle.id.0
),
"the run's keys are under the interpreter module's spawn_graph"
);
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
let running: Value = Status::Running.into();
for round in 1..=3u32 {
assert_eq!(interp.tick(&mut ctx).unwrap(), BehaviorStatus::Running);
assert_eq!(
*ticks.borrow().get(&leaf).unwrap(),
round,
"the run's leaf ticks once per step"
);
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(running.clone())]
);
}
interp.halt(handle.id).unwrap();
interp.tick(&mut ctx).unwrap();
let failure: Value = Status::Failure.into();
assert_eq!(
store.read(std::slice::from_ref(&handle.status)),
vec![Some(failure)],
"a halted run ends Failure"
);
assert_eq!(
*ticks.borrow().get(&leaf).unwrap(),
3,
"a halted run is not ticked"
);
assert_eq!(
interp.graph().nodes.len(),
1,
"only the runner remains after the halt pruned the run's nodes"
);
}
#[test]
fn an_edit_reaches_a_spawned_behavior_and_a_load_keeps_it() {
use arora_behavior::graph::{Graph, GraphDiff, Link, LinkSource, Node as GraphNode, Port};
let a = Uuid::from_u128(0xA1);
let b = Uuid::from_u128(0xB1);
let statuses: LeafStatuses = Rc::new(RefCell::new(HashMap::from([
(a, Status::Success),
(b, Status::Success),
])));
let ticks: LeafTicks = Rc::new(RefCell::new(HashMap::new()));
let mut bridge = TestBridge::new(statuses, ticks.clone());
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let [parallel, call_node, run] = [0x20, 0x21, 0x22].map(Uuid::from_u128);
let handle = interp
.spawn_graph(
&graph_type(),
stepping_graph([0x20, 0x21, 0x22], a),
RunPolicy::Concurrent,
)
.unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
assert_eq!(ticks.borrow().get(&a), Some(&1));
interp
.apply(GraphDiff {
add_links: vec![Link::new(
Port::new(call_node, nodes::RUN_CALL_PARAM_ID),
LinkSource::Literal(
arora_types::value_serde::to_value(&call_to(0xB0, b)).unwrap(),
),
)],
..GraphDiff::default()
})
.expect("the edit applies");
interp.tick(&mut ctx).unwrap();
assert_eq!(ticks.borrow().get(&a), Some(&1), "the old call is gone");
assert_eq!(ticks.borrow().get(&b), Some(&1), "the relinked call runs");
let added = Uuid::from_u128(0x23);
interp
.apply(GraphDiff {
add_nodes: vec![
GraphNode {
id: parallel,
function: nodes::PARALLEL_FUNCTION_ID,
children: Some(vec![call_node, run, added]),
..GraphNode::default()
},
GraphNode {
id: added,
function: nodes::RUN_FUNCTION_ID,
..GraphNode::default()
},
],
..GraphDiff::default()
})
.expect("the edit applies");
let main_node = Uuid::from_u128(0x30);
let mut main = Graph::empty();
main.root = Some(main_node);
main.nodes.insert(
main_node,
GraphNode {
id: main_node,
function: nodes::SUCCEED_FUNCTION_ID,
..GraphNode::default()
},
);
interp.load(main).expect("the main behavior loads");
interp.tick(&mut ctx).unwrap();
assert_eq!(
ticks.borrow().get(&b),
Some(&2),
"the run survived the load"
);
for id in [parallel, call_node, run, added, main_node] {
assert!(
interp.graph().nodes.contains_key(&id),
"{id} is in the graph"
);
}
interp
.load(Graph::empty())
.expect("an empty main behavior loads");
assert!(!interp.graph().nodes.contains_key(&main_node));
assert!(
interp.graph().nodes.contains_key(&added),
"the run's nodes stay"
);
interp.halt(handle.id).unwrap();
interp.tick(&mut ctx).unwrap();
assert_eq!(
interp.graph().nodes.len(),
1,
"only the runner remains: {:?}",
interp.graph().nodes.keys().collect::<Vec<_>>()
);
}
#[test]
fn runs_tick_in_spawn_order_and_keep_their_decorators() {
use arora_behavior::graph::{GraphDiff, Link, LinkSource, Node as GraphNode, Port};
let leaf = Uuid::from_u128(0xC1);
let (mut bridge, _statuses, _ticks) = scripted(leaf, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let mut decorators = Vec::new();
for (n, ids) in [[0x40, 0x41, 0x42], [0x50, 0x51, 0x52], [0x60, 0x61, 0x62]]
.into_iter()
.enumerate()
{
let handle = interp
.spawn_graph(
&graph_type(),
stepping_graph(ids, leaf),
RunPolicy::Concurrent,
)
.unwrap();
let decorator = interp
.graph()
.nodes
.values()
.find(|node| {
node.outputs
.first()
.and_then(|io| io.predetermined_key.as_deref())
== Some(handle.status.path.as_str())
})
.expect("the run's decorator")
.id;
decorators.push(decorator);
let root = interp.graph().root.unwrap();
assert_eq!(
interp.graph().nodes[&root].children.as_deref(),
Some(decorators.as_slice()),
"after {} spawns the runner holds the runs in spawn order",
n + 1
);
}
let refused = [
GraphDiff {
remove_nodes: vec![decorators[1]],
..GraphDiff::default()
},
GraphDiff {
add_nodes: vec![GraphNode {
id: decorators[1],
function: nodes::SUCCEED_FUNCTION_ID,
..GraphNode::default()
}],
..GraphDiff::default()
},
GraphDiff {
add_links: vec![Link::new(
Port::new(decorators[1], nodes::RUN_STATUS_LATCH_PARAM_ID),
LinkSource::Literal(Status::Success.into()),
)],
..GraphDiff::default()
},
GraphDiff {
remove_nodes: vec![Uuid::from_u128(0x50)],
..GraphDiff::default()
},
];
for diff in refused {
assert!(interp.apply(diff.clone()).is_err(), "{diff:?} is refused");
}
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).expect("the scaffold still lowers");
assert!(interp.graph().nodes.contains_key(&decorators[1]));
}
#[test]
fn a_node_placed_under_a_run_by_a_later_edit_belongs_to_it() {
use arora_behavior::graph::{Graph, GraphDiff, Node as GraphNode};
let leaf = Uuid::from_u128(0xC2);
let (mut bridge, _statuses, _ticks) = scripted(leaf, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let [parallel, call_node, run] = [0x90, 0x91, 0x92].map(Uuid::from_u128);
let handle = interp
.spawn_graph(
&graph_type(),
stepping_graph([0x90, 0x91, 0x92], leaf),
RunPolicy::Concurrent,
)
.unwrap();
let loose = Uuid::from_u128(0x93);
interp
.apply(GraphDiff {
add_nodes: vec![GraphNode {
id: loose,
function: nodes::RUN_FUNCTION_ID,
..GraphNode::default()
}],
..GraphDiff::default()
})
.unwrap();
interp
.apply(GraphDiff {
add_nodes: vec![GraphNode {
id: parallel,
function: nodes::PARALLEL_FUNCTION_ID,
children: Some(vec![call_node, run, loose]),
..GraphNode::default()
}],
..GraphDiff::default()
})
.unwrap();
interp.load(Graph::empty()).unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp
.tick(&mut ctx)
.expect("the run still lowers after the load");
assert!(
interp.graph().nodes.contains_key(&loose),
"the load kept it"
);
interp.halt(handle.id).unwrap();
interp.tick(&mut ctx).unwrap();
assert_eq!(
interp.graph().nodes.len(),
1,
"the halt pruned it with the run"
);
}
fn reading(root: u128, variable: Uuid, name: &str) -> arora_behavior::graph::Graph {
use arora_behavior::graph::{Graph, Link, LinkSource, Node as GraphNode, Port};
let root = Uuid::from_u128(root);
let mut graph = Graph::empty();
graph.root = Some(root);
graph.nodes.insert(
root,
GraphNode {
id: root,
function: nodes::SUCCEED_FUNCTION_ID,
..GraphNode::default()
},
);
graph.links.push(Link::new(
Port::new(root, Uuid::from_u128(0x1)),
LinkSource::Variable(variable),
));
graph.variables.insert(variable, name.to_string());
graph
}
#[test]
fn a_spawned_graph_that_overlaps_the_device_s_is_refused() {
use arora_behavior::graph::{Graph, GraphType, Link, LinkSource, Node as GraphNode, Port};
let leaf = Uuid::from_u128(0xD1);
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
interp
.spawn_graph(
&graph_type(),
stepping_graph([0x70, 0x71, 0x72], leaf),
RunPolicy::Concurrent,
)
.unwrap();
let variable = Uuid::from_u128(0x5);
interp.load(reading(0xA0, variable, "face/x")).unwrap();
let nodes_before = interp.graph().nodes.len();
let fresh = || stepping_graph([0x80, 0x81, 0x82], leaf);
let [parallel, call_node, run] = [0x80, 0x81, 0x82].map(Uuid::from_u128);
let mut rootless = fresh();
rootless.root = None;
let mut stray_root = fresh();
stray_root.root = Some(Uuid::from_u128(0x99));
let reused = stepping_graph([0x80, 0x71, 0x82], leaf);
let mut reaching_out = fresh();
reaching_out.links.push(Link::new(
Port::new(Uuid::from_u128(0x71), nodes::RUN_CALL_PARAM_ID),
LinkSource::Literal(Value::Unit),
));
let mut renaming = fresh();
renaming.variables.insert(variable, "face/y".to_string());
let mut dangling_child = fresh();
dangling_child.nodes.get_mut(¶llel).unwrap().children =
Some(vec![call_node, run, Uuid::from_u128(0x99)]);
let mut cycle = fresh();
cycle.nodes.get_mut(&run).unwrap().children = Some(vec![run]);
let mut two_parents = fresh();
two_parents.nodes.get_mut(&run).unwrap().children = Some(vec![call_node]);
let mut stray_node = fresh();
stray_node.nodes.insert(
Uuid::from_u128(0x83),
GraphNode {
id: Uuid::from_u128(0x83),
function: nodes::RUN_FUNCTION_ID,
..GraphNode::default()
},
);
let mut child_outside = fresh();
child_outside.nodes.get_mut(&run).unwrap().children = Some(vec![Uuid::from_u128(0xA0)]);
let tree = graph_type();
let ty = |name: &str, major, minor| GraphType {
name: name.to_string(),
version: semver::Version::new(major, minor, 0),
};
for (what, graph_type, graph) in [
("no root", tree.clone(), rootless),
("a root that is not a node", tree.clone(), stray_root),
("a node id the device uses", tree.clone(), reused),
("a link onto another node", tree.clone(), reaching_out),
("a variable in use renamed", tree.clone(), renaming),
("a child that is not a node", tree.clone(), dangling_child),
("a cycle", tree.clone(), cycle),
("a node with two parents", tree.clone(), two_parents),
("a node outside the tree", tree.clone(), stray_node),
("a child of the main behavior", tree.clone(), child_outside),
("another language", ty("node-graph", 1, 0), fresh()),
("another major", ty("behavior-tree", 2, 0), fresh()),
("a newer minor", ty("behavior-tree", 1, 99), fresh()),
] {
let refused = interp.spawn_graph(&graph_type, graph, RunPolicy::Concurrent);
assert!(refused.is_err(), "a graph with {what} is refused");
}
assert_eq!(
interp.graph().nodes.len(),
nodes_before,
"a refused spawn leaves the graph as it was"
);
let mut over_a_run = Graph::empty();
let run_node = Uuid::from_u128(0x72);
over_a_run.root = Some(run_node);
over_a_run.nodes.insert(
run_node,
GraphNode {
id: run_node,
function: nodes::SUCCEED_FUNCTION_ID,
..GraphNode::default()
},
);
assert!(
interp.load(over_a_run).is_err(),
"a load cannot replace a run's node"
);
}
#[test]
fn a_run_s_variables_leave_with_it() {
let leaf = Uuid::from_u128(0xE1);
let (mut bridge, _statuses, _ticks) = scripted(leaf, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let variable = Uuid::from_u128(0x6);
let handle = interp
.spawn_graph(
&graph_type(),
reading(0xB0, variable, "a"),
RunPolicy::Concurrent,
)
.unwrap();
assert!(
interp.load(reading(0xB1, variable, "b")).is_err(),
"a load cannot rename a variable a run reads"
);
interp.halt(handle.id).unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
assert!(!interp.graph().variables.contains_key(&variable));
interp
.spawn_graph(
&graph_type(),
reading(0xB2, variable, "b"),
RunPolicy::Concurrent,
)
.expect("the halted run's declaration is gone");
}
#[test]
fn a_run_that_errors_ends_failure_and_the_others_tick_on() {
let good = Uuid::from_u128(0xF5);
let missing = Uuid::from_u128(0xF6);
let (mut bridge, _statuses, ticks) = scripted(good, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let failing = interp
.spawn_graph(
&graph_type(),
stepping_graph([0x10, 0x11, 0x12], missing),
RunPolicy::Concurrent,
)
.unwrap();
interp
.spawn_graph(
&graph_type(),
stepping_graph([0x20, 0x21, 0x22], good),
RunPolicy::Concurrent,
)
.unwrap();
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
assert!(interp.tick(&mut ctx).is_err(), "the error is reported");
let failure: Value = Status::Failure.into();
assert_eq!(
store.read(std::slice::from_ref(&failing.status)),
vec![Some(failure)]
);
for _ in 0..2 {
interp
.tick(&mut ctx)
.expect("the failed run no longer errors");
}
assert_eq!(
ticks.borrow().get(&good),
Some(&2),
"the other run ticks on"
);
}
#[test]
fn a_halted_or_finished_run_s_ids_can_be_spawned_again() {
let leaf = Uuid::from_u128(0xF7);
let (mut bridge, _statuses, _ticks) = scripted(leaf, Status::Success);
let store = SimpleDataStore::new();
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
let ids = [0x30, 0x31, 0x32];
let first = interp
.spawn_graph(
&graph_type(),
stepping_graph(ids, leaf),
RunPolicy::Concurrent,
)
.unwrap();
interp.halt(first.id).unwrap();
let second = interp
.spawn_graph(
&graph_type(),
stepping_graph(ids, leaf),
RunPolicy::Concurrent,
)
.expect("the halted run's ids are free");
let mut ctx = BehaviorContext {
store: &store,
call_bridge: &mut bridge,
};
interp.tick(&mut ctx).unwrap();
let failure: Value = Status::Failure.into();
let running: Value = Status::Running.into();
assert_eq!(
store.read(&[first.status.clone(), second.status.clone()]),
vec![Some(failure), Some(running)]
);
use arora_behavior::graph::{Graph, Node as GraphNode};
let one = Uuid::from_u128(0x40);
let mut once = Graph::empty();
once.root = Some(one);
once.nodes.insert(
one,
GraphNode {
id: one,
function: nodes::SUCCEED_FUNCTION_ID,
..GraphNode::default()
},
);
interp
.spawn_graph(&graph_type(), once.clone(), RunPolicy::Concurrent)
.unwrap();
interp.tick(&mut ctx).unwrap();
interp
.spawn_graph(&graph_type(), once, RunPolicy::Concurrent)
.expect("the finished run's ids are free");
}
#[test]
fn an_edit_that_would_break_the_tree_is_refused() {
use arora_behavior::graph::{GraphDiff, Node as GraphNode};
let leaf = Uuid::from_u128(0xF8);
let mut interp = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
interp
.spawn_graph(
&graph_type(),
stepping_graph([0x50, 0x51, 0x52], leaf),
RunPolicy::Concurrent,
)
.unwrap();
let runner = interp.graph().root.unwrap();
let decorator = interp.graph().nodes[&runner].children.as_ref().unwrap()[0];
let [parallel, _, run] = [0x50, 0x51, 0x52].map(Uuid::from_u128);
let before = interp.graph().clone();
let node = |id, children| GraphNode {
id,
function: nodes::SEQ_FUNCTION_ID,
children: Some(children),
..GraphNode::default()
};
for (what, diff) in [
(
"the main root set to a decorator",
GraphDiff {
set_root: Some(decorator),
..GraphDiff::default()
},
),
(
"a decorator as a child",
GraphDiff {
add_nodes: vec![node(Uuid::from_u128(0x60), vec![decorator])],
..GraphDiff::default()
},
),
(
"the runner as a child",
GraphDiff {
add_nodes: vec![node(Uuid::from_u128(0x60), vec![runner])],
..GraphDiff::default()
},
),
(
"a run's node removed under its parent",
GraphDiff {
remove_nodes: vec![run],
..GraphDiff::default()
},
),
(
"a cycle",
GraphDiff {
add_nodes: vec![node(run, vec![parallel])],
..GraphDiff::default()
},
),
] {
assert!(interp.apply(diff).is_err(), "{what} is refused");
assert_eq!(interp.graph(), &before, "{what} changed nothing");
}
}
}
mod core_string_nodes {
use super::{LeafStatuses, LeafTicks, TestBridge};
use crate::arora_generated::behavior_tree::status::Status;
use crate::behavior::BehaviorTreeInterpreter;
use crate::nodes;
use crate::schema::Expression;
use crate::variable::VariableCell;
use crate::BehaviorTreeRuntime;
use arora_behavior::{BehaviorContext, BehaviorInterpreter};
use arora_simple_data_store::SimpleDataStore;
use arora_types::data::{DataStore, Key, StateChange};
use arora_types::value::Value;
use std::collections::HashMap;
use std::rc::Rc;
fn groot(body: &str) -> String {
format!(
r#"<root main_tree_to_execute="MainTree"><BehaviorTree ID="MainTree">{body}</BehaviorTree></root>"#
)
}
fn interpreter(body: &str) -> BehaviorTreeInterpreter {
let mut interpreter = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
interpreter
.load_groot(&groot(body))
.expect("the core nodes need no module");
interpreter
}
struct Device {
tree: BehaviorTreeInterpreter,
bridge: TestBridge,
}
fn device(body: &str) -> Device {
Device {
tree: interpreter(body),
bridge: TestBridge::new(LeafStatuses::default(), LeafTicks::default()),
}
}
fn tick(device: &mut Device, store: &SimpleDataStore) {
let mut ctx = BehaviorContext {
store,
call_bridge: &mut device.bridge,
};
device.tree.tick(&mut ctx).expect("the tree ticks");
}
fn read(store: &SimpleDataStore, key: &str) -> Option<Value> {
store.read(&[Key::from(key)]).remove(0)
}
fn string(value: &str) -> Option<Value> {
Some(Value::String(value.to_string()))
}
fn unset(key: &str) -> StateChange {
StateChange {
set: HashMap::new(),
unset: std::collections::HashSet::from([Key::from(key)]),
}
}
#[test]
fn a_branch_runs_once_per_write_of_a_key() {
let store = SimpleDataStore::new();
let mut tree = device(
r#"<Sequence>
<IsStringSet value="{message}"/>
<SetString variable="{heard}" value="{message}"/>
<UnsetString variable="{message}"/>
</Sequence>"#,
);
tick(&mut tree, &store);
assert_eq!(read(&store, "heard"), None, "no message yet: no branch");
store
.write(StateChange::set("message", Value::String("hi".into())))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "heard"), string("hi"), "the branch ran");
assert_eq!(read(&store, "message"), None, "and consumed the message");
store.write(unset("heard")).unwrap();
tick(&mut tree, &store);
tick(&mut tree, &store);
assert_eq!(read(&store, "heard"), None, "no write, no run");
store
.write(StateChange::set("message", Value::String("again".into())))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "heard"), string("again"));
assert_eq!(read(&store, "message"), None);
store.write(unset("heard")).unwrap();
store
.write(StateChange::set("message", Value::String(String::new())))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "heard"), None, "an empty string is not set");
}
#[test]
fn set_string_creates_a_missing_key() {
let store = SimpleDataStore::new();
let mut tree = device(r#"<SetString variable="{greeting}" value="hello"/>"#);
assert_eq!(read(&store, "greeting"), None);
tick(&mut tree, &store);
assert_eq!(read(&store, "greeting"), string("hello"));
}
#[test]
fn wait_string_set_waits_for_its_key() {
let store = SimpleDataStore::new();
let mut tree = device(
r#"<Sequence>
<WaitStringSet value="{reply}"/>
<SetString variable="{done}" value="yes"/>
</Sequence>"#,
);
tick(&mut tree, &store);
tick(&mut tree, &store);
assert_eq!(read(&store, "done"), None, "waiting for the reply");
store
.write(StateChange::set("reply", Value::String("ok".into())))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "done"), string("yes"));
}
#[test]
fn wait_string_set_is_running_then_success() {
let reply = VariableCell::local();
let tree = crate::tests::build(nodes::wait_str_set(Expression::Variable(reply.clone())));
let mut bridge = TestBridge::empty();
let mut runtime =
BehaviorTreeRuntime::setup(&tree, Rc::new(HashMap::new()), &mut bridge, false)
.expect("runtime sets up");
assert_eq!(runtime.tick().unwrap(), Status::Running);
assert_eq!(runtime.tick().unwrap(), Status::Running);
reply.set(Value::String("ok".into()));
assert_eq!(runtime.tick().unwrap(), Status::Success);
}
#[test]
fn regex_match_writes_the_match() {
let store = SimpleDataStore::new();
let mut tree = device(
r#"<RegexMatch value="{input}" matcher="(Ross|Brad|Victor)" first_match="{name}"/>"#,
);
tick(&mut tree, &store);
assert_eq!(read(&store, "name"), None, "no input: no match");
store
.write(StateChange::set(
"input",
Value::String("I am Victor".into()),
))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "name"), string("Victor"));
store
.write(StateChange::set("input", Value::String("nobody".into())))
.unwrap();
tick(&mut tree, &store);
assert_eq!(read(&store, "name"), string("Victor"), "a miss leaves it");
}
#[test]
fn a_non_string_value_fails_the_tick() {
let value = VariableCell::local_with(Value::F32(1.0));
let tree = crate::tests::build(nodes::is_str_set(Expression::Variable(value)));
let mut bridge = TestBridge::empty();
let mut runtime =
BehaviorTreeRuntime::setup(&tree, Rc::new(HashMap::new()), &mut bridge, false)
.expect("runtime sets up");
assert!(runtime.tick().is_err());
}
}
mod optional_parameters {
use crate::behavior::BehaviorTreeInterpreter;
use crate::ModuleFunction;
use arora_behavior::{BehaviorContext, BehaviorInterpreter};
use arora_simple_data_store::SimpleDataStore;
use arora_types::call::{Call, CallBridge, CallError, CallResult, Callable, CallableId};
use arora_types::data::{DataStore, Key, StateChange};
use arora_types::record::module::frozen::{Function, Parameter};
use arora_types::record::ty::{FrozenOption, FrozenTy, PrimitiveKind};
use arora_types::value::{StructureField, Value};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use uuid::Uuid;
const ECHO: Uuid = Uuid::from_u128(0xEC40);
const INPUT: Uuid = Uuid::from_u128(0xEC41);
const OUTPUT: Uuid = Uuid::from_u128(0xEC42);
fn unset(key: &str) -> StateChange {
StateChange {
set: HashMap::new(),
unset: std::collections::HashSet::from([Key::from(key)]),
}
}
fn optional_string() -> FrozenTy {
FrozenTy::FrozenOption(FrozenOption {
element: Box::new(FrozenTy::from(PrimitiveKind::String)),
})
}
fn echo_index() -> HashMap<Uuid, ModuleFunction> {
HashMap::from([(
ECHO,
ModuleFunction {
module_id: Uuid::from_u128(0xEC00),
function_id: ECHO,
function_name: "echo".to_string(),
function: Function {
parameters: HashMap::from([
(
INPUT,
Parameter {
name: "input".to_string(),
ty: optional_string(),
mutable: false,
},
),
(
OUTPUT,
Parameter {
name: "output".to_string(),
ty: optional_string(),
mutable: true,
},
),
]),
parameter_ordering: vec![INPUT, OUTPUT],
return_ty: FrozenTy::from(PrimitiveKind::U8),
},
},
)])
}
#[derive(Default)]
struct EchoBridge {
registered: HashMap<u64, Rc<dyn Callable>>,
next_id: u64,
inputs: Rc<RefCell<Vec<Value>>>,
}
impl CallBridge for EchoBridge {
fn arora_call(&mut self, call: Call) -> Result<CallResult, CallError> {
let input = call
.args
.iter()
.find(|arg| arg.id == INPUT)
.map(|arg| (*arg.value).clone())
.ok_or(CallError::Generic {
message: "no input".into(),
})?;
self.inputs.borrow_mut().push(input.clone());
let output = match input {
Value::Option(None) => Value::Option(None),
Value::Option(Some(s)) if matches!(*s, Value::String(_)) => Value::Option(Some(s)),
other => {
return Err(CallError::Generic {
message: format!("input: unexpected value {other}"),
})
}
};
Ok(CallResult {
ret: Value::Unit,
mutated: vec![StructureField {
id: OUTPUT,
value: Box::new(output),
}],
})
}
fn arora_register_callable(&mut self, callable: Rc<dyn Callable>) -> CallableId {
let id = self.next_id;
self.next_id += 1;
self.registered.insert(id, callable);
CallableId { id }
}
fn arora_unregister_callable(&mut self, callable_id: &CallableId) {
self.registered.remove(&callable_id.id);
}
fn arora_call_indirect(&mut self, callable_id: &CallableId) -> Result<Value, CallError> {
let callable = self.registered[&callable_id.id].clone();
callable.call(self)
}
}
#[test]
fn an_absent_key_binds_none_and_an_optional_output_maps_to_the_key() {
let store = SimpleDataStore::new();
let mut tree = BehaviorTreeInterpreter::new(Rc::new(echo_index()));
tree.load_groot(
r#"<root main_tree_to_execute="MainTree"><BehaviorTree ID="MainTree">
<Echo input="{in}" output="{out}"/>
</BehaviorTree></root>"#,
)
.expect("echo resolves by its PascalCase name");
let mut bridge = EchoBridge::default();
let inputs = bridge.inputs.clone();
let read = |key: &str| store.read(&[Key::from(key)]).remove(0);
let mut tick = |bridge: &mut EchoBridge| {
let mut ctx = BehaviorContext {
store: &store,
call_bridge: bridge,
};
tree.tick(&mut ctx).expect("the tree ticks");
};
tick(&mut bridge);
assert_eq!(inputs.borrow().last(), Some(&Value::Option(None)));
assert_eq!(read("out"), None);
store
.write(StateChange::set("in", Value::String("x".into())))
.unwrap();
tick(&mut bridge);
assert_eq!(
inputs.borrow().last(),
Some(&Value::Option(Some(Box::new(Value::String("x".into())))))
);
assert_eq!(read("out"), Some(Value::String("x".into())));
store.write(unset("in")).unwrap();
tick(&mut bridge);
assert_eq!(inputs.borrow().last(), Some(&Value::Option(None)));
assert_eq!(read("out"), None);
}
}