#![cfg(not(loom))]
use affinitypool::{Builder, Threadpool};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[tokio::test]
async fn test_basic_task_execution() {
let pool = Threadpool::new(4);
let result = pool.spawn(|| 42).await;
assert_eq!(result, 42);
let result = pool.spawn(|| "hello world").await;
assert_eq!(result, "hello world");
}
#[tokio::test]
async fn test_multiple_tasks() {
let pool = Threadpool::new(4);
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..100 {
let counter = counter.clone();
handles.push(pool.spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
}
for handle in handles {
handle.await;
}
assert_eq!(counter.load(Ordering::SeqCst), 100);
}
#[tokio::test]
async fn test_work_distribution() {
let pool = Threadpool::new(8);
let thread_ids = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for _ in 0..100 {
let thread_ids = thread_ids.clone();
handles.push(pool.spawn(move || {
let id = thread::current().id();
thread_ids.lock().unwrap().push(id);
thread::sleep(Duration::from_micros(10));
}));
}
for handle in handles {
handle.await;
}
let ids = thread_ids.lock().unwrap();
let unique_threads: std::collections::HashSet<_> = ids.iter().collect();
assert!(unique_threads.len() > 1);
println!("Tasks distributed across {} threads", unique_threads.len());
}
#[tokio::test]
async fn test_heavy_computational_load() {
let pool = Threadpool::new(4);
let mut results = Vec::new();
for i in 0..50 {
results.push(pool.spawn(move || {
let mut sum = 0u64;
for j in 0..100_000 {
sum = sum.wrapping_add((i * j) as u64);
}
sum
}));
}
let mut total = 0u64;
for result in results {
total = total.wrapping_add(result.await);
}
assert!(total > 0);
}
#[tokio::test]
async fn test_global_threadpool() {
let pool = Threadpool::new(4);
assert!(pool.build_global().is_ok());
let result = affinitypool::spawn(|| {
thread::sleep(Duration::from_millis(10));
123
})
.await;
assert_eq!(result, 123);
}
#[tokio::test]
async fn test_spawn_local() {
let pool = Threadpool::new(4);
let data = [1, 2, 3, 4, 5];
let result = pool.spawn_local(|| data.iter().sum::<i32>()).await;
assert_eq!(result, 15);
assert_eq!(data.len(), 5);
}
#[tokio::test]
async fn test_panic_recovery() {
let pool = Threadpool::new(4);
let initial_thread_count = pool.thread_count();
let result = pool.spawn(|| {
panic!("Test panic!");
});
let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tokio::runtime::Runtime::new().unwrap().block_on(result)
}));
assert!(panic_result.is_err());
thread::sleep(Duration::from_millis(100));
let result = pool.spawn(|| 42).await;
assert_eq!(result, 42);
assert_eq!(pool.thread_count(), initial_thread_count);
}
#[tokio::test]
async fn test_builder_configuration() {
let pool = Builder::new().worker_threads(2).build();
assert_eq!(pool.num_threads(), 2);
let result = pool.spawn(|| 42).await;
assert_eq!(result, 42);
}
#[tokio::test]
async fn test_thread_naming() {
let pool = Builder::new().worker_threads(2).thread_name("test-worker").build();
let thread_name = pool.spawn(|| thread::current().name().unwrap().to_string()).await;
assert!(thread_name.contains("test-worker"));
}
#[tokio::test]
async fn test_concurrent_spawns() {
let pool = Arc::new(Threadpool::new(4));
let mut handles = Vec::new();
for i in 0..10 {
let pool = pool.clone();
let handle = tokio::spawn(async move {
let mut results = Vec::new();
for j in 0..10 {
let result = pool.spawn(move || i * 10 + j).await;
results.push(result);
}
results
});
handles.push(handle);
}
let mut all_results = Vec::new();
for handle in handles {
let results = handle.await.unwrap();
all_results.extend(results);
}
all_results.sort();
let expected: Vec<i32> = (0..100).collect();
assert_eq!(all_results, expected);
}
#[tokio::test]
async fn test_mixed_workload_completion() {
let pool = Threadpool::new(4);
let mut all_results = Vec::new();
let mut quick_handles1 = Vec::new();
for i in 0..50 {
quick_handles1.push(pool.spawn(move || i * 2));
}
let mut slow_handles = Vec::new();
for i in 0..10 {
slow_handles.push(pool.spawn(move || {
thread::sleep(Duration::from_millis(10));
i * 3
}));
}
let mut quick_handles2 = Vec::new();
for i in 0..50 {
quick_handles2.push(pool.spawn(move || i * 4));
}
for handle in quick_handles1 {
all_results.push(handle.await);
}
for handle in slow_handles {
all_results.push(handle.await);
}
for handle in quick_handles2 {
all_results.push(handle.await);
}
assert_eq!(all_results.len(), 110);
let group1_results: Vec<_> = all_results[0..50].to_vec();
let expected1: Vec<_> = (0..50).map(|i| i * 2).collect();
assert_eq!(group1_results, expected1, "First group of quick tasks produced incorrect results");
let slow_results: Vec<_> = all_results[50..60].to_vec();
let expected_slow: Vec<_> = (0..10).map(|i| i * 3).collect();
assert_eq!(slow_results, expected_slow, "Slow tasks produced incorrect results");
let group2_results: Vec<_> = all_results[60..110].to_vec();
let expected2: Vec<_> = (0..50).map(|i| i * 4).collect();
assert_eq!(group2_results, expected2, "Second group of quick tasks produced incorrect results");
}
#[tokio::test]
async fn test_zero_tasks() {
let pool = Threadpool::new(4);
assert_eq!(pool.thread_count(), 4);
assert_eq!(pool.num_threads(), 4);
}
#[tokio::test]
async fn test_single_thread_pool() {
let pool = Threadpool::new(1);
let mut results = Vec::new();
for i in 0..10 {
results.push(pool.spawn(move || i).await);
}
assert_eq!(results, (0..10).collect::<Vec<_>>());
}
#[tokio::test]
async fn test_zero_threads_clamped_to_one() {
let pool = Builder::new().worker_threads(0).build();
assert_eq!(pool.num_threads(), 1);
let result = pool.spawn(|| 42).await;
assert_eq!(result, 42);
}
#[tokio::test]
async fn test_zero_threads_direct_clamped_to_one() {
let pool = Threadpool::new(0);
assert_eq!(pool.num_threads(), 1);
let result = pool.spawn(|| "hello").await;
assert_eq!(result, "hello");
}
#[tokio::test]
async fn test_too_many_threads_clamped_to_max() {
let pool = Threadpool::new(1000);
assert_eq!(pool.num_threads(), 512);
let result = pool.spawn(|| 123).await;
assert_eq!(result, 123);
}
#[tokio::test]
async fn test_builder_too_many_threads_clamped() {
let pool = Builder::new().worker_threads(10_000).build();
assert_eq!(pool.num_threads(), 512);
let result = pool.spawn(|| vec![1, 2, 3]).await;
assert_eq!(result, vec![1, 2, 3]);
}
#[tokio::test]
async fn test_max_threads_allowed() {
let pool = Threadpool::new(512);
assert_eq!(pool.num_threads(), 512);
let result = pool.spawn(|| 42).await;
assert_eq!(result, 42);
let pool2 = Threadpool::new(513);
assert_eq!(pool2.num_threads(), 512);
}
#[tokio::test]
async fn test_reasonable_thread_counts() {
for count in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] {
let pool = Threadpool::new(count);
assert_eq!(pool.num_threads(), count);
let result = pool.spawn(move || count * 2).await;
assert_eq!(result, count * 2);
}
}
#[tokio::test]
async fn test_clamping_behavior() {
assert_eq!(Threadpool::new(0).num_threads(), 1);
assert_eq!(Threadpool::new(513).num_threads(), 512);
assert_eq!(Threadpool::new(1000).num_threads(), 512);
assert_eq!(Threadpool::new(usize::MAX).num_threads(), 512);
assert_eq!(Builder::new().worker_threads(0).build().num_threads(), 1);
assert_eq!(Builder::new().worker_threads(600).build().num_threads(), 512);
assert_eq!(Builder::new().build().num_threads(), 2);
}
#[tokio::test]
async fn test_default_threadpool() {
let pool = Threadpool::default();
let cpu_count = num_cpus::get();
let expected = cpu_count.min(512);
assert_eq!(pool.num_threads(), expected);
let result = pool.spawn(|| "default pool").await;
assert_eq!(result, "default pool");
}
#[tokio::test]
async fn test_large_return_values() {
let pool = Threadpool::new(4);
let large_vec = pool
.spawn(|| {
vec![1u8; 1_000_000] })
.await;
assert_eq!(large_vec.len(), 1_000_000);
assert!(large_vec.iter().all(|&x| x == 1));
}
#[tokio::test]
async fn test_nested_spawns() {
let pool = Arc::new(Threadpool::new(4));
let pool_clone = pool.clone();
let result = pool
.spawn(move || {
let pool = pool_clone;
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { pool.spawn(|| 42).await })
})
.join()
.unwrap()
})
.await;
assert_eq!(result, 42);
}
#[tokio::test]
async fn test_threads_properly_joined_on_drop() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
let thread_exited = Arc::new(AtomicBool::new(false));
let exit_clone = thread_exited.clone();
{
let pool = Threadpool::new(4);
pool.spawn(move || {
thread::sleep(Duration::from_millis(50));
exit_clone.store(true, Ordering::SeqCst);
})
.await;
}
assert!(
thread_exited.load(Ordering::SeqCst),
"Task should have completed before pool was dropped"
);
let start = Instant::now();
{
let pool = Threadpool::new(2);
for i in 0..10 {
pool.spawn(move || {
thread::sleep(Duration::from_millis(10));
i * 2
})
.await;
}
}
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(50),
"Drop completed too quickly ({:?}), threads may not have been joined properly",
elapsed
);
}
#[tokio::test]
async fn test_drop_waits_for_running_tasks() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
let completed_tasks = Arc::new(AtomicUsize::new(0));
let start = Instant::now();
{
let pool = Threadpool::new(2);
for _ in 0..4 {
let counter = completed_tasks.clone();
pool.spawn(move || {
std::thread::sleep(Duration::from_millis(100));
counter.fetch_add(1, Ordering::SeqCst);
})
.await;
}
}
let elapsed = start.elapsed();
assert_eq!(completed_tasks.load(Ordering::SeqCst), 4);
assert!(elapsed >= Duration::from_millis(150), "Drop returned too quickly: {:?}", elapsed);
}
#[tokio::test]
async fn test_all_threads_joined_including_panicked() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
let threads_created = Arc::new(AtomicUsize::new(0));
let created_clone = threads_created.clone();
{
let pool = Threadpool::new(2);
pool.spawn(move || {
created_clone.fetch_add(1, Ordering::SeqCst);
})
.await;
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
pool.spawn(|| {
panic!("Intentional panic for testing");
})
.await
})
}));
std::thread::sleep(Duration::from_millis(100));
let created_clone2 = threads_created.clone();
pool.spawn(move || {
created_clone2.fetch_add(1, Ordering::SeqCst);
})
.await;
}
assert!(
threads_created.load(Ordering::SeqCst) >= 2,
"Should have executed tasks on multiple threads"
);
}
#[tokio::test]
async fn test_no_lost_wakeups_race_condition() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
let pool = Threadpool::new(2);
let tasks_completed = Arc::new(AtomicUsize::new(0));
for _ in 0..100 {
let counter = tasks_completed.clone();
pool.spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
})
.await;
tokio::time::sleep(Duration::from_micros(100)).await;
}
let start = Instant::now();
while tasks_completed.load(Ordering::SeqCst) < 100 {
if start.elapsed() > Duration::from_secs(1) {
panic!(
"Tasks didn't complete in time, only {} of 100 completed. \
Possible lost wakeup in park/unpark race condition.",
tasks_completed.load(Ordering::SeqCst)
);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert_eq!(tasks_completed.load(Ordering::SeqCst), 100);
}
#[tokio::test]
async fn test_spawn_local_lifetime_safety() {
let pool = Threadpool::new(2);
{
let local_data = [1, 2, 3, 4, 5];
let result = pool
.spawn_local(move || {
local_data.iter().sum::<i32>()
})
.await;
assert_eq!(result, 15);
}
let mut results = Vec::new();
for i in 0..5 {
let data = [i; 5];
results.push(
pool.spawn_local(move || {
data.iter().sum::<i32>()
})
.await,
);
}
assert_eq!(results, vec![0, 5, 10, 15, 20]);
{
let string_data = String::from("Hello, World!");
let vec_data = vec![1, 2, 3];
let tuple_data = (42, "test");
let result = pool
.spawn_local(move || format!("{} - {:?} - {:?}", string_data, vec_data, tuple_data))
.await;
assert_eq!(result, "Hello, World! - [1, 2, 3] - (42, \"test\")");
}
let value = 100;
let result = pool.spawn_local(|| value * 2).await;
assert_eq!(result, 200);
}
#[tokio::test]
async fn test_spawn_local_no_nested_deadlock() {
let pool = Arc::new(Threadpool::new(2));
let pool_clone = pool.clone();
let result = pool
.spawn(move || {
let pool = pool_clone;
{
let _future = pool.spawn_local(|| 42);
}
"no deadlock"
})
.await;
assert_eq!(result, "no deadlock");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_spawn_local_single_worker_drop_without_poll() {
let pool = Arc::new(Threadpool::new(1));
let pool_clone = pool.clone();
let outcome = tokio::time::timeout(
Duration::from_secs(5),
tokio::task::spawn(async move {
pool_clone
.spawn(move || {
let inner = pool.clone();
let _future = inner.spawn_local(|| 42);
"ok"
})
.await
}),
)
.await;
assert!(
matches!(outcome, Ok(Ok("ok"))),
"single-worker drop-without-poll must not deadlock: {outcome:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_spawn_mixed_return_types_smoke() {
let pool = Arc::new(Threadpool::new(4));
let mut joins = Vec::new();
for i in 0..1_000 {
let pool = pool.clone();
joins.push(tokio::spawn(async move {
pool.spawn(|| {}).await;
let n: usize = pool.spawn(move || i + 1).await;
assert_eq!(n, i + 1);
let v: Vec<u8> = pool.spawn(|| vec![7u8; 4096]).await;
assert_eq!(v.len(), 4096);
assert!(v.iter().all(|&b| b == 7));
}));
}
for j in joins {
j.await.unwrap();
}
}
#[tokio::test]
async fn test_spawn_panic_propagates_to_awaiter() {
let pool = Threadpool::new(2);
let res = std::panic::AssertUnwindSafe(pool.spawn(|| {
panic!("boom from worker");
}))
.catch_unwind()
.await;
assert!(res.is_err(), "panic should propagate to the awaiter");
}
trait CatchUnwindAsync: std::future::Future + Sized {
fn catch_unwind(self) -> CatchUnwind<Self>;
}
impl<F: std::future::Future + std::panic::UnwindSafe> CatchUnwindAsync for F {
fn catch_unwind(self) -> CatchUnwind<Self> {
CatchUnwind {
inner: Some(self),
}
}
}
struct CatchUnwind<F> {
inner: Option<F>,
}
impl<F: std::future::Future + std::panic::UnwindSafe> std::future::Future for CatchUnwind<F> {
type Output = std::thread::Result<F::Output>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = unsafe { self.as_mut().get_unchecked_mut() };
let inner = this.inner.as_mut().expect("polled after completion");
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
unsafe { std::pin::Pin::new_unchecked(inner) }.poll(cx)
})) {
Ok(std::task::Poll::Ready(v)) => {
this.inner = None;
std::task::Poll::Ready(Ok(v))
}
Ok(std::task::Poll::Pending) => std::task::Poll::Pending,
Err(p) => {
this.inner = None;
std::task::Poll::Ready(Err(p))
}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_spawn_dropped_before_poll_releases_payload() {
struct DropTracker(Arc<AtomicUsize>);
impl Drop for DropTracker {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let pool = Threadpool::new(2);
let drops = Arc::new(AtomicUsize::new(0));
{
let tracker = DropTracker(drops.clone());
let fut = pool.spawn(move || {
let _t = tracker;
});
drop(fut);
}
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while drops.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(5)).await;
}
assert_eq!(drops.load(Ordering::SeqCst), 1, "tracker should be dropped exactly once");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_stealer_swap_during_panic() {
let pool = Arc::new(Threadpool::new(4));
let completed = Arc::new(AtomicUsize::new(0));
let mut joins = Vec::new();
for i in 0..200 {
let pool = pool.clone();
let completed = completed.clone();
joins.push(tokio::spawn(async move {
if i % 25 == 0 {
let _ = std::panic::AssertUnwindSafe(pool.spawn(|| panic!("respawn me")))
.catch_unwind()
.await;
} else {
pool.spawn(move || {
completed.fetch_add(1, Ordering::SeqCst);
})
.await;
}
}));
}
for j in joins {
let _ = j.await;
}
assert_eq!(completed.load(Ordering::SeqCst), 192);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forget_spawn_local_future_does_not_dangle() {
use std::future::Future as _;
use std::pin::Pin;
use std::task::{Context, Waker};
struct DropTracker(Arc<AtomicUsize>);
impl Drop for DropTracker {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let pool = Threadpool::new(2);
let drops = Arc::new(AtomicUsize::new(0));
let tracker = DropTracker(drops.clone());
let mut fut = Box::pin(pool.spawn_local(move || {
let _t = tracker;
}));
let waker = Waker::noop().clone();
let mut cx = Context::from_waker(&waker);
let _ = Pin::as_mut(&mut fut).poll(&mut cx);
std::mem::forget(fut);
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while drops.load(Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(5)).await;
}
assert_eq!(
drops.load(Ordering::SeqCst),
1,
"forgetting the polled future must not leak the captured payload"
);
}
#[cfg(target_os = "windows")]
#[test]
fn test_windows_affinity_high_coreid_safe() {
use affinitypool::affinity::{self, CoreId};
let res = affinity::set_for_current(CoreId {
id: 999,
});
assert!(!res, "out-of-range core id must not be applied");
}
#[allow(dead_code)]
fn _spawn_send_bound_compile_check() {
fn assert_send<T: Send>(_: T) {}
let pool = Threadpool::new(1);
let fut = pool.spawn(|| 42u64);
assert_send(fut);
}
#[allow(dead_code)]
fn _spawn_local_send_bound_compile_check() {
fn assert_send<T: Send>(_: T) {}
let pool = Threadpool::new(1);
let fut = pool.spawn_local(|| 42u64);
assert_send(fut);
}