#![cfg(target_arch = "wasm32")]
use arora::{Arora, ModuleBuilder};
use arora_behavior::Status;
use arora_types::call::{CallError, CallResult};
use arora_types::data::{DataStore, Key, StateChange};
use arora_types::module::low::Header;
use arora_types::record::module::frozen::{Function, Parameter};
use arora_types::record::ty::{FrozenScalar, FrozenTy, PrimitiveKind};
use arora_types::record::{FrozenReference, Version};
use arora_types::value::Value;
use arora_web::{AroraWeb, AroraWebBuilder, CompiledModule, Engine};
use uuid::Uuid;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
const WASM_BYTES: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_TEST_RUST_WASM_test_rust_wasm"));
const PING_FN_ID: &str = "5f423ba9-d5f9-46d7-a9b5-fb7d28f99ea6";
fn header() -> Header {
test_rust_wasm::test_rust_wasm::header(arora_types::module::low::Executor {
name: "wasm".to_string(),
min_version: None,
max_version: None,
})
}
fn header_json() -> String {
serde_json::to_string(&header()).expect("the declared header serializes to json")
}
#[wasm_bindgen_test]
fn load_and_ping_test_rust_wasm() {
let header_json = header_json();
let mut engine = Engine::new();
let module_id: String = engine
.load_module(&header_json, WASM_BYTES)
.map_err(jsval_to_string)
.expect("loadModule succeeded");
assert_eq!(module_id, header().id.to_string());
let call_json = format!(r#"{{"id":"{PING_FN_ID}","args":[]}}"#);
let result = engine
.call(&call_json)
.map_err(jsval_to_string)
.expect("call(ping) succeeded");
assert!(!result.is_empty(), "result was empty");
}
#[wasm_bindgen_test]
async fn prepare_load_and_ping_test_rust_wasm() {
let header_json = header_json();
let mut engine = Engine::new();
wasm_bindgen_futures::JsFuture::from(engine.prepare_module(&header_json, WASM_BYTES.to_vec()))
.await
.expect("prepareModule succeeded");
let module_id: String = engine
.load_prepared_module(&header_json)
.map_err(jsval_to_string)
.expect("loadPreparedModule succeeded");
assert_eq!(module_id, header().id.to_string());
let call_json = format!(r#"{{"id":"{PING_FN_ID}","args":[]}}"#);
let result = engine
.call(&call_json)
.map_err(jsval_to_string)
.expect("call(ping) succeeded");
assert!(!result.is_empty(), "result was empty");
}
#[wasm_bindgen_test]
async fn devices_share_one_compiled_module() {
let compiled = CompiledModule::new(&header_json(), WASM_BYTES)
.map_err(jsval_to_string)
.expect("the test guest compiles");
let call_json = format!(
r#"{{"module_id":"{}","id":"{PING_FN_ID}","args":[]}}"#,
header().id
);
for _ in 0..2 {
let mut builder = AroraWebBuilder::new();
builder.with_compiled_module(&compiled);
let web = builder.build().map_err(jsval_to_string).expect("build");
let pinged = web.call(&call_json);
web.step(10.0).expect("step");
JsFuture::from(pinged)
.await
.map_err(jsval_to_string)
.expect("ping answers");
}
}
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);
fn answer(ret: Value) -> Result<CallResult, CallError> {
Ok(CallResult {
ret,
mutated: Vec::new(),
})
}
fn device() -> AroraWeb {
let signature = |parameters: &[(Uuid, &str)], return_ty: FrozenTy| 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,
};
let status = FrozenTy::FrozenScalar(FrozenScalar {
reference: FrozenReference {
id: arora_behavior::STATUS_ENUMERATION_ID,
version: Version::parse("1.0.0").expect("a valid version"),
},
});
let unit = || FrozenTy::from(PrimitiveKind::Unit);
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)),
_ => Err(CallError::Generic {
message: "double takes an f64".to_string(),
}),
},
)
.described_function(WAVE, "wave", signature(&[], status), |_call| {
answer(Status::Running.into())
})
.described_function(TOOLS_STOP, "stop", signature(&[], unit()), |_call| {
answer(Value::Unit)
})
.build();
let player = ModuleBuilder::new(PLAYER)
.described_function(PLAYER_STOP, "stop", signature(&[], unit()), |_call| {
answer(Value::String("player stopped".to_string()))
})
.build();
AroraWeb::from(
Arora::builder()
.with_host_module(tools)
.with_host_module(player)
.build()
.expect("build the device"),
)
}
async fn yield_to_tasks() {
for _ in 0..4 {
JsFuture::from(js_sys::Promise::resolve(&JsValue::NULL))
.await
.expect("a resolved promise");
}
}
fn json(value: JsValue) -> serde_json::Value {
serde_wasm_bindgen::from_value(value).expect("a JSON-shaped answer")
}
fn status_json(status: Status) -> serde_json::Value {
serde_json::to_value(Value::from(status)).expect("a status serializes")
}
#[wasm_bindgen_test]
async fn an_operation_is_applied_by_a_step_in_the_same_turn() {
let web = device();
web.set_value("face/mouth", r#"{"f64": 0.5}"#)
.expect("write a key");
let keys = web.list_keys(Some("face/".to_string()));
let methods = web.describe_methods(Some("dou".to_string()));
web.step(10.0).expect("step");
let keys = json(JsFuture::from(keys).await.expect("listKeys resolves"));
assert_eq!(keys[0]["path"], "face/mouth", "{keys}");
assert!(keys[0]["__meta"].is_object(), "{keys}");
assert_eq!(keys.as_array().map(Vec::len), Some(1), "{keys}");
let methods = json(
JsFuture::from(methods)
.await
.expect("describeMethods resolves"),
);
assert_eq!(methods[0]["path"], "double", "{methods}");
assert_eq!(methods[0]["module"], TOOLS.to_string(), "{methods}");
assert_eq!(methods[0]["params"][0]["name"], "x", "{methods}");
assert_eq!(methods[0]["task"], false, "{methods}");
}
#[wasm_bindgen_test]
async fn invoke_answers_a_value_or_starts_a_run() {
let web = device();
let doubled = web.invoke("double", r#"{"x": {"f64": 2.5}}"#, None);
web.step(10.0).expect("step");
yield_to_tasks().await;
web.step(10.0).expect("step");
let doubled = json(JsFuture::from(doubled).await.expect("double answers"));
assert_eq!(doubled["f64"].as_f64(), Some(5.0), "{doubled}");
let started = web.invoke("wave", "{}", None);
web.step(10.0).expect("step");
yield_to_tasks().await;
web.step(10.0).expect("step");
let handle = json(JsFuture::from(started).await.expect("wave starts"));
let run = handle["run"]
.as_str()
.unwrap_or_else(|| panic!("the handle names its run: {handle}"))
.to_string();
assert!(Uuid::parse_str(&run).is_ok(), "{handle}");
let status_key = handle["status"]
.as_str()
.unwrap_or_else(|| panic!("the handle names its status key: {handle}"))
.to_string();
for keys in ["feedback", "result", "update"] {
assert!(handle[keys].is_array(), "{keys} is a path array: {handle}");
}
web.step(10.0).expect("step");
let read = |web: &AroraWeb| {
let paths = serde_wasm_bindgen::to_value(&vec![status_key.clone()]).expect("paths");
json(web.read_values(paths).expect("read the status"))[&status_key].clone()
};
assert_eq!(read(&web), status_json(Status::Running));
let halted = web.halt(&run);
web.step(10.0).expect("step");
JsFuture::from(halted).await.expect("the halt is applied");
web.step(10.0).expect("step");
assert_eq!(
read(&web),
status_json(Status::Failure),
"a halted run ends Failure"
);
}
#[wasm_bindgen_test]
async fn invoke_of_a_shared_name_needs_its_module() {
let web = device();
let ambiguous = web.invoke("stop", "{}", None);
web.step(10.0).expect("step");
let error = jsval_to_string(
JsFuture::from(ambiguous)
.await
.expect_err("stop is ambiguous"),
);
assert!(error.contains(&TOOLS.to_string()), "{error}");
assert!(error.contains(&PLAYER.to_string()), "{error}");
let chosen = web.invoke("stop", "{}", Some(PLAYER.to_string()));
web.step(10.0).expect("step");
yield_to_tasks().await;
web.step(10.0).expect("step");
let stopped = json(
JsFuture::from(chosen)
.await
.expect("the module disambiguates"),
);
assert_eq!(stopped, serde_json::json!({"str": "player stopped"}));
}
fn device_and_store() -> (AroraWeb, Box<dyn DataStore>) {
let arora = Arora::builder().build().expect("build the device");
let store = arora.store().clone_box();
(AroraWeb::from(arora), store)
}
fn value_json(value: Value) -> serde_json::Value {
serde_json::to_value(value).expect("a value serializes")
}
#[wasm_bindgen_test]
fn a_drain_holds_the_net_change_since_the_previous_one() {
let (web, store) = device_and_store();
web.drain_changes().expect("the first drain");
let set = |path: &str, value: f64| {
store
.write(StateChange::set(path, Value::F64(value)))
.expect("set a key");
};
set("face/mouth", 0.25);
set("face/brow", 1.5);
web.step(10.0).expect("step");
set("face/mouth", 0.5);
set("face/brow", 2.5);
web.step(10.0).expect("step");
let mut unset = StateChange::new();
unset.unset.insert(Key::new("face/brow"));
store.write(unset).expect("unset a key");
web.step(10.0).expect("step");
let drained = json(web.drain_changes().expect("drain"));
assert_eq!(
drained["face/mouth"],
value_json(Value::F64(0.5)),
"{drained}"
);
assert_eq!(drained["face/brow"], serde_json::Value::Null, "{drained}");
let again = json(web.drain_changes().expect("drain again"));
assert_eq!(
again,
serde_json::json!({}),
"nothing changed since: {again}"
);
}
#[wasm_bindgen_test]
fn the_first_drain_holds_the_full_state() {
let arora = Arora::builder().build().expect("build the device");
let mut before = StateChange::new();
before
.set
.insert(Key::new("face/mouth"), Some(Value::F64(0.25)));
before
.set
.insert(Key::new("face/brow"), Some(Value::F64(1.5)));
arora.store().write(before).expect("write before wrapping");
let web = AroraWeb::from(arora);
for _ in 0..3 {
web.step(10.0).expect("step");
}
let drained = json(web.drain_changes().expect("the first drain"));
let snapshot = json(web.snapshot().expect("snapshot"));
assert_eq!(drained, snapshot, "the first drain is the store's state");
assert_eq!(
drained["face/mouth"],
value_json(Value::F64(0.25)),
"{drained}"
);
assert_eq!(
drained["face/brow"],
value_json(Value::F64(1.5)),
"{drained}"
);
}
fn jsval_to_string(v: JsValue) -> String {
v.as_string().unwrap_or_else(|| format!("{v:?}"))
}