#[cfg(feature = "native")]
pub mod device_dir;
#[cfg(feature = "native")]
pub mod module_dir;
mod module_discovery;
#[cfg(feature = "native")]
pub mod operator;
mod run;
pub mod runtime;
#[cfg(feature = "studio-bridge")]
pub mod studio;
#[cfg(feature = "tui")]
pub mod tui;
#[cfg(feature = "native")]
pub use arora_bridge_ws as bridge_ws;
#[cfg(feature = "native")]
pub use run::{
local_ws_bridge, local_ws_bridge_with, run, run_with, run_with_frontend, run_with_hal,
serve_local_ws_bridge, standard_frontend, DeviceCli,
};
pub use runtime::RuntimeError;
pub use arora_behavior_tree::behavior::BehaviorTreeInterpreter;
pub use arora_behavior_tree::ModuleFunction;
use crate::runtime::EndpointInbound;
use anyhow::Result;
use arora_behavior::{interpreter_module, BehaviorInterpreter};
pub use arora_behavior::{TaskHandle, TaskId};
pub use arora_bridge::Caller;
pub use arora_bridge::MethodSignature;
use arora_bridge::{client, Bridge, BridgeCommand, BridgeError, BridgeOp, Inbound};
pub use arora_engine::compiled::CompiledModule;
use arora_engine::engine::{EngineBuilder, PinnedEngine};
#[cfg(feature = "native")]
use arora_engine::executor::{native::NativeExecutor, wasm::WebAssemblyExecutor};
pub use arora_engine::module::{FunctionDescription, HostModule, ModuleBuilder};
use arora_hal::{FakeHal, Hal, UpdatesStream};
use arora_simple_data_store::SimpleDataStore;
use arora_types::call::{Call, CallBridge, CallError, CallResult};
use arora_types::data::{DataStore, KeyMeta, Subscription};
use arora_types::module::declared::AroraModule;
use arora_types::module::low::{self, Header};
use arora_types::record::module::frozen::ExportKind;
use arora_types::value::Value;
use futures::channel::{mpsc, oneshot};
use futures::stream::{self, Fuse, SelectAll};
use futures::StreamExt;
use runtime::{Clock, Pending};
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::rc::Rc;
use std::time::Duration;
use tokio::sync::watch;
use uuid::Uuid;
pub struct Arora {
pub(crate) store: Box<dyn DataStore>,
pub(crate) engine: PinnedEngine,
pub(crate) function_index: Rc<HashMap<Uuid, ModuleFunction>>,
pub(crate) interpreter: runtime::InterpreterCell,
pub(crate) behavior_error: watch::Sender<Option<String>>,
pub(crate) hal: Box<dyn Hal>,
pub(crate) bridges: Vec<Box<dyn Bridge>>,
pub(crate) hal_feed: Fuse<UpdatesStream>,
pub(crate) inbound: SelectAll<EndpointInbound>,
pub(crate) pending: Pending,
pub(crate) data_requested: Vec<bool>,
pub(crate) caller_tx: mpsc::UnboundedSender<Inbound>,
pub(crate) store_changes: Subscription,
pub(crate) clock: Clock,
}
impl Arora {
pub fn builder() -> AroraBuilder {
AroraBuilder::default()
}
pub fn store(&self) -> &dyn DataStore {
&*self.store
}
pub fn call(&mut self, call: Call) -> Result<CallResult, CallError> {
self.engine.arora_call(call)
}
pub fn load_groot(&mut self, xml: &str) -> Result<()> {
let tree = arora_behavior_tree::schema_groot::BehaviorTree::try_from_groot_xml(xml)
.map_err(|e| anyhow::anyhow!("the Groot tree does not parse: {e:?}"))?;
let graph = tree
.into_graph(&self.function_index)
.map_err(|e| anyhow::anyhow!("the Groot tree does not lower: {e:?}"))?;
self.call(interpreter_module::encode_load(&graph))
.map_err(|e| anyhow::anyhow!("the behavior did not load: {e}"))?;
Ok(())
}
pub fn engine(&mut self) -> &mut dyn CallBridge {
&mut self.engine
}
pub fn caller(&self) -> LocalCaller {
LocalCaller {
tx: self.caller_tx.clone(),
}
}
pub fn behavior_error(&self) -> watch::Receiver<Option<String>> {
self.behavior_error.subscribe()
}
}
#[derive(Clone)]
pub struct LocalCaller {
tx: mpsc::UnboundedSender<Inbound>,
}
impl LocalCaller {
fn ask(
&self,
op: BridgeOp,
) -> impl Future<Output = Result<CallResult, CallError>> + Send + 'static {
let (tx, rx) = oneshot::channel();
let sent = self
.tx
.unbounded_send(Inbound::Command(BridgeCommand::new(op, tx)))
.map_err(|_| generic("the device is gone"));
async move {
sent?;
match rx.await {
Ok(Ok(result)) => Ok(result),
Ok(Err(message)) => Err(CallError::Generic { message }),
Err(_) => Err(generic("the device dropped the request")),
}
}
}
pub fn list_keys(
&self,
prefix: Option<String>,
) -> impl Future<Output = Result<Vec<(String, KeyMeta)>, CallError>> + Send + 'static {
let reply = self.ask(BridgeOp::ListKeys { prefix });
async move {
arora_types::value_serde::from_value(reply.await?.ret)
.map_err(|e| generic(format!("the listed keys did not decode: {e}")))
}
}
pub fn describe_methods(
&self,
prefix: Option<String>,
) -> impl Future<Output = Result<Vec<MethodSignature>, CallError>> + Send + 'static {
let reply = self.ask(BridgeOp::DescribeMethods { prefix });
async move {
arora_types::value_serde::from_value(reply.await?.ret)
.map_err(|e| generic(format!("the described methods did not decode: {e}")))
}
}
pub fn invoke(
&self,
method: &str,
args: HashMap<String, Value>,
module: Option<Uuid>,
) -> impl Future<Output = Result<Invoked, CallError>> + Send + 'static {
let described = self.describe_methods(Some(method.to_string()));
let caller = self.clone();
let method = method.to_string();
async move {
let signatures = described.await?;
let signature = client::find_method(&signatures, &method, module).map_err(generic)?;
let call = client::call_of(signature, args).map_err(generic)?;
if client::task_shaped(&signature.function) {
caller.spawn(call).await.map(Invoked::Started)
} else {
caller
.ask(BridgeOp::Call(call))
.await
.map(|result| Invoked::Returned(result.ret))
}
}
}
pub fn spawn(
&self,
call: Call,
) -> impl Future<Output = Result<TaskHandle, CallError>> + Send + 'static {
let reply = self.ask(BridgeOp::Call(client::spawn(&call)));
async move { interpreter_module::decode_spawn_result(&reply.await?.ret).map_err(generic) }
}
pub fn halt(
&self,
run: TaskId,
) -> impl Future<Output = Result<(), CallError>> + Send + 'static {
let reply = self.ask(BridgeOp::Call(client::halt(run.0)));
async move { reply.await.map(|_| ()) }
}
}
impl Caller for LocalCaller {
fn call(&self, call: Call) -> arora_bridge::CallFuture<'_> {
Box::pin(self.ask(BridgeOp::Call(call)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Invoked {
Returned(Value),
Started(TaskHandle),
}
enum GuestModule {
Source(Header, Box<[u8]>),
Compiled(CompiledModule),
}
impl GuestModule {
fn header(&self) -> &Header {
match self {
GuestModule::Source(header, _) => header,
GuestModule::Compiled(compiled) => compiled.header(),
}
}
}
fn generic(message: impl Into<String>) -> CallError {
CallError::Generic {
message: message.into(),
}
}
#[derive(Default)]
pub struct AroraBuilder {
store: Option<Box<dyn DataStore>>,
hal: Option<Box<dyn Hal>>,
start_time: Duration,
bridges: Vec<Box<dyn Bridge>>,
interpreter: Option<Box<dyn BehaviorInterpreter>>,
functions: HashMap<Uuid, ModuleFunction>,
modules: Vec<GuestModule>,
host_modules: Vec<HostModule>,
groot: Option<String>,
#[cfg(feature = "native")]
step_period: Option<std::time::Duration>,
#[cfg(feature = "native")]
frontend: Option<operator::Frontend>,
}
impl AroraBuilder {
pub fn with_data_store(mut self, store: Box<dyn DataStore>) -> Self {
self.store = Some(store);
self
}
pub fn with_hal(mut self, hal: Box<dyn Hal>) -> Self {
self.hal = Some(hal);
self
}
pub fn with_start_time(mut self, start: Duration) -> Self {
self.start_time = start;
self
}
pub fn with_bridge(mut self, bridge: Box<dyn Bridge>) -> Self {
self.bridges.push(bridge);
self
}
pub fn with_behavior_interpreter(mut self, interpreter: Box<dyn BehaviorInterpreter>) -> Self {
self.interpreter = Some(interpreter);
self
}
pub fn with_module(mut self, header: Header, executable: impl Into<Box<[u8]>>) -> Self {
self.modules
.push(GuestModule::Source(header, executable.into()));
self
}
pub fn with_compiled_module(mut self, module: &CompiledModule) -> Self {
self.modules.push(GuestModule::Compiled(module.clone()));
self
}
pub fn with_declared_module<M: AroraModule>(
mut self,
executor: low::Executor,
executable: impl Into<Box<[u8]>>,
) -> Self {
for function in M::exports() {
self.functions.insert(
function.id,
ModuleFunction {
module_id: M::id(),
function_id: function.id,
function_name: function.name.to_string(),
function: function.signature,
},
);
}
self.modules
.push(GuestModule::Source(M::header(executor), executable.into()));
self
}
pub fn with_host_module(mut self, module: HostModule) -> Self {
self.host_modules.push(module);
self
}
pub fn with_groot(mut self, xml: impl Into<String>) -> Self {
self.groot = Some(xml.into());
self
}
#[cfg(feature = "native")]
pub fn with_step_period(mut self, period: std::time::Duration) -> Self {
self.step_period = Some(period);
self
}
#[cfg(feature = "native")]
pub fn with_frontend(mut self, frontend: operator::Frontend) -> Self {
self.frontend = Some(frontend);
self
}
#[cfg(feature = "native")]
pub async fn run(mut self) -> Result<()> {
let frontend = match self.frontend.take() {
Some(frontend) => frontend,
None => run::standard_frontend(),
};
if self.bridges.is_empty() {
#[cfg(feature = "studio-bridge")]
{
self = self.with_bridge(studio::default_bridge(&frontend).await?);
}
#[cfg(not(feature = "studio-bridge"))]
{
self = self.with_bridge(run::local_ws_bridge().await?);
}
}
run::run_builder_with_frontend(self, frontend).await
}
pub fn build(self) -> Result<Arora> {
let mut engine = build_engine()?;
let mut functions = self.functions;
let mut guest_modules: HashMap<Uuid, String> = HashMap::new();
for module in self.modules {
let header = module.header();
let module_id = header.id;
let module_name = header.name.clone();
if let Some(first) = guest_modules.insert(module_id, module_name.clone()) {
anyhow::bail!(
"guest modules '{first}' and '{module_name}' both have id {module_id}: a \
device loads one module per id"
);
}
for export in &header.exports {
let low::ExportSymbol::Function(function) = export;
if functions.contains_key(&function.id) {
continue;
}
match module_discovery::guest_function_signature(function) {
Some(signature) => {
functions.insert(
function.id,
ModuleFunction {
module_id,
function_id: function.id,
function_name: function.name.clone(),
function: signature,
},
);
}
None => log::warn!(
"guest module {module_id} export '{}' ({}) has a parameter or return \
type that is neither a primitive nor an optional over a scalar \
primitive; it \
dispatches but DescribeMethods does not list it",
function.name,
function.id
),
}
}
let function_count = header.exports.len();
let compiled = match module {
GuestModule::Source(header, executable) => {
CompiledModule::new(header, &executable).map_err(Into::into)
}
GuestModule::Compiled(compiled) => Ok(compiled),
};
compiled
.and_then(|compiled| engine.load_compiled_module(&compiled))
.map_err(|e| {
anyhow::anyhow!("failed to load module '{module_name}' ({module_id}): {e}")
})?;
log::info!("loaded module '{module_name}' ({module_id}): {function_count} function(s)");
}
for module in self.host_modules {
for description in module.descriptions() {
functions.insert(
description.id,
ModuleFunction {
module_id: module.id(),
function_id: description.id,
function_name: description.name.clone(),
function: description.function.clone(),
},
);
}
engine.register_module(module.id(), Box::new(module));
}
let interpreter_methods = self
.interpreter
.as_ref()
.map(|interpreter| interpreter.described_methods())
.unwrap_or_default();
let own = [
interpreter_module::LOAD,
interpreter_module::EDIT,
interpreter_module::SPAWN,
interpreter_module::HALT,
];
for (function_id, export) in &interpreter_methods {
if let Some(described) = functions.get(function_id) {
anyhow::bail!(
"function {function_id} ('{}') is described by module {} and by the behavior \
interpreter",
export.name,
described.module_id
);
}
if own.contains(function_id) {
anyhow::bail!(
"the behavior interpreter describes '{}' under the id of one of the \
interpreter module's own functions ({function_id})",
export.name
);
}
let ExportKind::Function(function) = &export.kind;
functions.insert(
*function_id,
ModuleFunction {
module_id: interpreter_module::ID,
function_id: *function_id,
function_name: export.name.clone(),
function: function.clone(),
},
);
}
let store = self
.store
.unwrap_or_else(|| Box::new(SimpleDataStore::new()));
let hal: Box<dyn Hal> = self.hal.unwrap_or_else(|| Box::new(FakeHal::new()));
let store_changes = store.subscribe();
let hal_feed = hal.updates().fuse();
let mut bridges = self.bridges;
let mut inbound = SelectAll::new();
for (endpoint, bridge) in bridges.iter_mut().enumerate() {
let disconnected = stream::once(async {
Inbound::DeviceInfo(Err(BridgeError::Disconnected(
"the endpoint's inbound stream ended".into(),
)))
});
inbound.push(
bridge
.take_inbound()
.chain(disconnected)
.map(move |event| (Some(endpoint), event))
.boxed(),
);
}
let (caller_tx, caller_rx) = mpsc::unbounded();
inbound.push(caller_rx.map(|event| (None, event)).boxed());
let endpoints = bridges.len();
let function_index = Rc::new(functions);
let interpreter = self
.interpreter
.unwrap_or_else(|| Box::new(BehaviorTreeInterpreter::new(function_index.clone())));
let interpreter: runtime::InterpreterCell = Rc::new(RefCell::new(Some(interpreter)));
let module = ModuleBuilder::new(interpreter_module::ID)
.function(interpreter_module::LOAD, {
let cell = interpreter.clone();
move |call| {
let graph = interpreter_module::decode_load(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter(&cell, |interpreter| interpreter.load(graph))
}
})
.function(interpreter_module::EDIT, {
let cell = interpreter.clone();
move |call| {
let diff = interpreter_module::decode_edit(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter(&cell, |interpreter| interpreter.apply(diff))
}
})
.function(interpreter_module::SPAWN, {
let cell = interpreter.clone();
move |call| {
let (spawned, policy) = interpreter_module::decode_spawn(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter_value(&cell, |interpreter| {
interpreter
.spawn(spawned, policy)
.map(|handle| interpreter_module::encode_spawn_result(&handle))
})
}
})
.function(interpreter_module::HALT, {
let cell = interpreter.clone();
move |call| {
let task = interpreter_module::decode_halt(&call)
.map_err(|message| CallError::Guest { message })?;
runtime::with_interpreter(&cell, |interpreter| interpreter.halt(task))
}
});
let module = interpreter_methods
.into_iter()
.fold(module, |module, (function_id, export)| {
let message = format!(
"'{}' is a task run the behavior interpreter implements: spawn it through \
the interpreter module",
export.name
);
module.function(function_id, move |_call| {
Err(CallError::Guest {
message: message.clone(),
})
})
})
.build();
engine.register_module(module.id(), Box::new(module));
let mut arora = Arora {
store,
engine,
function_index,
interpreter,
hal,
bridges,
hal_feed,
inbound,
pending: Pending::default(),
data_requested: vec![false; endpoints],
caller_tx,
store_changes,
clock: Clock::starting_at(self.start_time),
behavior_error: watch::Sender::new(None),
};
if let Some(xml) = self.groot {
arora.load_groot(&xml)?;
}
Ok(arora)
}
}
#[cfg(feature = "native")]
fn build_engine() -> Result<PinnedEngine> {
Ok(EngineBuilder::new()
.add_executor(
WebAssemblyExecutor::new()
.map_err(|e| anyhow::anyhow!("failed to create wasm executor: {e}"))?,
)
.add_executor(NativeExecutor::new())
.build())
}
#[cfg(all(not(feature = "native"), target_arch = "wasm32"))]
fn build_engine() -> Result<PinnedEngine> {
use arora_engine::executor::browser::BrowserExecutor;
Ok(EngineBuilder::new()
.add_executor(BrowserExecutor::new())
.build())
}
#[cfg(all(not(feature = "native"), not(target_arch = "wasm32")))]
fn build_engine() -> Result<PinnedEngine> {
Ok(EngineBuilder::new().build())
}
#[cfg(all(test, feature = "native"))]
mod module_loading_tests {
use super::*;
use arora_types::call::{Call, CallBridge};
use arora_types::value::Value;
const WASM: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_TEST_RUST_WASM_test_rust_wasm"));
const SUCCEED: &str = "00cd31a8-2cf4-48e6-a957-69a55de90424";
fn test_module_header() -> Header {
test_rust_wasm::test_rust_wasm::header(arora_types::module::low::Executor {
name: "wasm".to_string(),
min_version: None,
max_version: None,
})
}
#[test]
fn with_module_loads_a_wasm_module_reachable_through_call() {
let header = test_module_header();
let module_id = header.id;
let mut arora = Arora::builder()
.with_module(header, WASM.to_vec())
.build()
.expect("build a device with a loaded wasm module");
let result = arora
.call(Call {
module_id: Some(module_id),
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect("call succeed() on the loaded module");
assert_eq!(result.ret, Value::Boolean(true));
}
#[test]
fn with_declared_module_indexes_every_export_from_the_declaration() {
use test_rust_wasm::test_rust_wasm::Module;
let mut arora = Arora::builder()
.with_declared_module::<Module>(
arora_types::module::low::Executor {
name: "wasm".to_string(),
min_version: None,
max_version: None,
},
WASM.to_vec(),
)
.build()
.expect("build a device with a declared wasm module");
for declared in Module::exports() {
let indexed = arora
.function_index
.get(&declared.id)
.unwrap_or_else(|| panic!("{} is indexed", declared.name));
assert_eq!(indexed.module_id, Module::id());
assert_eq!(indexed.function_name, declared.name);
assert_eq!(indexed.function, declared.signature);
}
let result = arora
.call(Call {
module_id: Some(Module::id()),
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect("call succeed() on the declared module's guest");
assert_eq!(result.ret, Value::Boolean(true));
}
#[test]
fn load_groot_reaches_a_declared_module_function_by_name() {
use arora_types::data::{Key, StateChange};
use test_rust_wasm::test_rust_wasm::Module;
let store = SimpleDataStore::new();
store
.write(StateChange::set("angle", Value::F32(0.0)))
.expect("seed the input");
let mut arora = Arora::builder()
.with_data_store(Box::new(store.clone()))
.with_declared_module::<Module>(
arora_types::module::low::Executor {
name: "wasm".to_string(),
min_version: None,
max_version: None,
},
WASM.to_vec(),
)
.build()
.expect("build a device with a declared wasm module");
arora
.load_groot(
r#"<root main_tree_to_execute="MainTree"><BehaviorTree ID="MainTree">
<cos angle="{angle}" res="{cosine}"/>
</BehaviorTree></root>"#,
)
.expect("the tree names the guest's cos by its declared name");
arora
.step(std::time::Duration::from_millis(10))
.expect("one step ticks the tree");
assert_eq!(
store.read(&[Key::from("cosine")]),
vec![Some(Value::F32(1.0))],
"cos(0) written back to the bound key"
);
}
#[test]
fn with_groot_installs_the_tree_at_build() {
use arora_types::data::{Key, StateChange};
use test_rust_wasm::test_rust_wasm::Module;
let store = SimpleDataStore::new();
store
.write(StateChange::set("angle", Value::F32(0.0)))
.expect("seed the input");
let mut arora = Arora::builder()
.with_data_store(Box::new(store.clone()))
.with_declared_module::<Module>(
arora_types::module::low::Executor {
name: "wasm".to_string(),
min_version: None,
max_version: None,
},
WASM.to_vec(),
)
.with_groot(
r#"<root main_tree_to_execute="MainTree"><BehaviorTree ID="MainTree">
<cos angle="{angle}" res="{cosine}"/>
</BehaviorTree></root>"#,
)
.build()
.expect("the tree resolves against the declared module");
arora
.step(std::time::Duration::from_millis(10))
.expect("one step ticks the tree");
assert_eq!(
store.read(&[Key::from("cosine")]),
vec![Some(Value::F32(1.0))],
"cos(0) written back to the bound key"
);
}
#[test]
fn with_groot_fails_the_build_on_an_unknown_leaf() {
let result = Arora::builder()
.with_groot(
r#"<root main_tree_to_execute="MainTree"><BehaviorTree ID="MainTree">
<NoSuchLeaf/>
</BehaviorTree></root>"#,
)
.build();
assert!(result.is_err(), "an unknown leaf fails the build");
}
#[test]
fn guest_module_exports_join_the_method_index() {
use arora_types::record::ty::{FrozenOption, FrozenTy, PrimitiveKind};
let header = test_module_header();
let module_id = header.id;
let arora = Arora::builder()
.with_module(header, WASM.to_vec())
.build()
.expect("build a device with a loaded wasm module");
let by_name = |name: &str| {
arora
.function_index
.values()
.find(|f| f.function_name == name)
};
let cos = by_name("cos").expect("cos joined the method index");
assert_eq!(cos.module_id, module_id);
assert_eq!(cos.function.parameter_ordering.len(), 1);
let angle = &cos.function.parameters[&cos.function.parameter_ordering[0]];
assert_eq!(angle.name, "angle");
assert_eq!(angle.ty, FrozenTy::from(PrimitiveKind::F32));
assert_eq!(cos.function.return_ty, FrozenTy::from(PrimitiveKind::F32));
let succeed = by_name("succeed").expect("succeed joined the method index");
assert!(succeed.function.parameter_ordering.is_empty());
assert_eq!(
succeed.function.return_ty,
FrozenTy::from(PrimitiveKind::Boolean)
);
let add = by_name("add").expect("add joined the method index");
assert_eq!(add.function.parameter_ordering.len(), 2);
let optional_u64 = FrozenTy::FrozenOption(FrozenOption {
element: Box::new(FrozenTy::from(PrimitiveKind::U64)),
});
let window = by_name("window").expect("window joined the method index");
let parameter =
|index: usize| &window.function.parameters[&window.function.parameter_ordering[index]];
assert_eq!(parameter(0).name, "start_ns");
assert_eq!(parameter(0).ty, FrozenTy::from(PrimitiveKind::U64));
assert_eq!(parameter(1).name, "end_ns");
assert_eq!(parameter(1).ty, optional_u64);
assert_eq!(window.function.return_ty, optional_u64);
let greet = by_name("greet").expect("greet joined the method index");
let name = &greet.function.parameters[&greet.function.parameter_ordering[0]];
assert_eq!(
name.ty,
FrozenTy::FrozenOption(FrozenOption {
element: Box::new(FrozenTy::from(PrimitiveKind::String)),
})
);
}
#[test]
fn a_guest_export_is_described_as_its_module_record_declares_it() {
use arora_types::record::module::frozen::ExportKind;
let header = test_module_header();
let record = test_rust_wasm::test_rust_wasm::record(Uuid::nil());
assert_eq!(header.exports.len(), record.exports.len());
for export in &header.exports {
let low::ExportSymbol::Function(function) = export;
let ExportKind::Function(declared) = &record.exports[&function.id].kind;
assert_eq!(
module_discovery::guest_function_signature(function).as_ref(),
Some(declared),
"{}",
function.name
);
}
}
#[test]
fn invoke_calls_a_guest_function_with_an_optional_present_or_absent() {
use arora_bridge::client::method_info;
use arora_types::record::ty::{FrozenOption, FrozenTy, PrimitiveKind};
use arora_types::value::Type;
let mut arora = Arora::builder()
.with_module(test_module_header(), WASM.to_vec())
.build()
.expect("build a device with a loaded wasm module");
let caller = arora.caller();
let methods = caller_tests::settle(
&mut arora,
caller.describe_methods(Some("window".to_string())),
)
.expect("the device describes its methods");
assert_eq!(methods.len(), 1, "{methods:?}");
let optional_u64 = FrozenTy::FrozenOption(FrozenOption {
element: Box::new(FrozenTy::from(PrimitiveKind::U64)),
});
assert_eq!(methods[0].function.return_ty, optional_u64);
let info = method_info(&methods[0]);
let params: Vec<_> = info
.params
.iter()
.map(|p| (p.name.as_str(), p.param_type.clone(), p.required))
.collect();
assert_eq!(
params,
vec![
("start_ns", Type::U64, true),
("end_ns", Type::Option, false)
]
);
let mut window = |args: Vec<(&str, Value)>| {
let args = args
.into_iter()
.map(|(name, value)| (name.to_string(), value))
.collect();
caller_tests::settle(&mut arora, caller.invoke("window", args, None))
.expect("window answers")
};
let some = |value| Value::Option(Some(Box::new(Value::U64(value))));
assert_eq!(
window(vec![("start_ns", Value::U64(10)), ("end_ns", some(40))]),
Invoked::Returned(some(30))
);
assert_eq!(
window(vec![("start_ns", Value::U64(10))]),
Invoked::Returned(Value::Option(None))
);
assert_eq!(
window(vec![
("start_ns", Value::U64(10)),
("end_ns", Value::Option(None))
]),
Invoked::Returned(Value::Option(None))
);
assert_eq!(
window(vec![
("start_ns", Value::U64(10)),
("end_ns", Value::U64(40))
]),
Invoked::Returned(some(30)),
"a present optional may also be sent bare"
);
let mut greet = |args: HashMap<String, Value>| {
caller_tests::settle(&mut arora, caller.invoke("greet", args, None))
.expect("greet answers")
};
assert_eq!(
greet(HashMap::from([(
"name".to_string(),
Value::Option(Some(Box::new(Value::String("Ada".to_string()))))
)])),
Invoked::Returned(Value::String("hello, Ada".to_string()))
);
assert_eq!(
greet(HashMap::new()),
Invoked::Returned(Value::String("hello".to_string()))
);
}
#[test]
fn a_call_naming_no_module_is_refused() {
let mut arora = Arora::builder().build().expect("build the default device");
let err = arora
.call(Call {
module_id: None,
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect_err("a module-less call is refused");
assert!(err.to_string().contains("module id"), "{err}");
}
#[test]
fn call_loads_a_behavior_with_no_bridge() {
let mut arora = Arora::builder().build().expect("build the default device");
let result = arora
.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
))
.expect("the load call succeeds");
assert_eq!(result.ret, arora_types::value::Value::Unit);
}
#[tokio::test]
async fn a_call_is_enqueued_before_its_future_is_polled() {
let mut arora = Arora::builder().build().expect("build the default device");
let caller = arora.caller();
let call = caller.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
));
arora
.step(std::time::Duration::from_millis(10))
.expect("step");
let result = call.await.expect("the load call succeeds");
assert_eq!(result.ret, arora_types::value::Value::Unit);
}
#[tokio::test]
async fn a_failing_behavior_is_state_not_a_stop() {
struct Flaky {
failures_left: u32,
}
impl BehaviorInterpreter for Flaky {
fn tick(
&mut self,
_ctx: &mut arora_behavior::BehaviorContext,
) -> Result<arora_behavior::BehaviorStatus, arora_behavior::BehaviorError> {
if self.failures_left > 0 {
self.failures_left -= 1;
return Err(arora_behavior::BehaviorError {
message: "the input is not there yet".to_string(),
});
}
Ok(arora_behavior::BehaviorStatus::Running)
}
}
let mut arora = Arora::builder()
.with_behavior_interpreter(Box::new(Flaky { failures_left: 2 }))
.build()
.expect("build the device");
let mut errors = arora.behavior_error();
assert_eq!(*errors.borrow_and_update(), None);
let changes = arora.store.subscribe();
arora
.step(std::time::Duration::from_millis(10))
.expect("a failing behavior must not fail the step");
assert!(errors.has_changed().expect("the device is alive"));
assert_eq!(
errors.borrow_and_update().as_deref(),
Some("the input is not there yet"),
"the standing error is observable"
);
for _ in 0..3 {
arora
.step(std::time::Duration::from_millis(10))
.expect("a failing behavior must not fail the step");
}
assert_eq!(
*errors.borrow_and_update(),
None,
"recovery clears the standing error"
);
while let Some(change) = changes.try_recv() {
for key in change.set.keys() {
assert!(
key.path == arora_behavior::built_in::TIME
|| key.path == arora_behavior::built_in::DT,
"unexpected store write: {}",
key.path
);
}
assert!(change.unset.is_empty(), "unexpected store unset");
}
}
#[tokio::test]
async fn a_caller_call_lands_on_the_next_step() {
let mut arora = Arora::builder().build().expect("build the default device");
let caller = arora.caller();
let mut call = Box::pin(caller.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
)));
assert!(futures::poll!(call.as_mut()).is_pending());
arora
.step(std::time::Duration::from_millis(10))
.expect("step");
let result = call.await.expect("the load call succeeds");
assert_eq!(result.ret, arora_types::value::Value::Unit);
}
#[tokio::test]
async fn a_caller_reaches_a_running_device() {
use futures::FutureExt;
let mut arora = Arora::builder().build().expect("build the default device");
let caller = arora.caller();
let run = arora.run(std::time::Duration::from_millis(5));
let call = caller.call(interpreter_module::encode_load(
&arora_behavior::Graph::empty(),
));
futures::pin_mut!(run, call);
let outcome = tokio::time::timeout(std::time::Duration::from_secs(2), async {
futures::select! {
result = call.fuse() => result,
_ = run.fuse() => panic!("run ended before the call resolved"),
}
})
.await
.expect("the running device answers promptly");
assert_eq!(
outcome.expect("the load call succeeds").ret,
arora_types::value::Value::Unit
);
}
#[test]
fn engine_registers_and_dispatches_an_in_process_callable() {
use arora_types::call::Callable;
struct Answer;
impl Callable for Answer {
fn call(&self, _caller: &mut dyn CallBridge) -> Result<Value, CallError> {
Ok(Value::I32(42))
}
}
let mut arora = Arora::builder().build().expect("build the default device");
let id = arora.engine().arora_register_callable(Rc::new(Answer));
let result = arora
.engine()
.arora_call_indirect(&id)
.expect("the registered callable dispatches");
assert!(matches!(result, Value::I32(42)));
}
#[test]
fn builds_without_any_module() {
Arora::builder()
.build()
.expect("the default device builds with no modules loaded");
}
#[test]
fn two_guest_modules_with_one_id_fail_the_build() {
let first = test_module_header();
let mut second = test_module_header();
second.name = "test-rust-wasm-again".to_string();
let error = Arora::builder()
.with_module(first, WASM.to_vec())
.with_module(second, WASM.to_vec())
.build()
.err()
.expect("one id, two modules is refused");
assert!(
error
.to_string()
.contains("guest modules 'test-rust-wasm' and 'test-rust-wasm-again' both have id"),
"{error}"
);
}
#[test]
fn devices_share_one_compiled_module() {
let header = test_module_header();
let module_id = header.id;
let compiled = CompiledModule::new(header, WASM).expect("the test guest compiles");
let build = || {
Arora::builder()
.with_compiled_module(&compiled)
.build()
.expect("build a device from the compiled module")
};
let succeed = |arora: &mut Arora| {
arora
.call(Call {
module_id: Some(module_id),
id: Uuid::parse_str(SUCCEED).expect("valid uuid"),
args: Vec::new(),
})
.expect("call succeed() on the compiled module")
.ret
};
let first = build();
let mut second = build();
drop(first);
assert_eq!(succeed(&mut second), Value::Boolean(true));
let mut third = build();
assert_eq!(succeed(&mut third), Value::Boolean(true));
assert_eq!(succeed(&mut second), Value::Boolean(true));
}
#[test]
fn a_compiled_module_is_checked_as_a_module_given_as_bytes() {
let compiled =
CompiledModule::new(test_module_header(), WASM).expect("the test guest compiles");
let arora = Arora::builder()
.with_compiled_module(&compiled)
.build()
.expect("build a device from the compiled module");
assert!(
arora
.function_index
.values()
.any(|f| f.function_name == "cos"),
"the compiled module's exports join the method index"
);
let mut again = test_module_header();
again.name = "test-rust-wasm-again".to_string();
let error = Arora::builder()
.with_compiled_module(&compiled)
.with_module(again, WASM.to_vec())
.build()
.err()
.expect("one id, two modules is refused");
assert!(
error
.to_string()
.contains("guest modules 'test-rust-wasm' and 'test-rust-wasm-again' both have id"),
"{error}"
);
}
#[test]
fn a_compiled_module_loads_on_another_thread() {
let compiled =
CompiledModule::new(test_module_header(), WASM).expect("the test guest compiles");
std::thread::spawn(move || {
Arora::builder()
.with_compiled_module(&compiled)
.build()
.expect("build a device from the compiled module");
})
.join()
.expect("the device builds on its own thread");
}
#[test]
fn a_header_declaring_a_missing_export_fails_the_build() {
let mut header = test_module_header();
let low::ExportSymbol::Function(mut missing) = header.exports[0].clone();
missing.id = Uuid::from_u128(0x278);
header.exports.push(low::ExportSymbol::Function(missing));
let error = Arora::builder()
.with_module(header, WASM.to_vec())
.build()
.err()
.expect("a missing export is refused");
assert!(
error.to_string().contains("failed to find function export"),
"{error}"
);
}
#[test]
fn a_malformed_executable_does_not_compile() {
let error = CompiledModule::new(test_module_header(), &[0xDE, 0xAD, 0xBE, 0xEF])
.expect_err("not a valid wasm binary");
assert!(
matches!(
error,
arora_engine::executor::LoadModuleError::MalformedExecutable
),
"{error}"
);
}
#[test]
fn a_module_that_fails_to_load_fails_the_build() {
let header = test_module_header();
let error = Arora::builder()
.with_module(header, vec![0xDE, 0xAD, 0xBE, 0xEF]) .build()
.err()
.expect("build must fail when a module's executable cannot load");
assert!(
error.to_string().contains("module 'test-rust-wasm'"),
"{error}"
);
}
}
#[cfg(test)]
mod host_module_tests {
use super::*;
use arora_types::call::Call;
use arora_types::record::module::frozen;
use arora_types::value::Value;
#[test]
fn with_host_module_registers_a_native_module_reachable_through_call() {
use arora_types::call::CallResult;
use arora_types::value::StructureField;
let module_id = Uuid::from_u128(0xA11CE);
let echo = Uuid::from_u128(0xEC40);
let module = ModuleBuilder::new(module_id)
.function(echo, |call| {
let ret = call
.args
.into_iter()
.next()
.map(|field| *field.value)
.unwrap_or(Value::Unit);
Ok(CallResult {
ret,
mutated: Vec::new(),
})
})
.build();
let mut arora = Arora::builder()
.with_host_module(module)
.build()
.expect("build a device with a host module");
let result = arora
.call(Call {
module_id: Some(module_id),
id: echo,
args: vec![StructureField {
id: Uuid::from_u128(1),
value: Box::new(Value::U32(42)),
}],
})
.expect("call echo() on the host module");
assert_eq!(result.ret, Value::U32(42));
}
#[test]
fn described_host_functions_join_the_method_index() {
use arora_types::call::CallResult;
use arora_types::record::module::frozen::{Function, Parameter};
use arora_types::record::ty::{FrozenTy, PrimitiveKind};
use std::collections::HashMap;
let module_id = Uuid::from_u128(0x6761);
let described = Uuid::from_u128(0x6c61);
let undescribed = Uuid::from_u128(0x6c62);
let param = Uuid::from_u128(0x7801);
let signature = Function {
parameters: HashMap::from([(
param,
Parameter {
name: "x".to_string(),
ty: FrozenTy::from(PrimitiveKind::F64),
mutable: false,
},
)]),
parameter_ordering: vec![param],
return_ty: FrozenTy::from(PrimitiveKind::Unit),
};
let unit = |_call: Call| {
Ok(CallResult {
ret: Value::Unit,
mutated: Vec::new(),
})
};
let module = ModuleBuilder::new(module_id)
.described_function(described, "look_at", signature.clone(), unit)
.function(undescribed, unit)
.build();
let arora = Arora::builder()
.with_host_module(module)
.build()
.expect("build a device with a described host module");
let entry = arora
.function_index
.get(&described)
.expect("the described function is indexed");
assert_eq!(entry.module_id, module_id);
assert_eq!(entry.function_name, "look_at");
assert_eq!(entry.function, signature);
assert!(arora.function_index.get(&undescribed).is_none());
}
struct Describing;
const LOOK_AT: Uuid = Uuid::from_u128(0x6c6f6f6b);
impl BehaviorInterpreter for Describing {
fn tick(
&mut self,
_ctx: &mut arora_behavior::BehaviorContext,
) -> Result<arora_behavior::BehaviorStatus, arora_behavior::BehaviorError> {
Ok(arora_behavior::BehaviorStatus::Running)
}
fn described_methods(&self) -> HashMap<Uuid, frozen::Export> {
HashMap::from([(
LOOK_AT,
frozen::Export {
name: "look_at".to_string(),
kind: frozen::ExportKind::Function(unit_signature()),
},
)])
}
}
fn unit_signature() -> frozen::Function {
frozen::Function {
parameters: HashMap::new(),
parameter_ordering: Vec::new(),
return_ty: arora_types::record::ty::FrozenTy::from(
arora_types::record::ty::PrimitiveKind::Unit,
),
}
}
#[test]
fn the_interpreter_s_methods_join_the_method_index() {
let mut arora = Arora::builder()
.with_behavior_interpreter(Box::new(Describing))
.build()
.expect("build a device whose interpreter describes a method");
let entry = arora
.function_index
.get(&LOOK_AT)
.expect("the interpreter's method is indexed");
assert_eq!(entry.module_id, interpreter_module::ID);
assert_eq!(entry.function_name, "look_at");
assert_eq!(entry.function, unit_signature());
let error = arora
.call(Call {
module_id: Some(interpreter_module::ID),
id: LOOK_AT,
args: Vec::new(),
})
.expect_err("a task run is not called directly");
assert!(
error
.to_string()
.contains("spawn it through the interpreter module"),
"{error}"
);
}
#[test]
fn a_method_described_by_a_module_and_the_interpreter_fails_the_build() {
let module = ModuleBuilder::new(Uuid::from_u128(0x6761))
.described_function(LOOK_AT, "look_at", unit_signature(), |_call| {
Ok(CallResult {
ret: Value::Unit,
mutated: Vec::new(),
})
})
.build();
let error = Arora::builder()
.with_host_module(module)
.with_behavior_interpreter(Box::new(Describing))
.build()
.err()
.expect("the build is refused");
assert!(error.to_string().contains("described by module"), "{error}");
}
}
#[cfg(test)]
mod caller_tests {
use super::*;
use arora_behavior::Status;
use arora_types::data::Key;
use arora_types::record::module::frozen::{Function, Parameter};
use arora_types::record::ty::{FrozenScalar, FrozenTy, PrimitiveKind};
use arora_types::record::{FrozenReference, Version};
use std::pin::pin;
use std::task::{Context, Poll};
use std::time::Duration;
const TOOLS: Uuid = Uuid::from_u128(0x7001);
const DOUBLE: Uuid = Uuid::from_u128(0x7002);
const DOUBLE_X: Uuid = Uuid::from_u128(0x7003);
const WAVE: Uuid = Uuid::from_u128(0x7004);
const TOOLS_STOP: Uuid = Uuid::from_u128(0x7005);
const PLAYER: Uuid = Uuid::from_u128(0x7101);
const PLAYER_STOP: Uuid = Uuid::from_u128(0x7102);
pub(super) fn settle<T>(arora: &mut Arora, future: impl Future<Output = T>) -> T {
let mut future = pin!(future);
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
for _ in 0..8 {
arora.step(Duration::from_millis(10)).expect("step");
if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
return output;
}
}
panic!("no answer after eight steps");
}
fn signature(parameters: &[(Uuid, &str)], return_ty: FrozenTy) -> Function {
Function {
parameters: parameters
.iter()
.map(|(id, name)| {
(
*id,
Parameter {
name: name.to_string(),
ty: FrozenTy::from(PrimitiveKind::F64),
mutable: false,
},
)
})
.collect(),
parameter_ordering: parameters.iter().map(|(id, _)| *id).collect(),
return_ty,
}
}
fn status() -> FrozenTy {
FrozenTy::FrozenScalar(FrozenScalar {
reference: FrozenReference {
id: arora_behavior::STATUS_ENUMERATION_ID,
version: Version::parse("1.0.0").expect("a valid version"),
},
})
}
fn answer(ret: Value) -> Result<CallResult, CallError> {
Ok(CallResult {
ret,
mutated: Vec::new(),
})
}
fn device() -> Arora {
let tools = ModuleBuilder::new(TOOLS)
.described_function(
DOUBLE,
"double",
signature(&[(DOUBLE_X, "x")], FrozenTy::from(PrimitiveKind::F64)),
|call| match call.args.first().map(|field| field.value.as_ref()) {
Some(Value::F64(x)) => answer(Value::F64(2.0 * x)),
other => Err(generic(format!("double takes an f64, not {other:?}"))),
},
)
.described_function(WAVE, "wave", signature(&[], status()), |_call| {
answer(Status::Running.into())
})
.described_function(
TOOLS_STOP,
"stop",
signature(&[], FrozenTy::from(PrimitiveKind::Unit)),
|_call| answer(Value::String("tools stopped".to_string())),
)
.build();
let player = ModuleBuilder::new(PLAYER)
.described_function(
PLAYER_STOP,
"stop",
signature(&[], FrozenTy::from(PrimitiveKind::Unit)),
|_call| answer(Value::String("player stopped".to_string())),
)
.build();
Arora::builder()
.with_host_module(tools)
.with_host_module(player)
.build()
.expect("build the device")
}
#[test]
fn list_keys_reports_a_key_with_its_meta() {
let mut arora = device();
let caller = arora.caller();
let key = Key::from("face/mouth");
arora
.store()
.set_meta(HashMap::from([(
key.clone(),
KeyMeta::new().editable().in_unit("fraction"),
)]))
.expect("describe the key");
let keys = settle(&mut arora, caller.list_keys(Some("face/".to_string())))
.expect("the device lists its keys");
assert_eq!(keys.len(), 1, "{keys:?}");
let (path, meta) = &keys[0];
assert_eq!(path, "face/mouth");
assert!(meta.editable);
assert_eq!(meta.unit.as_deref(), Some("fraction"));
}
#[test]
fn describe_methods_lists_a_host_module_s_described_function() {
let mut arora = device();
let caller = arora.caller();
let methods = settle(&mut arora, caller.describe_methods(Some("dou".to_string())))
.expect("the device describes its methods");
assert_eq!(methods.len(), 1, "{methods:?}");
assert_eq!(methods[0].module_id, TOOLS);
assert_eq!(methods[0].id, DOUBLE);
assert_eq!(methods[0].name, "double");
assert_eq!(methods[0].function.parameter_ordering, vec![DOUBLE_X]);
}
#[test]
fn invoking_a_plain_function_returns_its_value() {
let mut arora = device();
let caller = arora.caller();
let invoked = settle(
&mut arora,
caller.invoke(
"double",
HashMap::from([("x".to_string(), Value::F64(2.5))]),
None,
),
)
.expect("double answers");
assert_eq!(invoked, Invoked::Returned(Value::F64(5.0)));
let error = settle(
&mut arora,
caller.invoke(
"double",
HashMap::from([("y".to_string(), Value::F64(2.5))]),
None,
),
)
.expect_err("double has no parameter y");
assert!(error.to_string().contains("'y'"), "{error}");
}
#[test]
fn invoking_a_task_shaped_method_starts_a_run_a_halt_ends() {
let mut arora = device();
let caller = arora.caller();
let Invoked::Started(handle) =
settle(&mut arora, caller.invoke("wave", HashMap::new(), None)).expect("wave starts")
else {
panic!("wave is task-shaped");
};
arora.step(Duration::from_millis(10)).expect("step");
let running: Value = Status::Running.into();
assert_eq!(
arora.store().read(std::slice::from_ref(&handle.status)),
vec![Some(running)]
);
settle(&mut arora, caller.halt(handle.id)).expect("the halt is applied");
arora.step(Duration::from_millis(10)).expect("step");
let failure: Value = Status::Failure.into();
assert_eq!(
arora.store().read(std::slice::from_ref(&handle.status)),
vec![Some(failure)],
"a halted run ends Failure"
);
}
#[test]
fn spawn_starts_a_run_from_a_call() {
let mut arora = device();
let caller = arora.caller();
let handle = settle(
&mut arora,
caller.spawn(Call {
module_id: Some(TOOLS),
id: WAVE,
args: Vec::new(),
}),
)
.expect("the run starts");
arora.step(Duration::from_millis(10)).expect("step");
let running: Value = Status::Running.into();
assert_eq!(
arora.store().read(std::slice::from_ref(&handle.status)),
vec![Some(running)]
);
}
#[test]
fn invoking_a_shared_name_needs_its_module() {
let mut arora = device();
let caller = arora.caller();
let error = settle(&mut arora, caller.invoke("stop", HashMap::new(), None))
.expect_err("stop is ambiguous");
let message = error.to_string();
assert!(message.contains(&TOOLS.to_string()), "{message}");
assert!(message.contains(&PLAYER.to_string()), "{message}");
let invoked = settle(
&mut arora,
caller.invoke("stop", HashMap::new(), Some(PLAYER)),
)
.expect("the module disambiguates");
assert_eq!(
invoked,
Invoked::Returned(Value::String("player stopped".to_string()))
);
}
}