#![cfg(feature = "noxu")]
use std::sync::Arc;
use dyniak::datastore::NoxuDatastore;
use dyniak::mapreduce::{
builtins::default_registry, run_job_full, Inputs, KeyDatum, MapReduceJob, Phase, PhaseRegistry,
};
use dyniak::proto::http::object::{HttpLink, HttpObject};
use dynomite::embed::Datastore;
use tempfile::TempDir;
fn registry() -> Arc<PhaseRegistry> {
Arc::new(default_registry())
}
fn put_with_links(store: &NoxuDatastore, bucket: &[u8], key: &[u8], links: Vec<HttpLink>) {
let obj = HttpObject {
value: b"src".to_vec(),
content_type: None,
indexes: Vec::new(),
links,
};
store
.put_object(bucket, key, &obj.to_storage_bytes(), &[])
.expect("put");
}
fn link(bucket: &str, key: &str, tag: &str) -> HttpLink {
HttpLink {
bucket: bucket.to_string(),
key: key.to_string(),
tag: tag.to_string(),
}
}
fn targets(out: &[dyniak::mapreduce::PhaseOutput]) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = out
.iter()
.map(|o| {
(
o.value["bucket"].as_str().expect("bucket").to_string(),
o.value["key"].as_str().expect("key").to_string(),
)
})
.collect();
v.sort();
v
}
fn seed(store: &NoxuDatastore) {
put_with_links(
store,
b"people",
b"a",
vec![
link("people", "b", "friend"),
link("people", "c", "friend"),
link("people", "d", "other"),
],
);
}
#[tokio::test]
async fn link_phase_filters_by_tag() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
seed(&store);
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![Phase::Link {
bucket: Some("people".into()),
tag: Some("friend".into()),
keep: true,
}],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("ok");
assert_eq!(
targets(&out),
vec![
("people".to_string(), "b".to_string()),
("people".to_string(), "c".to_string()),
],
"tag=friend matches exactly B and C"
);
}
#[tokio::test]
async fn link_phase_wildcard_tag_includes_all() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
seed(&store);
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![Phase::Link {
bucket: Some("people".into()),
tag: None,
keep: true,
}],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("ok");
assert_eq!(
targets(&out),
vec![
("people".to_string(), "b".to_string()),
("people".to_string(), "c".to_string()),
("people".to_string(), "d".to_string()),
],
"tag=None (wildcard) includes the 'other'-tagged D"
);
}
#[tokio::test]
async fn link_phase_filters_by_bucket() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
put_with_links(
&store,
b"people",
b"a",
vec![link("people", "b", "x"), link("work", "acme", "x")],
);
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![Phase::Link {
bucket: Some("work".into()),
tag: None,
keep: true,
}],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("ok");
assert_eq!(
targets(&out),
vec![("work".to_string(), "acme".to_string())],
"bucket=work selects only the work link"
);
}
#[tokio::test]
async fn link_phase_over_missing_object_yields_empty() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "ghost")]),
phases: vec![Phase::Link {
bucket: None,
tag: None,
keep: true,
}],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("missing object is not an error");
assert!(out.is_empty(), "a missing object contributes no links");
}
#[tokio::test]
async fn link_phase_feeds_following_reduce() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
seed(&store);
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![
Phase::Link {
bucket: None,
tag: Some("friend".into()),
keep: false,
},
Phase::Reduce {
fn_name: "reduce_count".into(),
arg: None,
keep: true,
},
],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("ok");
assert_eq!(out.len(), 1, "only the reduce phase keeps");
assert_eq!(
out[0].value,
serde_json::json!(2u64),
"two friend links flowed into the reduce"
);
}
#[tokio::test]
async fn link_phase_keep_is_honored() {
let dir = TempDir::new().expect("tempdir");
let store = NoxuDatastore::open_in(dir.path()).expect("open noxu");
seed(&store);
let ds: Arc<dyn Datastore> = Arc::new(store);
let job = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![
Phase::Link {
bucket: None,
tag: Some("friend".into()),
keep: false,
},
Phase::Reduce {
fn_name: "reduce_count".into(),
arg: None,
keep: true,
},
],
timeout_ms: None,
};
let out = run_job_full(job, registry(), None, Some(ds))
.await
.expect("ok");
assert!(
out.iter().all(|o| o.phase == 1),
"link phase (0) output suppressed by keep=false"
);
let dir2 = TempDir::new().expect("tempdir");
let store2 = NoxuDatastore::open_in(dir2.path()).expect("open noxu");
seed(&store2);
let ds2: Arc<dyn Datastore> = Arc::new(store2);
let job2 = MapReduceJob {
inputs: Inputs::KeyData(vec![KeyDatum::pair("people", "a")]),
phases: vec![
Phase::Link {
bucket: None,
tag: Some("friend".into()),
keep: true,
},
Phase::Reduce {
fn_name: "reduce_count".into(),
arg: None,
keep: true,
},
],
timeout_ms: None,
};
let out2 = run_job_full(job2, registry(), None, Some(ds2))
.await
.expect("ok");
assert!(
out2.iter().any(|o| o.phase == 0),
"link phase output kept with keep=true"
);
}