use serde_json::{Value, json};
use trusty_common::memory_rpc::MemoryHealthStatus;
use super::MemoryReachability;
use crate::uds_mock::RpcError;
async fn daemon_answering(body: Value) -> crate::uds_mock::MockUdsDaemon {
crate::uds_mock::spawn(move |_method: &str, _params: Value| {
let body = body.clone();
Box::pin(async move { Ok(body) })
})
.await
}
async fn answering_daemon() -> crate::uds_mock::MockUdsDaemon {
daemon_answering(json!({ "status": "ok" })).await
}
async fn probe_body(body: Value) -> MemoryReachability {
let daemon = daemon_answering(body).await;
super::probe_socket_reachability(daemon.socket()).await
}
fn blocking_reachable(socket: &std::path::Path) -> bool {
super::block_on_probe(async {
super::probe_socket_reachability(socket)
.await
.is_reachable()
})
}
#[tokio::test]
async fn reachable_when_the_daemon_reports_ok() {
let reach = probe_body(json!({ "status": "ok", "version": "x" })).await;
assert!(reach.is_reachable(), "{reach:?}");
assert_eq!(reach, MemoryReachability::Reachable);
}
#[tokio::test]
async fn wedged_daemon_is_not_reachable() {
let reach = probe_body(json!({
"status": "wedged",
"worker": { "in_flight": 3, "oldest_age_secs": 600, "wedged": true },
}))
.await;
assert!(!reach.is_reachable(), "{reach:?}");
assert_eq!(
reach,
MemoryReachability::Unhealthy(MemoryHealthStatus::Wedged)
);
assert!(reach.describe().contains("WEDGED"), "{}", reach.describe());
}
#[tokio::test]
async fn degraded_daemon_is_not_reachable() {
let reach = probe_body(json!({ "status": "degraded", "detail": "store failed" })).await;
assert!(!reach.is_reachable(), "{reach:?}");
assert_eq!(
reach,
MemoryReachability::Unhealthy(MemoryHealthStatus::Degraded)
);
}
#[tokio::test]
async fn malformed_health_body_is_not_reachable() {
let reach = probe_body(json!("<html>502 bad gateway</html>")).await;
assert!(!reach.is_reachable(), "{reach:?}");
assert_eq!(
reach,
MemoryReachability::Unhealthy(MemoryHealthStatus::Missing)
);
}
#[tokio::test]
async fn health_body_without_a_status_is_not_reachable() {
let reach = probe_body(json!({ "version": "1.2.3", "uptime_secs": 9 })).await;
assert!(!reach.is_reachable(), "{reach:?}");
assert_eq!(
reach,
MemoryReachability::Unhealthy(MemoryHealthStatus::Missing)
);
}
#[tokio::test]
async fn unknown_health_status_is_not_reachable() {
let reach = probe_body(json!({ "status": "warming" })).await;
assert!(!reach.is_reachable(), "{reach:?}");
assert_eq!(
reach,
MemoryReachability::Unhealthy(MemoryHealthStatus::Unrecognised("warming".to_string()))
);
}
#[tokio::test]
async fn refused_health_call_is_not_reachable() {
let daemon = crate::uds_mock::spawn(|_method: &str, _params: Value| {
Box::pin(async move { Err(RpcError::internal("no such method")) })
})
.await;
let reach = super::probe_socket_reachability(daemon.socket()).await;
assert!(!reach.is_reachable(), "{reach:?}");
assert!(matches!(reach, MemoryReachability::Refused(_)), "{reach:?}");
}
#[tokio::test]
async fn unreachable_socket_answers_false() {
let tmp = tempfile::tempdir().expect("tempdir");
let absent = tmp.path().join("no-daemon-here.sock");
let reach = super::probe_socket_reachability(&absent).await;
assert_eq!(reach, MemoryReachability::Unreachable);
assert!(
reach.describe().contains("unreachable"),
"{}",
reach.describe()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn resolved_reachability_skips_the_probe() {
let daemon = answering_daemon().await;
let socket = daemon.socket().to_path_buf();
let calls = std::sync::atomic::AtomicUsize::new(0);
let probe = || {
calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
blocking_reachable(&socket)
};
let resolved = super::resolve_memory_reachable_with(Some(false), probe);
assert!(
!resolved,
"the caller's resolved value must be taken verbatim"
);
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
0,
"a resolved reachability must dial no socket at all"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn unresolved_reachability_runs_the_probe() {
let daemon = answering_daemon().await;
let socket = daemon.socket().to_path_buf();
let calls = std::sync::atomic::AtomicUsize::new(0);
let probe = || {
calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
blocking_reachable(&socket)
};
assert!(super::resolve_memory_reachable_with(None, probe));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test]
fn a_panicking_probe_answers_false_without_propagating() {
assert!(
!super::block_on_probe(async { panic!("probe blew up") }),
"a panicking probe must answer false"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn blocking_probe_answers_inside_a_runtime() {
let daemon = answering_daemon().await;
let socket = daemon.socket().to_path_buf();
assert!(
blocking_reachable(&socket),
"the blocking bridge must reach a live daemon from inside a runtime"
);
let tmp = tempfile::tempdir().expect("tempdir");
let absent = tmp.path().join("no-daemon-here.sock");
assert!(
!blocking_reachable(&absent),
"an absent socket must answer false rather than hang"
);
}