use dever_runtime::task::{Error, Limits, Runtime}; mod deadline; mod group; mod panic; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; struct DropNotice(Arc); impl Drop for DropNotice { fn drop(&mut self) { std::thread::sleep(std::time::Duration::from_millis(20)); self.0.store(true, Ordering::SeqCst); } } fn runtime() -> Runtime { Runtime::new(Limits { threads: 2, workers: 1, tasks: 8, }) .unwrap() } #[test] fn scope_waits_for_blocking_work_after_async_parent_is_cancelled() { let entered = Arc::new(AtomicBool::new(false)); let finished = Arc::new(AtomicBool::new(false)); runtime() .run(async |scope| { let parent_entered = entered.clone(); let parent_finished = finished.clone(); let parent = scope .spawn(move |scope| async move { scope .blocking(move || { parent_entered.store(true, Ordering::SeqCst); std::thread::sleep(std::time::Duration::from_millis(30)); parent_finished.store(true, Ordering::SeqCst); }) .await .unwrap(); }) .unwrap(); while !entered.load(Ordering::SeqCst) { tokio::task::yield_now().await; } assert!(parent.stop().await.unwrap().is_none()); assert!(finished.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn task_errors_and_capacity_are_observable_and_slots_are_reused() { let runtime = Runtime::new(Limits { threads: 1, workers: 1, tasks: 1, }) .unwrap(); runtime .run(async |scope| { let pending = scope.spawn_leaf(std::future::pending::<()>).unwrap(); assert!(matches!(scope.spawn_sync(|| ()), Err(Error::Capacity))); assert!(pending.stop().await.unwrap().is_none()); let value = scope.spawn_sync(|| 42).unwrap().wait().await.unwrap(); assert_eq!(value, 42); let failure = scope .spawn_leaf(|| async { panic!("test panic"); }) .unwrap(); assert_eq!(failure.wait().await, Err(Error::Panicked)); let failure = scope.blocking(|| panic!("direct worker panic")).await; assert_eq!(failure, Err(Error::Panicked)); assert_eq!(scope.blocking(|| 42).await.unwrap(), 42); }) .unwrap(); } #[test] fn cancelled_function_wait_keeps_descendants_tracked_until_their_real_exit() { let finished = Arc::new(AtomicBool::new(false)); let observed = finished.clone(); let (started, ready) = tokio::sync::oneshot::channel(); runtime() .run(async |root| { let parent = root .spawn(move |child| async move { let nested = child .spawn_sync(move || { started.send(()).unwrap(); std::thread::sleep(std::time::Duration::from_millis(30)); finished.store(true, Ordering::SeqCst); }) .unwrap(); nested.wait().await.unwrap(); }) .unwrap(); ready.await.unwrap(); assert!(parent.stop().await.unwrap().is_none()); assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn cancelling_queued_worker_never_executes_its_call() { let called = Arc::new(AtomicBool::new(false)); runtime() .run(async |scope| { let (started, ready) = tokio::sync::oneshot::channel(); let (release, blocked) = std::sync::mpsc::channel(); let first = scope .spawn_sync(move || { started.send(()).unwrap(); blocked .recv_timeout(std::time::Duration::from_secs(2)) .unwrap(); }) .unwrap(); ready.await.unwrap(); let work_called = called.clone(); let queued = scope .spawn_sync(move || work_called.store(true, Ordering::SeqCst)) .unwrap(); assert!(queued.stop().await.unwrap().is_none()); release.send(()).unwrap(); first.wait().await.unwrap(); assert!(!called.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn claimed_output_can_outlive_its_task_scope() { let runtime = runtime(); for direct in [false, true] { let value = runtime .run(async |scope| { let value = if direct { scope .blocking(|| String::from("owned output")) .await .unwrap() } else { let task = scope.spawn_sync(|| String::from("owned output")).unwrap(); task.wait().await.unwrap() }; scope.finish().await; assert!(matches!( scope.spawn_leaf(|| async {}), Err(Error::Cancelled) )); assert_eq!(scope.blocking(|| 42).await, Err(Error::Cancelled)); value }) .unwrap(); assert_eq!(value, "owned output"); } } #[test] fn finished_empty_scope_rejects_new_work() { runtime() .run(async |scope| { scope.finish().await; assert!(matches!( scope.spawn_leaf(|| async {}), Err(Error::Cancelled) )); assert_eq!(scope.blocking(|| 42).await, Err(Error::Cancelled)); }) .unwrap(); } #[test] fn root_panic_drains_workers_from_dropped_function_handles() { let runtime = runtime(); let finished = Arc::new(AtomicBool::new(false)); let observed = finished.clone(); let result = runtime.run(async |root| { let (started, ready) = tokio::sync::oneshot::channel(); let _task = root .spawn_sync(move || { started.send(()).unwrap(); std::thread::sleep(std::time::Duration::from_millis(30)); finished.store(true, Ordering::SeqCst); }) .unwrap(); ready.await.unwrap(); panic!("root failure after worker started"); }); assert_eq!(result, Err(Error::Panicked)); assert!(observed.load(Ordering::SeqCst)); } #[test] fn root_panic_cancels_leaf_work_and_allows_the_next_entry() { let runtime = runtime(); let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); let result = runtime.run(async |root| { let (started, ready) = tokio::sync::oneshot::channel(); let _task = root .spawn_leaf(move || async move { let _owned = DropNotice(dropped); started.send(()).unwrap(); std::future::pending::<()>().await; }) .unwrap(); ready.await.unwrap(); panic!("root failure after leaf started"); }); assert_eq!(result, Err(Error::Panicked)); assert!(observed.load(Ordering::SeqCst)); let result = runtime.run(async |root| { root.spawn_leaf(|| async { Err::<(), _>(String::from("business failure")) }) .unwrap() .wait() .await .unwrap() }); assert_eq!(result, Ok(Err(String::from("business failure")))); } #[test] fn abandoned_task_results_are_dropped_before_scope_finish_returns() { let runtime = runtime(); for direct in [false, true] { let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); runtime .run(async |scope| { let (started, ready) = tokio::sync::oneshot::channel(); let call = move || { started.send(()).unwrap(); std::thread::sleep(std::time::Duration::from_millis(20)); DropNotice(dropped) }; let task = if direct { scope .spawn(move |child| async move { child.blocking(call).await.unwrap() }) .unwrap() } else { scope.spawn_sync(call).unwrap() }; ready.await.unwrap(); drop(task); scope.finish().await; assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } } #[test] fn finished_tasks_drop_unclaimed_outputs_before_scope_returns() { let runtime = Runtime::new(Limits { threads: 1, workers: 1, tasks: 8, }) .unwrap(); for leaf in [false, true] { let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); runtime .run(async |root| { let (completed, ready) = tokio::sync::oneshot::channel(); let call = move || async move { // 唯一 worker 在本任务完成后才会运行 marker,不用睡眠猜测完成时点。 tokio::spawn(async move { completed.send(()).unwrap(); }); DropNotice(dropped) }; let task = if leaf { root.spawn_leaf(call).unwrap() } else { root.spawn(move |_child| call()).unwrap() }; ready.await.unwrap(); drop(task); root.finish().await; assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } } #[test] fn unclaimed_panicking_leaf_drops_owned_values_before_scope_finishes() { let finished = Arc::new(AtomicBool::new(false)); let observed = finished.clone(); let (started, ready) = tokio::sync::oneshot::channel(); runtime() .run(async |scope| { let task = scope .spawn_leaf(move || async move { let _owned = DropNotice(finished); started.send(()).unwrap(); panic!("unclaimed leaf panic"); }) .unwrap(); ready.await.unwrap(); drop(task); scope.finish().await; assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn parent_panic_drains_started_workers_before_reporting_failure() { let finished = Arc::new(AtomicBool::new(false)); let observed = finished.clone(); runtime() .run(async |root| { let parent = root .spawn(move |child| async move { let (started, ready) = tokio::sync::oneshot::channel(); let _worker = child .spawn_sync(move || { started.send(()).unwrap(); std::thread::sleep(std::time::Duration::from_millis(20)); finished.store(true, Ordering::SeqCst); }) .unwrap(); ready.await.unwrap(); panic!("parent panic after worker started"); }) .unwrap(); assert_eq!(parent.wait().await, Err(Error::Panicked)); assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn cancelling_a_leaf_waits_for_its_pending_future_destructor() { let finished = Arc::new(AtomicBool::new(false)); let observed = finished.clone(); let (started, ready) = tokio::sync::oneshot::channel(); runtime() .run(async |root| { let leaf = root .spawn_leaf(move || async move { let _owned = DropNotice(finished); started.send(()).unwrap(); std::future::pending::<()>().await; }) .unwrap(); ready.await.unwrap(); assert_eq!(leaf.stop().await, Ok(None)); assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn cancelling_a_parent_drains_its_suspended_leaf() { let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); let (started, ready) = tokio::sync::oneshot::channel(); runtime() .run(async |root| { let parent = root .spawn(move |child| async move { let leaf = child .spawn_leaf(move || async move { let _owned = DropNotice(dropped); started.send(()).unwrap(); std::future::pending::<()>().await; }) .unwrap(); leaf.wait().await.unwrap(); }) .unwrap(); ready.await.unwrap(); assert_eq!(parent.stop().await, Ok(None)); assert!(observed.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn cancelling_completed_leaves_preserves_business_errors_and_panics() { let runtime = Runtime::new(Limits { threads: 1, workers: 1, tasks: 8, }) .unwrap(); for panicked in [false, true] { runtime .run(async |root| { let (completed, ready) = tokio::sync::oneshot::channel(); let task = root .spawn_leaf(move || async move { // 唯一 worker 只能在本任务完成清理后再运行 marker,避免靠睡眠猜测完成。 tokio::spawn(async move { completed.send(()).unwrap(); }); if panicked { panic!("completed leaf panic"); } Err::<(), _>("completed failure") }) .unwrap(); ready.await.unwrap(); let result = task.stop().await; if panicked { assert_eq!(result, Err(Error::Panicked)); } else { assert_eq!(result, Ok(Some(Err("completed failure")))); } }) .unwrap(); } } #[test] fn unpolled_leaf_inputs_are_dropped_before_scope_finishes() { let runtime = Runtime::new(Limits { threads: 1, workers: 1, tasks: 8, }) .unwrap(); let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); let entered = Arc::new(AtomicBool::new(false)); let polled = entered.clone(); runtime .run(async |root| { // 单线程入口在这里不挂起,子任务第一次 poll 前就会收到中止。 let input = DropNotice(dropped); let task = root .spawn_leaf(move || async move { entered.store(true, Ordering::SeqCst); let _input = input; std::future::pending::<()>().await; }) .unwrap(); drop(task); root.finish().await; assert!(observed.load(Ordering::SeqCst)); assert!(!polled.load(Ordering::SeqCst)); }) .unwrap(); } #[test] fn leaf_entries_drop_future_state_and_preserve_results_and_panics() { let runtime = runtime(); for panicked in [false, true] { let dropped = Arc::new(AtomicBool::new(false)); let observed = dropped.clone(); let result = runtime.run_leaf(async move { let _value = DropNotice(dropped); tokio::task::yield_now().await; if panicked { panic!("leaf entry panic"); } String::from("owned result") }); assert!(observed.load(Ordering::SeqCst)); if panicked { assert_eq!(result, Err(Error::Panicked)); } else { assert_eq!(result.unwrap(), "owned result"); } } }