use std::future::Future;
use std::pin::{Pin, pin};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, Weak};
use std::task::{Context, Poll, Waker};
use anyhow::anyhow;
use tokio::sync::watch;
use super::*;
use crate::console::changes::Revision;
#[derive(Clone)]
struct Changes(Arc<Mutex<Option<watch::Sender<Revision>>>>);
impl Changes {
fn commands(&self) {
self.send(|revision| revision.commands += 1);
}
fn accounts(&self) {
self.send(|revision| revision.accounts += 1);
}
fn close(&self) {
self.0.lock().unwrap().take();
}
fn send(&self, change: impl FnOnce(&mut Revision)) {
self.0.lock().unwrap().as_ref().unwrap().send_modify(change);
}
}
struct Fixture {
subscription: Subscription,
changes: Changes,
checks: Arc<AtomicUsize>,
lease: Weak<Following>,
}
impl Fixture {
fn checks(&self) -> usize {
self.checks.load(Ordering::SeqCst)
}
}
fn subscribe(script: impl Fn(&Changes, usize) -> Result<bool> + Send + Sync + 'static) -> Fixture {
let (sender, receiver) = watch::channel(Revision::default());
let changes = Changes(Arc::new(Mutex::new(Some(sender))));
let checks = Arc::new(AtomicUsize::new(0));
let (scripted, counted) = (changes.clone(), checks.clone());
let check = Check::Scripted(Arc::new(move || {
script(&scripted, counted.fetch_add(1, Ordering::SeqCst) + 1)
}));
let lease = Arc::new(Following::idle());
Fixture {
lease: Arc::downgrade(&lease),
subscription: Subscription::with(receiver, check, lease),
changes,
checks,
}
}
fn allowed() -> Fixture {
subscribe(|_, _| Ok(true))
}
fn run(test: impl Future<Output = ()>) {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(test);
}
fn poll_once<F: Future>(future: Pin<&mut F>) -> Poll<F::Output> {
future.poll(&mut Context::from_waker(Waker::noop()))
}
fn granted_now(subscription: &mut Subscription, mode: Readiness) -> bool {
match poll_once(pin!(subscription.ready(mode))) {
Poll::Ready(result) => result.unwrap(),
Poll::Pending => false,
}
}
#[test]
fn a_connection_gets_its_first_work_turn_after_one_check_in_either_mode() {
run(async {
for mode in [Readiness::Wait, Readiness::Drain] {
let mut fixture = allowed();
assert!(fixture.subscription.ready(mode).await.unwrap());
assert_eq!(fixture.checks(), 1);
}
});
}
#[test]
fn wait_sleeps_until_commands_change_but_drain_permits_work_now() {
run(async {
let mut fixture = allowed();
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
for _ in 0..3 {
assert!(granted_now(&mut fixture.subscription, Readiness::Drain));
}
{
let mut waiting = pin!(fixture.subscription.ready(Readiness::Wait));
assert!(poll_once(waiting.as_mut()).is_pending());
fixture.changes.commands();
assert!(waiting.await.unwrap());
}
assert_eq!(fixture.checks(), 1);
});
}
#[test]
fn commands_changes_coalesce_to_the_latest_revision() {
run(async {
let mut fixture = allowed();
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
for _ in 0..3 {
fixture.changes.commands();
}
assert!(granted_now(&mut fixture.subscription, Readiness::Wait));
assert!(!granted_now(&mut fixture.subscription, Readiness::Wait));
});
}
#[test]
fn an_account_only_change_rechecks_without_a_work_turn() {
run(async {
let mut fixture = subscribe(|changes, check| {
if check == 2 {
changes.close();
}
Ok(true)
});
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
fixture.changes.accounts();
assert!(!fixture.subscription.ready(Readiness::Wait).await.unwrap());
assert_eq!(fixture.checks(), 2);
});
}
#[test]
fn an_account_change_after_a_wait_is_rechecked_before_the_next_notice() {
run(async {
let mut fixture = allowed();
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
fixture.changes.accounts();
fixture.changes.commands();
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
assert_eq!(fixture.checks(), 2);
});
}
#[test]
fn a_revision_arriving_during_authorization_is_checked_before_work() {
run(async {
for (newer, permitted) in [(true, true), (false, false)] {
let mut fixture = subscribe(move |changes, check| {
if check == 1 {
changes.accounts();
return Ok(true);
}
Ok(newer)
});
assert_eq!(
fixture.subscription.ready(Readiness::Wait).await.unwrap(),
permitted
);
assert_eq!(fixture.checks(), 2);
}
});
}
#[test]
fn a_change_during_work_is_kept_for_the_next_wait() {
run(async {
for first in [Readiness::Wait, Readiness::Drain] {
let mut fixture = allowed();
assert!(fixture.subscription.ready(first).await.unwrap());
fixture.changes.commands();
assert!(granted_now(&mut fixture.subscription, Readiness::Wait));
assert!(!granted_now(&mut fixture.subscription, Readiness::Wait));
}
});
}
#[test]
fn closure_permits_no_further_work_and_releases_the_lease() {
run(async {
for mode in [Readiness::Wait, Readiness::Drain] {
let mut fixture = allowed();
assert!(fixture.subscription.ready(mode).await.unwrap());
fixture.changes.commands();
fixture.changes.close();
assert!(!fixture.subscription.ready(mode).await.unwrap());
assert!(fixture.lease.upgrade().is_none());
let mut fixture = allowed();
assert!(fixture.subscription.ready(mode).await.unwrap());
{
let mut waiting = pin!(fixture.subscription.ready(Readiness::Wait));
assert!(poll_once(waiting.as_mut()).is_pending());
fixture.changes.close();
assert!(!waiting.await.unwrap());
}
assert!(fixture.lease.upgrade().is_none());
let mut fixture = subscribe(|changes, _| {
changes.close();
Ok(true)
});
assert!(!fixture.subscription.ready(mode).await.unwrap());
assert_eq!(fixture.checks(), 1);
assert!(fixture.lease.upgrade().is_none());
}
});
}
#[test]
fn denial_and_check_failures_end_the_subscription_permanently() {
run(async {
let denied = subscribe(|_, _| Ok(false));
let failed = subscribe(|_, _| Err(anyhow!("accounts unreadable")));
let panicked = subscribe(|_, _| panic!("scripted blocking-task failure"));
for (mut fixture, error) in [(denied, false), (failed, true), (panicked, true)] {
let first = fixture.subscription.ready(Readiness::Drain).await;
assert_eq!(first.is_err(), error);
assert!(!first.unwrap_or(false));
assert!(fixture.lease.upgrade().is_none());
fixture.changes.accounts();
fixture.changes.commands();
for mode in [Readiness::Drain, Readiness::Wait] {
assert!(!fixture.subscription.ready(mode).await.unwrap());
}
assert_eq!(fixture.checks(), 1);
}
});
}
#[test]
fn dropping_a_subscription_releases_its_lease() {
run(async {
let mut fixture = allowed();
assert!(fixture.subscription.ready(Readiness::Wait).await.unwrap());
assert!(fixture.lease.upgrade().is_some());
let lease = fixture.lease.clone();
drop(fixture);
assert!(lease.upgrade().is_none());
});
}