use std::{
collections::HashSet,
num::NonZeroUsize,
panic,
sync::{Arc, Barrier, mpsc},
thread::{self, ThreadId},
time::Duration,
};
use ntest::timeout;
use crate::unblock::pool::Pool;
#[test]
#[timeout(15000)]
fn an_idle_thread_takes_up_the_next_job() {
let pool = pool(4, LONG_IDLE_TIMEOUT);
let first = run(&pool, || thread::current().id());
wait_until(|| pool.idle_threads() == 1);
let second = run(&pool, || thread::current().id());
assert_eq!(first, second);
assert_eq!(pool.threads(), 1);
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn jobs_that_wait_on_each_other_each_get_a_thread() {
const JOBS: usize = 3;
let pool = pool(JOBS + 1, SHORT_IDLE_TIMEOUT);
let barrier = Arc::new(Barrier::new(JOBS + 1));
let (report, reported) = mpsc::channel();
for _ in 0..JOBS {
let barrier = barrier.clone();
let report = report.clone();
Pool::submit(
pool.clone(),
Box::new(move || {
barrier.wait();
let id = thread::current().id();
report.send(id).expect("the test waits for the job");
}),
);
}
assert_eq!(pool.threads(), JOBS);
barrier.wait();
let threads: HashSet<ThreadId> = reported.iter().take(JOBS).collect();
assert_eq!(threads.len(), JOBS);
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn past_its_cap_a_job_waits_for_a_thread_to_be_done() {
const CAP: usize = 2;
let pool = pool(CAP, SHORT_IDLE_TIMEOUT);
let (started, starts) = mpsc::channel();
let releases: Vec<mpsc::Sender<()>> = (0..CAP)
.map(|job| {
let (release, released) = mpsc::channel();
let started = started.clone();
Pool::submit(
pool.clone(),
Box::new(move || {
let id = thread::current().id();
started.send((job, id)).expect("the test waits for the job");
released.recv().expect("the test lets the job end");
}),
);
release
})
.collect();
let mut busy: Vec<(usize, ThreadId)> = starts.iter().take(CAP).collect();
busy.sort_by_key(|&(job, _)| job);
let (ran, runs) = mpsc::channel();
Pool::submit(
pool.clone(),
Box::new(move || {
let id = thread::current().id();
ran.send(id).expect("the test waits for the job");
}),
);
assert_eq!(pool.threads(), CAP);
releases[0].send(()).expect("the job waits for the test");
assert_eq!(runs.recv().expect("the job left waiting runs"), busy[0].1);
for release in &releases[1..] {
release.send(()).expect("the job waits for the test");
}
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn an_idle_thread_goes_after_the_idle_timeout() {
let pool = pool(4, SHORT_IDLE_TIMEOUT);
let (release, released) = mpsc::channel();
Pool::submit(
pool.clone(),
Box::new(move || released.recv().expect("the test lets the job end")),
);
assert_eq!(pool.threads(), 1);
release.send(()).expect("the job waits for the test");
wind_down(&pool);
assert_eq!(run(&pool, || 42), 42);
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn a_job_that_panics_leaves_its_thread_to_the_jobs_after_it() {
let pool = pool(4, LONG_IDLE_TIMEOUT);
let (report, reported) = mpsc::channel();
Pool::submit(
pool.clone(),
Box::new(move || {
report
.send(thread::current().id())
.expect("the test waits for the job");
panic!("the job panicked");
}),
);
let panicked = reported.recv().expect("the job runs");
wait_until(|| pool.idle_threads() == 1);
let next = run(&pool, || thread::current().id());
assert_eq!(next, panicked);
assert_eq!(pool.threads(), 1);
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn a_panic_whose_payload_panics_as_it_drops_leaves_the_thread_to_the_jobs_after_it() {
let pool = pool(1, LONG_IDLE_TIMEOUT);
Pool::submit(pool.clone(), Box::new(|| panic::panic_any(PanicsOnDrop)));
let ran = run(&pool, || "the next job ran");
assert_eq!(ran, "the next job ran");
assert_eq!(pool.threads(), 1);
wind_down(&pool);
}
#[test]
#[timeout(15000)]
fn the_threads_of_a_pool_have_its_name() {
let pool = pool(1, SHORT_IDLE_TIMEOUT);
let name = run(&pool, || thread::current().name().map(ToOwned::to_owned));
assert_eq!(name.as_deref(), Some(POOL_NAME));
wind_down(&pool);
}
fn pool(threads: usize, idle_timeout: Duration) -> Arc<Pool> {
let cap = NonZeroUsize::new(threads).expect("a pool has room for a thread");
Arc::new(Pool::new(POOL_NAME, cap, idle_timeout))
}
fn run<F, T>(pool: &Arc<Pool>, job: F) -> T
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (report, reported) = mpsc::channel();
Pool::submit(
pool.clone(),
Box::new(move || report.send(job()).expect("the test waits for the job")),
);
reported.recv().expect("the job runs")
}
fn wind_down(pool: &Pool) {
wait_until(|| pool.threads() == 0);
}
fn wait_until<F>(condition: F)
where
F: Fn() -> bool,
{
while !condition() {
thread::sleep(Duration::from_millis(1));
}
}
const LONG_IDLE_TIMEOUT: Duration = Duration::from_secs(2);
const SHORT_IDLE_TIMEOUT: Duration = Duration::from_millis(50);
const POOL_NAME: &str = "zruntime test pool";
struct PanicsOnDrop;
impl Drop for PanicsOnDrop {
fn drop(&mut self) {
panic!("the payload of the panic panicked as it dropped");
}
}