use std::time::Duration;
use zenkey::qos::QosProfile;
use zenkey_fleet::report::ExpectVerdict;
use zenkey_fleet::{ExpectSpec, QosCheck, declare_publication, run_expect};
mod util;
use util::peer_pair;
const KEY: &str = "v1/h-cccccccccccc/state/demo/health";
fn spec(selector: &str, within_s: f64) -> ExpectSpec {
ExpectSpec {
selector: selector.to_string(),
within: Duration::from_secs_f64(within_s),
..ExpectSpec::default()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn presence_meets_early_and_a_clean_shortfall_is_not_met() {
let (a, b) = peer_pair().await;
let slices = zenkey_fleet::SliceSet::default();
let publication = declare_publication(&a, KEY, QosProfile::Transition, None)
.await
.expect("declare");
let matching = publication.matching_events().await.expect("events");
let expect = tokio::spawn({
let b = b.clone();
async move {
let slices = zenkey_fleet::SliceSet::default();
run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&store_of(),
&spec(KEY, 10.0),
)
.await
}
});
assert!(
tokio::time::timeout(util::SETTLE, matching.recv())
.await
.expect("matching within 5s")
.expect("listener alive")
);
publication.send(b"{}".to_vec(), None).await.expect("send");
let report = expect.await.expect("join").expect("run");
assert_eq!(report.verdict, ExpectVerdict::Met);
assert!(report.ended_early, "one sample satisfies the default count");
assert_eq!(report.samples, 1);
assert_eq!(report.dropped, 0);
let shortfall = ExpectSpec {
count: Some(3),
..spec(KEY, 1.0)
};
let report = run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&store_of(),
&shortfall,
)
.await
.expect("run");
assert_eq!(report.verdict, ExpectVerdict::NotMet);
assert!(
report.unmet.iter().any(|u| u.contains("3 required")),
"{:?}",
report.unmet
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn absence_is_scoped_clean_and_conclusively_breakable() {
let (a, b) = peer_pair().await;
let slices = zenkey_fleet::SliceSet::default();
let quiet = ExpectSpec {
absent: true,
..spec("v1/*/state/demo/retired", 1.0)
};
let report = run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&store_of(),
&quiet,
)
.await
.expect("run");
assert_eq!(report.verdict, ExpectVerdict::Met);
assert_eq!(report.samples, 0);
assert_eq!(report.dropped, 0, "the claim states its observer was clean");
let publication = declare_publication(&a, KEY, QosProfile::Transition, None)
.await
.expect("declare");
let matching = publication.matching_events().await.expect("events");
let broken = ExpectSpec {
absent: true,
..spec(KEY, 5.0)
};
let expect = tokio::spawn({
let b = b.clone();
async move {
let slices = zenkey_fleet::SliceSet::default();
run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&store_of(),
&broken,
)
.await
}
});
assert!(
tokio::time::timeout(util::SETTLE, matching.recv())
.await
.expect("matching within 5s")
.expect("listener alive")
);
publication.send(b"{}".to_vec(), None).await.expect("send");
let report = expect.await.expect("join").expect("run");
assert_eq!(report.verdict, ExpectVerdict::NotMet);
assert_eq!(report.violations_total, 1);
assert!(
report.violations[0].contains("none may be"),
"{:?}",
report.violations
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn declared_qos_is_checked_per_sample() {
let slice = zenkey::parse_slice(
r#"
[registry]
version = "1.0"
app = "demo"
convention = 1
[producer]
name = "demo"
[[subject]]
path = "health"
class = "state"
type = "Health"
qos = "transition"
"#,
)
.expect("fixture slice parses");
let slices = zenkey_fleet::SliceSet::from_slices(vec![slice]);
let run_with = |qos_profile: QosProfile, slices: zenkey_fleet::SliceSet| async move {
let (a, b) = peer_pair().await;
let publication = declare_publication(&a, KEY, qos_profile, None)
.await
.expect("declare");
let matching = publication.matching_events().await.expect("events");
let spec = ExpectSpec {
qos: Some(QosCheck::Declared),
..spec(KEY, 5.0)
};
let expect = tokio::spawn({
let b = b.clone();
async move {
run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&store_of(),
&spec,
)
.await
}
});
assert!(
tokio::time::timeout(util::SETTLE, matching.recv())
.await
.expect("matching within 5s")
.expect("listener alive")
);
publication.send(b"{}".to_vec(), None).await.expect("send");
expect.await.expect("join").expect("run")
};
let report = run_with(QosProfile::Transition, slices.clone()).await;
assert_eq!(report.verdict, ExpectVerdict::Met, "{:?}", report.unmet);
let report = run_with(QosProfile::Sampled, slices).await;
assert_eq!(report.verdict, ExpectVerdict::NotMet);
assert_eq!(
report.violations.len(),
1,
"expected one QoS violation; an empty list means the window observed \
no samples, which is a different failure: unmet={:?}",
report.unmet
);
assert!(
report.violations[0].contains("did not ride transition"),
"{:?}",
report.violations
);
}
fn store_of() -> zenkey_fleet::model::decode::SchemaStore {
zenkey_fleet::model::decode::SchemaStore::new("", Duration::from_millis(300))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cold_schema_store_costs_the_window_nothing() {
const BURSTS: usize = 12;
const PER_BURST: usize = 120;
const SAMPLES: u64 = (BURSTS * PER_BURST) as u64;
let slice = zenkey::parse_slice(
r#"
[registry]
version = "1.0"
app = "demo"
convention = 1
[producer]
name = "demo"
[[subject]]
path = "health"
class = "state"
type = "Health"
"#,
)
.expect("fixture slice parses");
let slices = zenkey_fleet::SliceSet::from_slices(vec![slice]);
let (a, b) = peer_pair().await;
let stuck: std::sync::Arc<std::sync::Mutex<Vec<zenoh::query::Query>>> =
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let _describe = a
.declare_queryable("v1/h-cccccccccccc/@rpc/demo/describe")
.callback({
let stuck = std::sync::Arc::clone(&stuck);
move |q| stuck.lock().expect("stuck lock").push(q)
})
.await
.expect("describe queryable");
let publication = declare_publication(&a, KEY, QosProfile::Transition, None)
.await
.expect("declare");
let matching = publication.matching_events().await.expect("events");
let expect = tokio::spawn({
let b = b.clone();
async move {
let spec = ExpectSpec {
valid_payload: true,
count: Some(SAMPLES),
..spec(KEY, 4.0)
};
run_expect(
&zenkey_fleet::Fleet::new(&b, ""),
Some(&slices),
&zenkey_fleet::model::decode::SchemaStore::new("", Duration::from_millis(800)),
&spec,
)
.await
}
});
assert!(
tokio::time::timeout(Duration::from_secs(10), matching.recv())
.await
.expect("matching within 10s")
.expect("listener alive")
);
for _ in 0..BURSTS {
for _ in 0..PER_BURST {
publication.send(b"{}".to_vec(), None).await.expect("send");
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
let report = expect.await.expect("join").expect("run");
assert_eq!(
report.dropped, 0,
"the observer lost samples to its own schema fetch"
);
assert_eq!(report.samples, SAMPLES, "the whole burst was observed");
assert_eq!(report.verdict, ExpectVerdict::NotMet, "no schema is served");
assert!(
report.violations[0].contains("validity unknowable"),
"{:?}",
report.violations
);
}