From fd5c5453e5295748f137ffde6bc0599fe6e97e5e Mon Sep 17 00:00:00 2001 From: Manuel Raimann Date: Thu, 12 Feb 2026 16:22:38 +0100 Subject: [PATCH] test(async): add concurrency verification tests for optimize_parallel - Timing-based proof that trials run in parallel, not sequentially - Atomic counter proof that multiple workers overlap simultaneously - Panic propagation returns TaskError with panic message - Partial failures with parallel path store only successful trials --- tests/async_tests.rs | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/async_tests.rs b/tests/async_tests.rs index ba9754f..a5c271f 100644 --- a/tests/async_tests.rs +++ b/tests/async_tests.rs @@ -4,6 +4,8 @@ #![cfg(feature = "async")] +use std::sync::Arc; + use optimizer::parameter::{FloatParam, Parameter}; use optimizer::sampler::random::RandomSampler; use optimizer::sampler::tpe::TpeSampler; @@ -192,3 +194,118 @@ async fn test_optimize_parallel_single_concurrency() { assert_eq!(study.n_trials(), 10); } + +#[tokio::test] +async fn test_parallel_executes_concurrently() { + let sampler = RandomSampler::with_seed(42); + let study: Study = Study::with_sampler(Direction::Minimize, sampler); + + let x_param = FloatParam::new(0.0, 10.0); + + let start = tokio::time::Instant::now(); + study + .optimize_parallel(4, 4, move |trial: &mut optimizer::Trial| { + let x = x_param.suggest(trial)?; + std::thread::sleep(std::time::Duration::from_millis(100)); + Ok::<_, Error>(x) + }) + .await + .expect("parallel optimization should succeed"); + + let elapsed = start.elapsed(); + assert_eq!(study.n_trials(), 4); + // Sequential would take ~400ms; parallel with concurrency=4 should be ~100ms + assert!( + elapsed < std::time::Duration::from_millis(350), + "expected parallel execution under 350ms, took {elapsed:?}" + ); +} + +#[tokio::test] +async fn test_parallel_max_concurrency_reached() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + let sampler = RandomSampler::with_seed(42); + let study: Study = Study::with_sampler(Direction::Minimize, sampler); + + let x_param = FloatParam::new(0.0, 10.0); + let active = Arc::new(AtomicUsize::new(0)); + let max_active = Arc::new(AtomicUsize::new(0)); + + let active_c = Arc::clone(&active); + let max_active_c = Arc::clone(&max_active); + + study + .optimize_parallel(8, 4, move |trial: &mut optimizer::Trial| { + let x = x_param.suggest(trial)?; + + let current = active_c.fetch_add(1, Ordering::SeqCst) + 1; + // Update max_active if this is the highest seen so far + max_active_c.fetch_max(current, Ordering::SeqCst); + + std::thread::sleep(std::time::Duration::from_millis(50)); + + active_c.fetch_sub(1, Ordering::SeqCst); + Ok::<_, Error>(x) + }) + .await + .expect("parallel optimization should succeed"); + + assert_eq!(study.n_trials(), 8); + let max = max_active.load(Ordering::SeqCst); + assert!( + max >= 2, + "expected at least 2 concurrent workers, but max was {max}" + ); +} + +#[tokio::test] +async fn test_parallel_panic_returns_task_error() { + let study: Study = Study::new(Direction::Minimize); + + let result = study + .optimize_parallel(3, 2, |_trial: &mut optimizer::Trial| { + panic!("boom"); + #[allow(unreachable_code)] + Ok::<_, Error>(0.0) + }) + .await; + + match result { + Err(Error::TaskError(msg)) => { + assert!( + msg.contains("panic"), + "expected error message to contain 'panic', got: {msg}" + ); + } + other => panic!("expected TaskError, got {other:?}"), + } +} + +#[tokio::test] +async fn test_parallel_partial_failures_trial_count() { + use std::sync::atomic::{AtomicUsize, Ordering}; + + let sampler = RandomSampler::with_seed(42); + let study: Study = Study::with_sampler(Direction::Minimize, sampler); + + let x_param = FloatParam::new(0.0, 10.0); + let counter = Arc::new(AtomicUsize::new(0)); + + let counter_c = Arc::clone(&counter); + + study + .optimize_parallel(10, 3, move |trial: &mut optimizer::Trial| { + let idx = counter_c.fetch_add(1, Ordering::SeqCst); + if idx % 2 == 1 { + return Err(Error::NoCompletedTrials); + } + let x = x_param.suggest(trial)?; + Ok::<_, Error>(x) + }) + .await + .expect("should succeed with partial failures"); + + // Even indices succeed (0, 2, 4, 6, 8), odd indices fail + assert_eq!(study.n_trials(), 5); +}