use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use arora_behavior::{golden, BehaviorContext, BehaviorInterpreter, BehaviorStatus};
use arora_behavior_tree::ModuleFunction;
use arora_bridge::{Bridge, BridgeCommand, BridgeOp, Inbound};
use arora_engine::engine::HostFunction;
use arora_hal::Hal;
use arora_types::call::{Call, CallBridge, CallError, CallResult};
use arora_types::data::{DataStore, Key, StateChange, Subscription};
use arora_types::value::Value;
use futures::{FutureExt, Stream, StreamExt};
use uuid::Uuid;
use crate::Arora;
#[derive(Debug, PartialEq, Eq)]
pub enum StepOutcome {
Live,
Unregistered,
}
#[derive(Debug)]
pub enum RuntimeError {
Store(String),
BehaviorTree(String),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::Store(m) => write!(f, "data store error: {m}"),
RuntimeError::BehaviorTree(m) => write!(f, "behavior tree error: {m}"),
}
}
}
impl std::error::Error for RuntimeError {}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TelemetrySnapshot {
pub loop_hz: Option<f32>,
pub claimed: bool,
pub behavior: Option<String>,
}
#[derive(Clone, Default)]
pub struct Telemetry {
inner: Arc<std::sync::RwLock<TelemetrySnapshot>>,
}
impl Telemetry {
pub fn snapshot(&self) -> TelemetrySnapshot {
self.inner.read().expect("telemetry lock poisoned").clone()
}
fn update(&self, apply: impl FnOnce(&mut TelemetrySnapshot)) {
apply(&mut self.inner.write().expect("telemetry lock poisoned"));
}
}
#[derive(Default)]
pub struct Clock {
time_ns: u64,
}
pub struct ClockValues {
pub time_ns: u64,
pub dt_ns: u64,
}
#[derive(Default)]
pub struct Pending {
pub sensors: Vec<StateChange>,
pub events: Vec<Inbound>,
}
pub fn sweep_now(
hal_feed: &mut (impl Stream<Item = StateChange> + Unpin),
inbound: &mut (impl Stream<Item = Inbound> + Unpin),
pending: &mut Pending,
) {
while let Some(Some(reading)) = hal_feed.next().now_or_never() {
pending.sensors.push(reading);
}
while let Some(Some(event)) = inbound.next().now_or_never() {
pending.events.push(event);
}
}
pub fn tick_clock(clock: &mut Clock, dt: Duration) -> ClockValues {
let dt_ns = dt.as_nanos() as u64;
clock.time_ns = clock.time_ns.saturating_add(dt_ns);
ClockValues {
time_ns: clock.time_ns,
dt_ns,
}
}
pub fn publish_clock(store: &dyn DataStore, clock: &ClockValues) -> Result<(), RuntimeError> {
let mut change = StateChange::new();
change
.set
.insert(Key::from(golden::DT), Some(Value::U64(clock.dt_ns)));
change
.set
.insert(Key::from(golden::TIME), Some(Value::U64(clock.time_ns)));
store
.write(change)
.map_err(|e| RuntimeError::Store(e.to_string()))
}
pub fn apply_sensors(
store: &dyn DataStore,
sensors: Vec<StateChange>,
) -> Result<StateChange, RuntimeError> {
let mut applied = StateChange::new();
for change in sensors {
for (key, value) in &change.set {
applied.unset.remove(key);
applied.set.insert(key.clone(), value.clone());
}
for key in &change.unset {
applied.set.remove(key);
applied.unset.insert(key.clone());
}
store
.write(change)
.map_err(|e| RuntimeError::Store(e.to_string()))?;
}
Ok(applied)
}
pub fn apply_events(
store: &dyn DataStore,
function_index: &HashMap<Uuid, ModuleFunction>,
call_bridge: &mut dyn CallBridge,
events: Vec<Inbound>,
telemetry: &Telemetry,
) -> Result<StepOutcome, RuntimeError> {
let mut outcome = StepOutcome::Live;
for event in events {
match event {
Inbound::Command(cmd) => apply_command(store, function_index, call_bridge, cmd)?,
Inbound::DeviceInfo(Ok(None)) => outcome = StepOutcome::Unregistered,
Inbound::DeviceInfo(Ok(Some(_info))) => { }
Inbound::DeviceInfo(Err(e)) => {
log::warn!("bridge endpoint error: {e}");
}
Inbound::DataRequested(requested) => {
telemetry.update(|t| t.claimed = requested);
}
}
}
Ok(outcome)
}
fn apply_command(
store: &dyn DataStore,
function_index: &HashMap<Uuid, ModuleFunction>,
call_bridge: &mut dyn CallBridge,
cmd: BridgeCommand,
) -> Result<(), RuntimeError> {
let result = match &cmd.op {
BridgeOp::Get(keys) => {
let values = store.read(keys);
let array = values
.into_iter()
.map(|v| Value::Option(v.map(Box::new)))
.collect();
Ok(CallResult {
ret: Value::ArrayValue(array),
mutated: Vec::new(),
})
}
BridgeOp::Update(change) => match store.write(change.clone()) {
Ok(()) => Ok(CallResult {
ret: Value::Unit,
mutated: Vec::new(),
}),
Err(e) => Err(e.to_string()),
},
BridgeOp::Call(call) => {
match call.module_id {
Some(module) => call_bridge
.arora_call(&module, call.clone())
.map_err(|e| format!("call failed: {e:?}")),
None => Err("call is missing its module id".to_string()),
}
}
BridgeOp::ListKeys { prefix } => {
let snapshot = store.snapshot();
let mut paths: Vec<String> = snapshot
.storage
.iter()
.filter(|(_, value)| value.is_some())
.map(|(key, _)| key.path.clone())
.filter(|path| prefix.as_ref().is_none_or(|p| path.starts_with(p.as_str())))
.collect();
paths.sort();
Ok(CallResult {
ret: Value::ArrayValue(paths.into_iter().map(Value::String).collect()),
mutated: Vec::new(),
})
}
BridgeOp::ListMethods { prefix } => {
let mut names: Vec<String> = function_index
.values()
.map(|f| f.function_name.clone())
.filter(|name| prefix.as_ref().is_none_or(|p| name.starts_with(p.as_str())))
.collect();
names.sort();
names.dedup();
Ok(CallResult {
ret: Value::ArrayValue(names.into_iter().map(Value::String).collect()),
mutated: Vec::new(),
})
}
};
cmd.reply(result);
Ok(())
}
pub type InterpreterCell = Rc<RefCell<Option<Box<dyn BehaviorInterpreter>>>>;
pub(crate) struct GoldenEdit {
pub(crate) interpreter: InterpreterCell,
}
impl HostFunction for GoldenEdit {
fn call(&self, call: Call) -> Result<CallResult, CallError> {
let diff = golden::decode_apply(&call).map_err(|message| CallError::Guest { message })?;
let mut cell = self
.interpreter
.try_borrow_mut()
.map_err(|_| CallError::Generic {
message: "the behavior is being ticked; edit between steps".to_string(),
})?;
let interpreter = cell.as_mut().ok_or_else(|| CallError::Generic {
message: "no behavior interpreter is installed".to_string(),
})?;
interpreter.apply(diff).map_err(|e| CallError::Guest {
message: e.to_string(),
})?;
Ok(CallResult {
ret: Value::Unit,
mutated: Vec::new(),
})
}
}
pub fn tick_behavior(
interpreter: &mut Option<Box<dyn BehaviorInterpreter>>,
store: &dyn DataStore,
engine: &mut arora_engine::engine::PinnedEngine,
telemetry: &Telemetry,
) -> Result<(), RuntimeError> {
let Some(behavior) = interpreter.as_mut() else {
return Ok(());
};
let mut ctx = BehaviorContext {
store,
call_bridge: engine,
};
let status = behavior
.tick(&mut ctx)
.map_err(|e| RuntimeError::BehaviorTree(e.to_string()))?;
if status == BehaviorStatus::Done {
*interpreter = None;
telemetry.update(|t| t.behavior = None);
}
Ok(())
}
pub fn flush(changes: &Subscription) -> StateChange {
let mut merged = StateChange::new();
while let Some(change) = changes.try_recv() {
for (key, value) in change.set {
if golden::is_golden(key.get_path()) {
continue;
}
merged.unset.remove(&key);
merged.set.insert(key, value);
}
for key in change.unset {
if golden::is_golden(key.get_path()) {
continue;
}
merged.set.remove(&key);
merged.unset.insert(key);
}
}
merged
}
pub fn write_hal(hal: &dyn Hal, out: &StateChange, sensor_applied: &StateChange) {
let mut for_hal = StateChange::new();
for (key, value) in &out.set {
if sensor_applied.set.get(key) == Some(value) {
continue;
}
for_hal.set.insert(key.clone(), value.clone());
}
for key in &out.unset {
if sensor_applied.unset.contains(key) {
continue;
}
for_hal.unset.insert(key.clone());
}
if !for_hal.is_empty() {
hal.try_send(&for_hal);
}
}
pub fn write_bridges(bridges: &mut [Box<dyn Bridge>], out: &StateChange) {
for bridge in bridges {
bridge.try_send(out);
}
}
impl Arora {
pub fn telemetry(&self) -> Telemetry {
self.telemetry.clone()
}
pub fn step(&mut self, dt: Duration) -> Result<StepOutcome, RuntimeError> {
sweep_now(&mut self.hal_feed, &mut self.inbound, &mut self.pending);
let clock = tick_clock(&mut self.clock, dt);
publish_clock(&*self.store, &clock)?;
let sensor_applied =
apply_sensors(&*self.store, std::mem::take(&mut self.pending.sensors))?;
let outcome = apply_events(
&*self.store,
&self.function_index,
&mut self.engine,
std::mem::take(&mut self.pending.events),
&self.telemetry,
)?;
tick_behavior(
&mut self.interpreter.borrow_mut(),
&*self.store,
&mut self.engine,
&self.telemetry,
)?;
let out = flush(&self.store_changes);
if !out.is_empty() {
write_hal(&*self.hal, &out, &sensor_applied);
write_bridges(&mut self.bridges, &out);
}
Ok(outcome)
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
impl Arora {
pub const DEFAULT_STEP_PERIOD: Duration = Duration::from_millis(10);
pub async fn run(&mut self, period: Duration) -> Result<(), RuntimeError> {
let mut ticker = tokio::time::interval(period);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut window_start = std::time::Instant::now();
let mut steps_in_window: u32 = 0;
let mut last_step = std::time::Instant::now();
loop {
tokio::select! {
biased;
_ = ticker.tick() => {}
Some(reading) = self.hal_feed.next() => {
self.pending.sensors.push(reading);
continue;
}
Some(event) = self.inbound.next() => {
self.pending.events.push(event);
continue;
}
}
let now = std::time::Instant::now();
let dt = now.duration_since(last_step);
last_step = now;
if self.step(dt)? == StepOutcome::Unregistered {
return Ok(());
}
steps_in_window += 1;
let elapsed = window_start.elapsed();
if elapsed >= Duration::from_secs(1) {
let hz = steps_in_window as f32 / elapsed.as_secs_f32();
self.telemetry.update(|t| t.loop_hz = Some(hz));
window_start = std::time::Instant::now();
steps_in_window = 0;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use arora_behavior_tree::behavior::BehaviorTreeInterpreter;
use arora_bridge::{BridgeResult, DeviceInfo, FakeBridge, InboundStream};
use arora_hal::FakeHal;
use arora_simple_data_store::{NamespacedStore, SimpleDataStore};
use async_trait::async_trait;
use futures::channel::{mpsc, oneshot};
use futures::stream;
use std::rc::Rc;
const FRAME: Duration = Duration::from_millis(16);
struct UnregisterBridge;
#[async_trait]
impl Bridge for UnregisterBridge {
fn take_inbound(&mut self) -> InboundStream {
Box::pin(stream::once(async { Inbound::DeviceInfo(Ok(None)) }))
}
fn try_send(&mut self, _change: &StateChange) {}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
fn build(bridge: Box<dyn Bridge>) -> Arora {
Arora::builder()
.with_hal(Box::new(FakeHal::new()))
.with_bridge(bridge)
.build()
.expect("arora builds")
}
fn build_in(bridge: Box<dyn Bridge>, store: Box<dyn DataStore>) -> Arora {
Arora::builder()
.with_hal(Box::new(FakeHal::new()))
.with_bridge(bridge)
.with_data_store(store)
.build()
.expect("arora builds")
}
fn build_with(bridge: Box<dyn Bridge>, interpreter: Box<dyn BehaviorInterpreter>) -> Arora {
Arora::builder()
.with_hal(Box::new(FakeHal::new()))
.with_bridge(bridge)
.with_behavior_interpreter(interpreter)
.build()
.expect("arora builds")
}
fn build_in_with(
bridge: Box<dyn Bridge>,
store: Box<dyn DataStore>,
interpreter: Box<dyn BehaviorInterpreter>,
) -> Arora {
Arora::builder()
.with_hal(Box::new(FakeHal::new()))
.with_bridge(bridge)
.with_data_store(store)
.with_behavior_interpreter(interpreter)
.build()
.expect("arora builds")
}
fn groot_interpreter(xml: &str, store: &dyn DataStore) -> Box<dyn BehaviorInterpreter> {
let mut interpreter = BehaviorTreeInterpreter::new(Rc::new(HashMap::new()));
interpreter.load_groot(xml, store).expect("tree loads");
Box::new(interpreter)
}
fn drive_until_unregistered(mut arora: Arora) {
for _ in 0..1000 {
if arora.step(FRAME).expect("step ok") == StepOutcome::Unregistered {
return;
}
}
panic!("device never reported unregistered");
}
#[test]
fn builder_defaults_to_a_self_contained_device() {
let arora = Arora::builder().build().expect("default device builds");
assert!(
arora.interpreter.borrow().is_some(),
"default installs an (empty) behavior interpreter"
);
assert!(arora.bridges.is_empty(), "no bridge unless one is added");
}
#[test]
fn a_bridgeless_device_steps() {
let mut arora = Arora::builder().build().expect("builds");
for _ in 0..5 {
assert_eq!(arora.step(FRAME).expect("step"), StepOutcome::Live);
}
}
#[test]
fn a_default_devices_empty_interpreter_idles() {
let mut arora = build(Box::new(FakeBridge::new()));
for _ in 0..5 {
arora.step(FRAME).expect("step");
}
assert!(
arora.interpreter.borrow().is_some(),
"the empty interpreter idles and stays installed"
);
}
#[test]
fn stops_when_unregistered() {
let arora = build(Box::new(UnregisterBridge));
drive_until_unregistered(arora);
}
#[tokio::test]
async fn a_call_edits_the_behavior_through_the_engine() {
let mut arora = build(Box::new(UnregisterBridge));
let (tx, rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(
BridgeOp::Call(golden::encode_apply(&arora_behavior::GraphDiff::default())),
tx,
),
)
.unwrap();
let result = rx.await.expect("reply").expect("the edit call succeeds");
assert_eq!(result.ret, Value::Unit);
}
#[test]
fn a_tick_time_edit_fails_cleanly() {
let interpreter: InterpreterCell = Rc::new(RefCell::new(Some(Box::new(
BehaviorTreeInterpreter::new(Rc::new(HashMap::new())),
)
as Box<dyn BehaviorInterpreter>)));
let edit = GoldenEdit {
interpreter: interpreter.clone(),
};
let _phase_4_holds_it = interpreter.borrow_mut();
let err = edit
.call(golden::encode_apply(&arora_behavior::GraphDiff::default()))
.expect_err("a tick-time edit is refused");
assert!(err.to_string().contains("being ticked"), "{err}");
}
#[test]
fn an_edit_without_an_interpreter_errors() {
let edit = GoldenEdit {
interpreter: Rc::new(RefCell::new(None)),
};
let err = edit
.call(golden::encode_apply(&arora_behavior::GraphDiff::default()))
.expect_err("no interpreter to edit");
assert!(err.to_string().contains("no behavior interpreter"), "{err}");
}
#[test]
fn runs_a_set_tree() {
let xml = r#"<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="11111111-1111-4111-8111-111111111111">
<Succeed name="22222222-2222-4222-8222-222222222222" />
</Sequence>
</BehaviorTree>
</root>"#;
let store = SimpleDataStore::new();
let interpreter = groot_interpreter(xml, &store);
let arora = build_in_with(
Box::new(UnregisterBridge),
Box::new(store.clone()),
interpreter,
);
drive_until_unregistered(arora);
}
#[tokio::test]
async fn get_and_update_commands_round_trip() {
let mut arora = build(Box::new(UnregisterBridge));
let key = Key::from("greeting");
let (tx, rx) = oneshot::channel();
let mut set = HashMap::new();
set.insert(key.clone(), Some(Value::String("hi".into())));
let change = StateChange {
set,
unset: std::collections::HashSet::new(),
};
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(BridgeOp::Update(change), tx),
)
.unwrap();
assert!(rx.await.unwrap().is_ok(), "update should succeed");
let (tx, rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(BridgeOp::Get(vec![key]), tx),
)
.unwrap();
let result = rx.await.unwrap().expect("get should succeed");
assert_eq!(
result.ret,
Value::ArrayValue(vec![Value::Option(Some(Box::new(Value::String(
"hi".into()
))))])
);
}
struct WriteOnce;
impl BehaviorInterpreter for WriteOnce {
fn tick(
&mut self,
ctx: &mut BehaviorContext,
) -> Result<BehaviorStatus, arora_behavior::BehaviorError> {
ctx.store
.write(StateChange::set(
"from_behavior",
Value::String("hi".into()),
))
.map_err(|e| arora_behavior::BehaviorError {
message: e.to_string(),
})?;
Ok(BehaviorStatus::Done)
}
}
#[tokio::test]
async fn runs_an_installed_non_tree_behavior() {
let mut arora = build_with(Box::new(FakeBridge::new()), Box::new(WriteOnce));
arora.step(FRAME).expect("step");
let (tx, rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(BridgeOp::Get(vec![Key::from("from_behavior")]), tx),
)
.unwrap();
let result = rx.await.unwrap().expect("get ok");
assert_eq!(
result.ret,
Value::ArrayValue(vec![Value::Option(Some(Box::new(Value::String(
"hi".into()
))))])
);
}
#[tokio::test]
async fn list_keys_enumerates_the_store_by_prefix() {
let mut arora = build(Box::new(UnregisterBridge));
let mut set = HashMap::new();
set.insert(Key::from("face/mouth"), Some(Value::F32(0.5)));
set.insert(Key::from("face/eyes"), Some(Value::F32(0.1)));
set.insert(Key::from("body/hand"), Some(Value::F32(0.9)));
let (tx, _rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(
BridgeOp::Update(StateChange {
set,
unset: std::collections::HashSet::new(),
}),
tx,
),
)
.unwrap();
let (tx, rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(
BridgeOp::ListKeys {
prefix: Some("face".into()),
},
tx,
),
)
.unwrap();
let result = rx.await.unwrap().expect("list_keys ok");
assert_eq!(
result.ret,
Value::ArrayValue(vec![
Value::String("face/eyes".into()),
Value::String("face/mouth".into()),
])
);
let (tx, rx) = oneshot::channel();
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(BridgeOp::ListMethods { prefix: None }, tx),
)
.unwrap();
let methods = rx.await.unwrap().expect("list_methods ok");
assert!(
matches!(methods.ret, Value::ArrayValue(_)),
"list_methods returns an array"
);
}
struct WriteKey {
key: &'static str,
value: Value,
}
impl BehaviorInterpreter for WriteKey {
fn tick(
&mut self,
ctx: &mut BehaviorContext,
) -> Result<BehaviorStatus, arora_behavior::BehaviorError> {
ctx.store
.write(StateChange::set(self.key, self.value.clone()))
.map_err(|e| arora_behavior::BehaviorError {
message: e.to_string(),
})?;
Ok(BehaviorStatus::Done)
}
}
#[tokio::test]
async fn device_over_namespaced_store_writes_under_namespace() {
let shared = SimpleDataStore::new();
let store = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
let mut arora = build_in(Box::new(FakeBridge::new()), Box::new(store));
let (tx, rx) = oneshot::channel();
let mut set = HashMap::new();
set.insert(Key::from("greeting"), Some(Value::String("hi".into())));
apply_command(
&*arora.store,
&arora.function_index,
&mut arora.engine,
BridgeCommand::new(
BridgeOp::Update(StateChange {
set,
unset: std::collections::HashSet::new(),
}),
tx,
),
)
.unwrap();
assert!(rx.await.unwrap().is_ok(), "update should succeed");
assert_eq!(
shared.read(&[Key::from("robotA/greeting")]),
vec![Some(Value::String("hi".into()))],
"the write landed under the device namespace"
);
assert_eq!(
shared.read(&[Key::from("greeting")]),
vec![None],
"the bare key must not be set in the shared store"
);
}
#[tokio::test]
async fn behavior_writes_land_in_the_namespaced_store() {
let shared = SimpleDataStore::new();
let store = NamespacedStore::new(Arc::new(shared.clone()), "robotA");
let mut arora = build_in_with(
Box::new(FakeBridge::new()),
Box::new(store),
Box::new(WriteKey {
key: "greeting",
value: Value::String("hi".into()),
}),
);
assert_eq!(arora.step(FRAME).expect("step"), StepOutcome::Live);
assert_eq!(
shared.read(&[Key::from("robotA/greeting")]),
vec![Some(Value::String("hi".into()))],
"the behavior's write landed under the device namespace"
);
assert_eq!(
shared.read(&[Key::from("greeting")]),
vec![None],
"the bare key must not be set in the shared store"
);
}
#[test]
fn golden_clock_is_published_to_the_store_each_step() {
let store = SimpleDataStore::new();
let mut arora = build_in(Box::new(FakeBridge::new()), Box::new(store.clone()));
assert_eq!(store.read(&[Key::from(golden::DT)]), vec![None]);
assert_eq!(store.read(&[Key::from(golden::TIME)]), vec![None]);
assert_eq!(
arora.step(Duration::from_millis(16)).expect("step"),
StepOutcome::Live
);
assert_eq!(
store.read(&[Key::from(golden::DT)]),
vec![Some(Value::U64(16_000_000))]
);
assert_eq!(
store.read(&[Key::from(golden::TIME)]),
vec![Some(Value::U64(16_000_000))]
);
assert_eq!(
arora.step(Duration::from_millis(4)).expect("step"),
StepOutcome::Live
);
assert_eq!(
store.read(&[Key::from(golden::DT)]),
vec![Some(Value::U64(4_000_000))]
);
assert_eq!(
store.read(&[Key::from(golden::TIME)]),
vec![Some(Value::U64(20_000_000))]
);
}
struct RecordingBridge {
sent: mpsc::UnboundedSender<StateChange>,
}
#[async_trait]
impl Bridge for RecordingBridge {
fn take_inbound(&mut self) -> InboundStream {
Box::pin(stream::pending())
}
fn try_send(&mut self, change: &StateChange) {
let _ = self.sent.unbounded_send(change.clone());
}
async fn get_device_info(&self) -> BridgeResult<Option<DeviceInfo>> {
Ok(None)
}
async fn update_device_info(
&self,
info: Option<DeviceInfo>,
) -> BridgeResult<Option<DeviceInfo>> {
Ok(info)
}
}
#[test]
fn golden_keys_are_not_forwarded_outbound() {
let (sent_tx, mut sent_rx) = mpsc::unbounded();
let mut arora = build_with(
Box::new(RecordingBridge { sent: sent_tx }),
Box::new(WriteKey {
key: "greeting",
value: Value::String("hi".into()),
}),
);
for _ in 0..5 {
arora.step(FRAME).expect("step");
}
let mut forwarded_keys: Vec<String> = Vec::new();
while let Ok(change) = sent_rx.try_recv() {
forwarded_keys.extend(change.set.keys().map(|k| k.path.clone()));
}
assert!(
forwarded_keys.iter().any(|k| k.as_str() == "greeting"),
"the ordinary behavior write should be forwarded outbound, got {forwarded_keys:?}"
);
assert!(
!forwarded_keys.iter().any(|k| golden::is_golden(k.as_str())),
"golden keys must never be forwarded outbound, got {forwarded_keys:?}"
);
}
}