use std::future::Future;
use std::time::Duration;
use super::*;
async fn within<F: Future>(why: &str, future: F) -> F::Output {
tokio::time::timeout(Duration::from_secs(5), future)
.await
.unwrap_or_else(|_| panic!("{why}"))
}
#[tokio::test]
async fn a_pipe_starts_idle_and_publishes_what_it_is_told() {
let life = Lifecycle::new();
assert_eq!(life.status(), PipeStatus::Idle);
life.set_status(PipeStatus::Direct);
assert_eq!(life.status(), PipeStatus::Direct);
life.set_status(PipeStatus::Relayed);
assert_eq!(life.status(), PipeStatus::Relayed);
}
#[tokio::test]
async fn a_watcher_holding_a_stale_snapshot_returns_immediately() {
let life = Lifecycle::new();
life.set_status(PipeStatus::Direct);
let seen = within(
"a stale snapshot must resolve without waiting",
life.changed_since(PipeStatus::Idle),
)
.await;
assert_eq!(seen, PipeStatus::Direct);
}
#[tokio::test]
async fn a_watcher_holding_a_current_snapshot_waits_for_the_next_change() {
let life = std::sync::Arc::new(Lifecycle::new());
let watcher = {
let life = life.clone();
tokio::spawn(async move { life.changed_since(PipeStatus::Idle).await })
};
tokio::task::yield_now().await;
life.set_status(PipeStatus::Relayed);
let seen = within("the watcher must wake", watcher).await.unwrap();
assert_eq!(seen, PipeStatus::Relayed);
}
#[tokio::test]
async fn concurrent_watchers_each_resolve_against_their_own_snapshot() {
let life = std::sync::Arc::new(Lifecycle::new());
life.set_status(PipeStatus::Direct);
let from_idle = {
let life = life.clone();
tokio::spawn(async move { life.changed_since(PipeStatus::Idle).await })
};
let from_direct = {
let life = life.clone();
tokio::spawn(async move { life.changed_since(PipeStatus::Direct).await })
};
tokio::task::yield_now().await;
life.set_status(PipeStatus::Relayed);
assert_eq!(
within("stale snapshot", from_idle).await.unwrap(),
PipeStatus::Direct,
"the stale watcher gets the value it had not seen"
);
assert_eq!(
within("current snapshot", from_direct).await.unwrap(),
PipeStatus::Relayed,
"the current watcher gets the new one"
);
}
#[tokio::test]
async fn a_watcher_arriving_after_close_resolves_immediately_rather_than_hanging() {
let life = Lifecycle::new();
life.close();
for snapshot in [PipeStatus::Idle, PipeStatus::Direct, PipeStatus::Closed] {
let seen = within(
"a closed pipe must never block a watcher",
life.changed_since(snapshot),
)
.await;
assert_eq!(seen, PipeStatus::Closed, "from snapshot {snapshot:?}");
}
}
#[tokio::test]
async fn a_parked_watcher_is_woken_by_the_close() {
let life = std::sync::Arc::new(Lifecycle::new());
let watcher = {
let life = life.clone();
tokio::spawn(async move { life.changed_since(PipeStatus::Idle).await })
};
tokio::task::yield_now().await;
life.close();
assert_eq!(
within("close must wake a parked watcher", watcher)
.await
.unwrap(),
PipeStatus::Closed
);
}
#[tokio::test]
async fn nothing_moves_a_pipe_out_of_closed() {
let life = Lifecycle::new();
life.close();
for late in [PipeStatus::Idle, PipeStatus::Direct, PipeStatus::Relayed] {
life.set_status(late);
assert_eq!(life.status(), PipeStatus::Closed, "after a late {late:?}");
}
}
#[tokio::test]
async fn closing_twice_is_harmless() {
let life = Lifecycle::new();
life.close();
life.close();
assert_eq!(life.status(), PipeStatus::Closed);
}
#[tokio::test]
async fn closing_the_pipe_does_not_by_itself_mean_teardown_finished() {
let life = std::sync::Arc::new(Lifecycle::new());
life.close();
assert_eq!(life.status(), PipeStatus::Closed);
let waiter = {
let life = life.clone();
tokio::spawn(async move { life.wait_until_torn_down().await })
};
tokio::task::yield_now().await;
assert!(
!waiter.is_finished(),
"a closed status must not be mistaken for released resources"
);
life.mark_torn_down();
within("teardown must complete the wait", waiter)
.await
.unwrap();
}
#[tokio::test]
async fn a_waiter_arriving_after_teardown_returns_immediately() {
let life = Lifecycle::new();
life.mark_torn_down();
within(
"an already-finished teardown must not be waited on",
life.wait_until_torn_down(),
)
.await;
}
#[tokio::test]
async fn every_concurrent_waiter_is_released_by_one_teardown() {
let life = std::sync::Arc::new(Lifecycle::new());
let waiters: Vec<_> = (0..4)
.map(|_| {
let life = life.clone();
tokio::spawn(async move { life.wait_until_torn_down().await })
})
.collect();
tokio::task::yield_now().await;
life.mark_torn_down();
for waiter in waiters {
within("all waiters must be released", waiter)
.await
.unwrap();
}
}
#[test]
fn a_listener_reports_the_worst_active_path() {
use PeerPath::{Direct, Relayed};
assert_eq!(aggregate(&[]), PipeStatus::Idle, "no peers");
assert_eq!(aggregate(&[Direct]), PipeStatus::Direct);
assert_eq!(aggregate(&[Direct, Direct]), PipeStatus::Direct);
assert_eq!(aggregate(&[Relayed]), PipeStatus::Relayed);
assert_eq!(
aggregate(&[Direct, Relayed]),
PipeStatus::Relayed,
"one relayed peer is what the user needs told"
);
assert_eq!(
aggregate(&[Relayed, Direct]),
PipeStatus::Relayed,
"and order does not matter"
);
}
#[test]
fn the_phone_and_laptop_case_is_representable() {
let phone_relayed_laptop_direct = [PeerPath::Relayed, PeerPath::Direct];
assert_eq!(
aggregate(&phone_relayed_laptop_direct),
PipeStatus::Relayed,
"the slow device is the one that needs explaining"
);
}
#[tokio::test]
async fn a_drain_waits_for_every_in_flight_exchange() {
let life = std::sync::Arc::new(Lifecycle::new());
let first = life.enter();
let second = life.enter();
assert_eq!(life.in_flight(), 2);
let drain = {
let life = life.clone();
tokio::spawn(async move { life.wait_until_drained().await })
};
tokio::task::yield_now().await;
assert!(!drain.is_finished(), "two exchanges are still running");
drop(first);
tokio::task::yield_now().await;
assert!(!drain.is_finished(), "one still is");
drop(second);
within("the drain must complete", drain).await.unwrap();
}
#[tokio::test]
async fn a_drain_with_nothing_in_flight_returns_at_once() {
let life = Lifecycle::new();
assert_eq!(life.in_flight(), 0);
within(
"an idle pipe has nothing to drain",
life.wait_until_drained(),
)
.await;
}
#[tokio::test]
async fn a_panicking_exchange_still_releases_its_slot() {
let life = std::sync::Arc::new(Lifecycle::new());
let panicked = {
let life = life.clone();
tokio::spawn(async move {
let _guard = life.enter();
panic!("an exchange died mid-body");
})
};
assert!(panicked.await.is_err(), "the task really did panic");
assert_eq!(life.in_flight(), 0, "and its slot came back");
within(
"a drain must not hang on a dead exchange",
life.wait_until_drained(),
)
.await;
}