use std::sync::atomic::{AtomicUsize, Ordering};
use threadteam::{Error, Exec};
#[test]
fn a_zero_share_is_rejected() {
let calls = AtomicUsize::new(0);
let err = Exec::serial()
.fork_sized(&[1, 0], |_, _| {
calls.fetch_add(1, Ordering::Relaxed);
})
.unwrap_err();
assert_eq!(err, Error::ZeroBudget);
assert_eq!(calls.into_inner(), 0);
}
#[test]
fn oversubscribing_shares_are_rejected_with_budget_and_sum() {
let calls = AtomicUsize::new(0);
let f = |_: usize, _: Exec<'_>| {
calls.fetch_add(1, Ordering::Relaxed);
};
assert_eq!(
Exec::serial().fork_sized(&[1, 1], f).unwrap_err(),
Error::SharesExceedBudget { budget: 1, sum: 2 }
);
assert_eq!(
Exec::serial().fork_sized(&[usize::MAX, 1], f).unwrap_err(),
Error::SharesExceedBudget {
budget: 1,
sum: usize::MAX
}
);
assert_eq!(calls.into_inner(), 0);
}
#[test]
fn single_and_empty_shares_run_inline() {
let here = std::thread::current().id();
let calls = AtomicUsize::new(0);
Exec::serial()
.fork_sized(&[1], |i, child| {
assert_eq!((i, child.budget().get()), (0, 1));
assert_eq!(std::thread::current().id(), here);
calls.fetch_add(1, Ordering::Relaxed);
})
.unwrap();
Exec::serial()
.fork_sized(&[], |_, _| {
calls.fetch_add(1, Ordering::Relaxed);
})
.unwrap();
assert_eq!(calls.into_inner(), 1);
}
#[cfg(feature = "parallel")]
mod parallel {
use std::collections::HashSet;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Barrier, Mutex};
use std::time::Duration;
use threadteam::{Exec, Pool};
fn watchdog() -> impl Drop {
struct Stop(std::sync::mpsc::Sender<()>);
impl Drop for Stop {
fn drop(&mut self) {
let _ = self.0.send(());
}
}
let (tx, rx) = std::sync::mpsc::channel::<()>();
std::thread::spawn(move || {
if rx.recv_timeout(Duration::from_secs(60)).is_err() {
eprintln!("watchdog: test stalled");
std::process::exit(101);
}
});
Stop(tx)
}
fn pool(n: usize) -> Pool {
Pool::builder()
.threads(n)
.idle_spin(Duration::from_millis(1))
.acquire_timeout(Duration::from_secs(5))
.build()
.unwrap()
}
#[derive(Default)]
struct Gauge {
live: AtomicUsize,
max: AtomicUsize,
}
impl Gauge {
fn enter(&self) {
let now = self.live.fetch_add(1, Ordering::SeqCst) + 1;
self.max.fetch_max(now, Ordering::SeqCst);
}
fn leave(&self) {
self.live.fetch_sub(1, Ordering::SeqCst);
}
}
#[test]
fn team_children_get_exactly_their_share_and_run_it_concurrently() {
let _w = watchdog();
let team = pool(8).into_team(8).unwrap();
let mut exec = team.exec(8);
let shares = [3, 1, 2];
let sum: usize = shares.iter().sum();
let all = Barrier::new(sum);
let gauges: Vec<Gauge> = shares.iter().map(|_| Gauge::default()).collect();
let budgets: Vec<AtomicUsize> = shares.iter().map(|_| AtomicUsize::new(0)).collect();
let threads = Mutex::new(HashSet::new());
exec.reborrow()
.fork_sized(&shares, |i, child| {
budgets[i].store(child.budget().get(), Ordering::Relaxed);
let share = Barrier::new(child.budget().get());
child
.broadcast(|_| {
threads.lock().unwrap().insert(std::thread::current().id());
gauges[i].enter();
share.wait();
all.wait();
gauges[i].leave();
})
.expect("a team child owns its members");
})
.unwrap();
for (i, &s) in shares.iter().enumerate() {
assert_eq!(budgets[i].load(Ordering::Relaxed), s, "budget of child {i}");
assert_eq!(
gauges[i].max.load(Ordering::SeqCst),
s,
"concurrency of child {i}"
);
}
assert_eq!(threads.into_inner().unwrap().len(), sum);
assert_eq!(exec.budget().get(), 8);
let full = Barrier::new(8);
let gauge = Gauge::default();
exec.broadcast(|_| {
gauge.enter();
full.wait();
gauge.leave();
})
.unwrap();
assert_eq!(gauge.max.into_inner(), 8);
}
#[test]
fn plain_children_get_exactly_their_share() {
let _w = watchdog();
let pool = pool(4);
let mut exec = Exec::new(&pool, 4).unwrap();
let shares = [2, 1];
let budgets: Vec<AtomicUsize> = shares.iter().map(|_| AtomicUsize::new(0)).collect();
exec.reborrow()
.fork_sized(&shares, |i, child| {
let width = child.budget().get();
budgets[i].store(width, Ordering::Relaxed);
let meet = Barrier::new(width);
let gauge = Gauge::default();
child.fork(width, |_, _| {
gauge.enter();
meet.wait();
gauge.leave();
});
assert_eq!(gauge.max.into_inner(), width, "concurrency of child {i}");
})
.unwrap();
for (i, &s) in shares.iter().enumerate() {
assert_eq!(budgets[i].load(Ordering::Relaxed), s);
}
assert_eq!(exec.budget().get(), 4);
}
#[test]
fn a_single_team_share_carries_a_subteam_of_its_share() {
let _w = watchdog();
let team = pool(8).into_team(8).unwrap();
let mut exec = team.exec(8);
let here = std::thread::current().id();
for share in [1, 2] {
exec.reborrow()
.fork_sized(&[share], |_, mut child| {
assert_eq!(std::thread::current().id(), here);
assert_eq!(child.budget().get(), share);
let size = child.team(|t| t.size().get()).unwrap();
assert_eq!(size, share);
})
.unwrap();
}
}
fn panic_after_siblings(exec: Exec<'_>) {
let siblings = Barrier::new(2);
let done = AtomicUsize::new(0);
let caught = catch_unwind(AssertUnwindSafe(|| {
exec.fork_sized(&[1, 1, 1], |i, _| {
if i == 1 {
panic!("child 1");
}
siblings.wait();
done.fetch_add(1, Ordering::SeqCst);
})
}));
let payload = caught.expect_err("the child's panic propagates");
assert_eq!(payload.downcast_ref::<&str>(), Some(&"child 1"));
assert_eq!(done.into_inner(), 2, "the siblings were joined first");
}
#[test]
fn a_panic_in_one_child_propagates_after_the_others_are_joined() {
let _w = watchdog();
let plain = pool(3);
panic_after_siblings(Exec::new(&plain, 3).unwrap());
let team = pool(3).into_team(3).unwrap();
panic_after_siblings(team.exec(3));
}
}