#![cfg(feature = "wasm")]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use dyniak::datatypes::keyfun::KeyFun;
use dyniak::datatypes::keyfun_wasm::WasmKeyfunStore;
use dyniak::mapreduce::{
builtins::default_registry, run_job_with_wasm, Inputs, KeyDatum, MapReduceJob, Phase, WasmHook,
WasmModuleStore,
};
use dyniak::replication::{ReplicationStrategy, RingPoint, RingView};
use dyniak::router::BucketRouter;
use dyniak::{BucketProps, BucketPropsRegistry};
use dynomite::hashkit::{hash64, HashType};
fn fixture_manifest(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
.join("Cargo.toml")
}
fn build_fixture(name: &str, crate_lib: &str) -> Option<PathBuf> {
let manifest = fixture_manifest(name);
if !manifest.exists() {
eprintln!("SKIP: fixture manifest missing at {}", manifest.display());
return None;
}
let probe = Command::new(env!("CARGO"))
.args(["build", "--release", "--target", "wasm32-unknown-unknown"])
.arg("--manifest-path")
.arg(&manifest)
.env("CARGO_TARGET_DIR", scratch_target(name))
.output();
let out = match probe {
Ok(o) => o,
Err(e) => {
eprintln!("SKIP: could not invoke cargo to build {name}: {e}");
return None;
}
};
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("wasm32-unknown-unknown")
&& (stderr.contains("target may not be installed")
|| stderr.contains("is not installed")
|| stderr.contains("can't find crate for `core`"))
{
eprintln!("SKIP: wasm32-unknown-unknown target unavailable:\n{stderr}");
return None;
}
panic!("fixture build failed for {name}:\n{stderr}");
}
let wasm = scratch_target(name)
.join("wasm32-unknown-unknown/release")
.join(format!("{crate_lib}.wasm"));
if !wasm.exists() {
eprintln!("SKIP: built ok but no .wasm at {}", wasm.display());
return None;
}
Some(wasm)
}
fn scratch_target(name: &str) -> PathBuf {
let base = if Path::new("/scratch").is_dir() {
PathBuf::from("/scratch")
} else {
std::env::temp_dir()
};
base.join("dyniak-wasm-fixtures").join(name)
}
fn five_peer_ring() -> Arc<RingView> {
let span = u64::from(u32::MAX);
let pts: Vec<RingPoint> = (0..5u32)
.map(|i| RingPoint::new(u64::from(i) * span / 5, i, "dc1", "r1"))
.collect();
Arc::new(RingView::new(pts))
}
#[test]
fn real_rust_wasm_custom_keyfun_routes_live_keys() {
let Some(wasm) = build_fixture("keyfun-reverse", "keyfun_reverse") else {
return;
};
let bytes = std::fs::read(&wasm).expect("read compiled keyfun wasm");
let store = WasmKeyfunStore::new().expect("keyfun store");
store.register("reverse", &bytes).expect("register wasm");
assert!(store.contains("reverse"));
let registry = Arc::new(BucketPropsRegistry::new_riak_defaults());
registry.set(
b"default",
b"shaped",
BucketProps {
keyfun: Some(KeyFun::Custom("reverse".to_string())),
strategy: Some(ReplicationStrategy::Successors),
n_val: Some(1),
custom_keyfun_module: Some("reverse".to_string()),
..BucketProps::default()
},
);
registry.set(
b"default",
b"plain",
BucketProps {
keyfun: Some(KeyFun::Std),
strategy: Some(ReplicationStrategy::Successors),
n_val: Some(1),
..BucketProps::default()
},
);
let hash = HashType::Murmur;
let router = BucketRouter::new(registry, five_peer_ring(), hash).with_keyfun_store(store);
for (bucket, key, expected) in [
(&b"shaped"[..], &b"alice"[..], &b"shaped:ecila"[..]),
(&b"shaped"[..], &b"bob"[..], &b"shaped:bob"[..]),
(&b"shaped"[..], &b"12345"[..], &b"shaped:54321"[..]),
] {
let decision = router
.try_route(b"default", bucket, key)
.expect("custom keyfun routes");
assert_eq!(
decision.route_bytes, expected,
"fixture rule <bucket>:<reversed key> for key {key:?}"
);
let want_hash = hash64(hash, expected);
assert_eq!(decision.key_hash, want_hash, "hash of route bytes");
}
let custom = router.try_route(b"default", b"shaped", b"alice").unwrap();
let std_dec = router.try_route(b"default", b"plain", b"alice").unwrap();
assert_eq!(custom.route_bytes, b"shaped:ecila");
assert_eq!(std_dec.route_bytes, b"plain/alice");
assert_ne!(
custom.key_hash, std_dec.key_hash,
"custom and std keyfuns must shape different hash inputs"
);
}
#[test]
fn real_rust_wasm_custom_keyfun_negative_paths() {
let Some(wasm) = build_fixture("keyfun-reverse", "keyfun_reverse") else {
return;
};
let bytes = std::fs::read(&wasm).expect("read wasm");
let store = WasmKeyfunStore::new().expect("store");
store.register("reverse", &bytes).expect("register");
let registry = Arc::new(BucketPropsRegistry::new_riak_defaults());
registry.set(
b"default",
b"bad",
BucketProps {
keyfun: Some(KeyFun::Custom("missing".to_string())),
strategy: Some(ReplicationStrategy::Successors),
n_val: Some(1),
custom_keyfun_module: Some("missing".to_string()),
..BucketProps::default()
},
);
let router =
BucketRouter::new(registry, five_peer_ring(), HashType::Murmur).with_keyfun_store(store);
let err = router
.try_route(b"default", b"bad", b"k")
.expect_err("unregistered module errors cleanly");
assert!(
matches!(err, dyniak::datatypes::keyfun::KeyFunError::ModuleNotFound(ref s) if s == "missing"),
"got {err:?}"
);
}
#[tokio::test]
async fn real_rust_wasm_mapreduce_phase_doubles_values() {
let Some(wasm) = build_fixture("mapreduce-double", "mapreduce_double") else {
return;
};
let bytes = std::fs::read(&wasm).expect("read compiled mapreduce wasm");
let store = WasmModuleStore::new().expect("module store");
store.register("double", &bytes).expect("register wasm");
let hook: Arc<dyn WasmHook> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![
KeyDatum::with_value("b", "k1", serde_json::json!(10)),
KeyDatum::with_value("b", "k2", serde_json::json!(21)),
KeyDatum::with_value("b", "k3", serde_json::json!(0)),
]),
phases: vec![Phase::WasmModule {
module_id: "double".into(),
fn_name: "apply".into(),
arg: None,
keep: true,
}],
timeout_ms: None,
};
let out = run_job_with_wasm(job, Arc::new(default_registry()), Some(hook))
.await
.expect("mapreduce job ok");
let mut got: Vec<i64> = out
.into_iter()
.map(|o| o.value.as_i64().expect("doubled number"))
.collect();
got.sort_unstable();
assert_eq!(got, vec![0, 20, 42], "value * 2 for each input");
}