use std::{
future::poll_fn,
panic::{AssertUnwindSafe, catch_unwind},
pin::pin,
task::{Context, Poll},
};
use ntest::timeout;
use super::core::counting_waker;
use crate::coop::{self, budgeted, poll_proceed, spend, unconstrained};
#[test]
#[timeout(15000)]
fn without_a_poll_every_operation_proceeds() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
assert_eq!(proceeded(&mut cx, 3 * BUDGET), 3 * BUDGET);
assert_eq!(woken.count(), 0);
}
#[test]
#[timeout(15000)]
fn an_operation_yields_once_the_budget_is_spent() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET);
assert_eq!(woken.count(), 1);
spend();
assert!(poll_proceed(&mut cx).is_pending());
assert_eq!(woken.count(), 2);
});
}
#[test]
#[timeout(15000)]
fn every_poll_starts_with_a_full_budget() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
for _ in 0..3 {
budgeted(|| assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET));
}
assert_eq!(woken.count(), 3);
}
#[test]
#[timeout(15000)]
fn a_poll_inside_another_gives_the_budget_back() {
let (_woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 5), 5);
budgeted(|| assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET));
assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET - 5);
});
}
#[test]
#[timeout(15000)]
fn a_spent_budget_turns_every_operation_away() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
let (nested_woken, nested_waker) = counting_waker();
let mut nested_cx = Context::from_waker(&nested_waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 5), 5);
assert_eq!(proceeded(&mut nested_cx, 3 * BUDGET), BUDGET - 5);
assert_eq!(nested_woken.count(), 1);
for asked in 2..=3 * BUDGET {
assert!(poll_proceed(&mut nested_cx).is_pending());
assert_eq!(nested_woken.count(), asked);
}
assert!(poll_proceed(&mut cx).is_pending());
});
assert_eq!(woken.count(), 1);
assert_eq!(nested_woken.count(), 3 * BUDGET);
}
#[test]
#[timeout(15000)]
fn the_budget_is_gone_once_the_outermost_poll_ends() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| assert_eq!(proceeded(&mut cx, 5), 5));
assert_eq!(proceeded(&mut cx, 3 * BUDGET), 3 * BUDGET);
assert_eq!(woken.count(), 0);
}
#[test]
#[timeout(15000)]
fn a_panic_out_of_a_poll_gives_the_budget_back() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
let panicking = |cx: &mut Context<'_>| {
let panicked = catch_unwind(AssertUnwindSafe(|| {
budgeted(|| {
proceeded(cx, 3 * BUDGET);
panic!("the poll panicked on purpose");
})
}));
assert!(panicked.is_err());
};
panicking(&mut cx);
assert_eq!(proceeded(&mut cx, 3 * BUDGET), 3 * BUDGET);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 5), 5);
panicking(&mut cx);
assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET - 5);
});
assert_eq!(woken.count(), 3);
}
#[test]
#[timeout(15000)]
fn unconstrained_lets_every_operation_through() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, BUDGET), BUDGET);
assert_eq!(
polled_unconstrained(&mut cx, |cx| proceeded(cx, 3 * BUDGET)),
3 * BUDGET
);
});
assert_eq!(woken.count(), 0);
}
#[test]
#[timeout(15000)]
fn unconstrained_gives_the_poll_around_it_its_budget_back() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 5), 5);
assert_eq!(
polled_unconstrained(&mut cx, |cx| proceeded(cx, 3 * BUDGET)),
3 * BUDGET
);
assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET - 5);
});
assert_eq!(woken.count(), 1);
}
#[test]
#[timeout(15000)]
fn a_panic_out_of_unconstrained_gives_the_poll_around_it_its_budget_back() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
budgeted(|| {
assert_eq!(proceeded(&mut cx, 5), 5);
let panicked = catch_unwind(AssertUnwindSafe(|| {
polled_unconstrained(&mut cx, |cx| {
proceeded(cx, 3 * BUDGET);
panic!("the future panicked on purpose");
})
}));
assert!(panicked.is_err());
assert_eq!(proceeded(&mut cx, 3 * BUDGET), BUDGET - 5);
});
assert_eq!(woken.count(), 1);
}
#[test]
#[timeout(15000)]
fn unconstrained_changes_nothing_outside_a_poll() {
let (woken, waker) = counting_waker();
let mut cx = Context::from_waker(&waker);
assert_eq!(
polled_unconstrained(&mut cx, |cx| proceeded(cx, 3 * BUDGET)),
3 * BUDGET
);
assert_eq!(proceeded(&mut cx, 3 * BUDGET), 3 * BUDGET);
assert_eq!(woken.count(), 0);
}
fn proceeded(cx: &mut Context<'_>, most: usize) -> usize {
let mut proceeded = 0;
while proceeded < most && poll_proceed(cx).is_ready() {
spend();
proceeded += 1;
}
proceeded
}
fn polled_unconstrained<R>(cx: &mut Context<'_>, poll: impl FnOnce(&mut Context<'_>) -> R) -> R {
let mut poll = Some(poll);
let mut future = pin!(unconstrained(poll_fn(move |cx| {
Poll::Ready(poll.take().expect("polled once")(cx))
})));
match future.as_mut().poll(cx) {
Poll::Ready(output) => output,
Poll::Pending => unreachable!("a future that is ready at once yields to nothing"),
}
}
const BUDGET: usize = coop::BUDGET as usize;