From df39dd80b90d678d893ff21d576255e368303362 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Sun, 18 Jan 2026 20:23:25 +0100 Subject: [PATCH 1/7] Don't use channels to send dependencies --- Cargo.lock | 21 ++++ Cargo.toml | 13 ++- README.md | 10 +- dagx-macros/src/lib.rs | 51 +++------ docs/COMPARISONS.md | 2 +- docs/CYCLE_PREVENTION.md | 2 +- src/builder.rs | 4 +- src/error.rs | 2 +- src/extract.rs | 135 ++++++++++-------------- src/lib.rs | 16 +-- src/node.rs | 52 +--------- src/runner.rs | 216 +++++++++++++++++---------------------- src/task.rs | 8 +- src/types.rs | 2 +- 14 files changed, 223 insertions(+), 311 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce288a7..e680cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,6 +442,7 @@ dependencies = [ "criterion", "dagrs", "dagx-macros", + "dashmap", "futures", "futures-executor", "parking_lot", @@ -461,6 +462,20 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "either" version = "1.15.0" @@ -641,6 +656,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hermit-abi" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 55c9914..ed84368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,16 +22,25 @@ default = [] tracing = ["dep:tracing"] [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpaulin_include)'] } +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(tarpaulin)', + 'cfg(tarpaulin_include)', +] } [dependencies] futures = "0.3" parking_lot = "0.12" dagx-macros = { version = "0.3.1", path = "./dagx-macros" } tracing = { version = "0.1", optional = true } +dashmap = "6.1.0" [dev-dependencies] -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] } +tokio = { version = "1", features = [ + "macros", + "rt-multi-thread", + "time", + "sync", +] } async-std = { version = "1", features = ["attributes"] } smol = "2" criterion = "0.7" diff --git a/README.md b/README.md index 4cc971f..2272c57 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ See [how it works](docs/CYCLE_PREVENTION.md). ```rust let sum = dag.add_task(Add).depends_on((&x, &y)); -dag.run(|fut| tokio::spawn(fut)).await?; +dag.run().await?; ``` That's it. No trait boilerplate, no manual channels, no node IDs. @@ -90,7 +90,7 @@ async fn main() { let sum = dag.add_task(Add).depends_on((&x, &y)); // Execute with true parallelism - dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); + dag.run().await.unwrap(); // Retrieve results assert_eq!(dag.get(sum).unwrap(), 5); @@ -223,13 +223,13 @@ dagx works with any async runtime. Provide a spawner function to `run()`: ```rust // With Tokio -dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +dag.run().await.unwrap(); // With async-std -dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); +dag.run().await.unwrap(); // With smol -dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); +dag.run().await.unwrap(); ``` ## Tutorials & Examples diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index f7d7fb5..4339bb3 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -335,7 +335,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { fn extract_and_run( self, - _receivers: Vec>, + _dependencies: Vec>, ) -> impl std::future::Future> + Send { async move { let input = (); @@ -350,24 +350,20 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { fn extract_and_run( self, - mut receivers: Vec>, + mut dependencies: Vec>, ) -> impl std::future::Future> + Send { async move { - use futures::channel::oneshot; use std::sync::Arc; - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers.pop() + let arc_value = dependencies.pop() .unwrap() - .downcast::>>() + .downcast::<#param_type>() .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<#param_type>()))?; - let arc_value = rx.await - .map_err(|_| "Channel closed before receiving value".to_string())?; - let input = (*arc_value).clone(); Ok(self.run(input).await) } @@ -380,51 +376,36 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { let param_types: Vec<_> = params.iter().map(|(_, ty)| ty).collect(); let indices: Vec<_> = (0..param_count).collect(); - // Generate unique receiver variable names - let rx_vars: Vec<_> = (0..param_count) - .map(|i| syn::Ident::new(&format!("rx_{}", i), proc_macro2::Span::call_site())) - .collect(); - // Create syn::Index for tuple field access (avoids the suffix warning) let syn_indices: Vec<_> = (0..param_count).map(syn::Index::from).collect(); quote! { fn extract_and_run( self, - receivers: Vec>, + dependencies: Vec>, ) -> impl std::future::Future> + Send { async move { - use futures::channel::oneshot; use std::sync::Arc; let expected_count = #param_count; - if receivers.len() != expected_count { - return Err(format!("Expected {} dependencies, got {}", expected_count, receivers.len())); + if dependencies.len() != expected_count { + return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len())); } - let mut iter = receivers.into_iter(); + let mut iter = dependencies.into_iter(); // Extract each receiver - #( - let #rx_vars = *iter.next() + let arc_results = (#( + iter.next() .ok_or_else(|| format!("Missing receiver at index {}", #indices))? - .downcast::>>() + .downcast::<#param_types>() .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", - #indices, std::any::type_name::<#param_types>()))?; - )* - - // Await all channels concurrently - let arc_results = futures::join!( - #( - async move { - #rx_vars.await.map_err(|_| format!("Channel {} closed", #indices)) - } - ),* - ); + #indices, std::any::type_name::<#param_types>()))?, + )*); // Clone inner values and build tuple let input = (#( - (*arc_results.#syn_indices?).clone() + (*arc_results.#syn_indices).clone() ),*); Ok(self.run(input).await) diff --git a/docs/COMPARISONS.md b/docs/COMPARISONS.md index ad30468..2c53ccc 100644 --- a/docs/COMPARISONS.md +++ b/docs/COMPARISONS.md @@ -19,7 +19,7 @@ let dag = DagRunner::new(); let x = dag.add_task(Value(2)); let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); -dag.run(|fut| tokio::spawn(fut)).await?; +dag.run().await?; ``` **dagrs** (Complex: traits + channels + IDs + manual wiring): diff --git a/docs/CYCLE_PREVENTION.md b/docs/CYCLE_PREVENTION.md index fbba17e..839609c 100644 --- a/docs/CYCLE_PREVENTION.md +++ b/docs/CYCLE_PREVENTION.md @@ -13,7 +13,7 @@ let a = dag.add_task(TaskA); let b = dag.add_task(TaskB); a.depends_on(b); b.depends_on(a); // Compiles fine... -dag.run().await?; // ❌ Error: "Cycle detected!" (at runtime) +dag.run() ``` **dagx prevents this at compile time:** diff --git a/src/builder.rs b/src/builder.rs index d5bba4e..b4e6b40 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -65,7 +65,7 @@ impl IsUnitType for () {} /// let b = b.depends_on(&a); /// // Now b is a TaskHandle /// -/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +/// dag.run().await.unwrap(); /// assert_eq!(dag.get(&b).unwrap(), 20); /// # }; /// ``` @@ -125,7 +125,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// // Multiple dependencies: tuple form /// let sum = dag.add_task(Add).depends_on((&x, &y)); /// - /// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); + /// dag.run().await.unwrap(); /// # }; /// ``` pub fn depends_on(self, deps: D) -> TaskHandle diff --git a/src/error.rs b/src/error.rs index b7afd57..7cbb01a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,7 +63,7 @@ impl std::fmt::Display for DagError { f, "Result not found for task #{}.\n\ \n\ - Call dag.run() before accessing results with get().", + Call dag.run().", task_id ) } diff --git a/src/extract.rs b/src/extract.rs index f1b1b54..7c9f06f 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -11,7 +11,7 @@ //! //! # How it works //! -//! When a task executes, it needs to extract its inputs from type-erased oneshot receivers. +//! When a task executes, it needs to extract its inputs from type-erased oneshot dependencies. //! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones). //! //! **Modern approach (via `#[task]` macro)**: @@ -35,8 +35,6 @@ use std::any::Any; use std::sync::Arc; -use futures::channel::oneshot; - /// Helper trait for async input extraction from oneshot channels. /// /// Type erasure occurs only at the ExecutableNode trait boundary - by the time @@ -47,14 +45,14 @@ use futures::channel::oneshot; /// which allows ANY type to work without requiring ExtractInput implementations. #[allow(dead_code)] pub(crate) trait ExtractInput: Sized + Clone { - fn extract_from_channels( - receivers: Vec>, + fn extract_from_deps( + deps: Vec>, ) -> impl std::future::Future> + Send; } // Unit type - no dependencies impl ExtractInput for () { - async fn extract_from_channels(_receivers: Vec>) -> Result { + async fn extract_from_deps(_deps: Vec>) -> Result { Ok(()) } } @@ -67,15 +65,14 @@ macro_rules! impl_extract_single { ($($t:ty),+) => { $( impl ExtractInput for $t { - async fn extract_from_channels(mut receivers: Vec>) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + async fn extract_from_deps(mut dependencies: Vec>) -> Result { + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers.pop() + let arc_value = dependencies.pop() .unwrap() - .downcast::>>() + .downcast::<$t>() .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<$t>()))?; - let arc_value = rx.await.map_err(|_| "Channel closed before receiving value".to_string())?; Ok((*arc_value).clone()) } } @@ -93,16 +90,16 @@ where T: 'static + Clone + Send + Sync, E: 'static + Clone + Send + Sync, { - async fn extract_from_channels( - mut receivers: Vec>, + async fn extract_from_deps( + mut dependencies: Vec>, ) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers + let arc_value = dependencies .pop() .unwrap() - .downcast::>>>() + .downcast::>() .map_err(|_| { format!( "Type mismatch: expected Arc>", @@ -110,9 +107,6 @@ where std::any::type_name::() ) })?; - let arc_value = rx - .await - .map_err(|_| "Channel closed before receiving value".to_string())?; Ok((*arc_value).clone()) } } @@ -122,25 +116,22 @@ impl ExtractInput for Option where T: 'static + Clone + Send + Sync, { - async fn extract_from_channels( - mut receivers: Vec>, + async fn extract_from_deps( + mut dependencies: Vec>, ) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers + let arc_value = dependencies .pop() .unwrap() - .downcast::>>>() + .downcast::>() .map_err(|_| { format!( "Type mismatch: expected Arc>", std::any::type_name::() ) })?; - let arc_value = rx - .await - .map_err(|_| "Channel closed before receiving value".to_string())?; Ok((*arc_value).clone()) } } @@ -150,25 +141,22 @@ impl ExtractInput for Vec where T: 'static + Clone + Send + Sync, { - async fn extract_from_channels( - mut receivers: Vec>, + async fn extract_from_deps( + mut dependencies: Vec>, ) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers + let arc_value = dependencies .pop() .unwrap() - .downcast::>>>() + .downcast::>() .map_err(|_| { format!( "Type mismatch: expected Arc>", std::any::type_name::() ) })?; - let arc_value = rx - .await - .map_err(|_| "Channel closed before receiving value".to_string())?; Ok((*arc_value).clone()) } } @@ -179,16 +167,16 @@ where K: 'static + Clone + Send + Sync + Eq + std::hash::Hash, V: 'static + Clone + Send + Sync, { - async fn extract_from_channels( - mut receivers: Vec>, + async fn extract_from_deps( + mut dependencies: Vec>, ) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers + let arc_value = dependencies .pop() .unwrap() - .downcast::>>>() + .downcast::>() .map_err(|_| { format!( "Type mismatch: expected Arc>", @@ -196,9 +184,6 @@ where std::any::type_name::() ) })?; - let arc_value = rx - .await - .map_err(|_| "Channel closed before receiving value".to_string())?; Ok((*arc_value).clone()) } } @@ -209,25 +194,22 @@ impl ExtractInput for Arc where T: 'static + Clone + Send + Sync, { - async fn extract_from_channels( - mut receivers: Vec>, + async fn extract_from_deps( + mut dependencies: Vec>, ) -> Result { - if receivers.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", receivers.len())); + if dependencies.len() != 1 { + return Err(format!("Expected 1 dependency, got {}", dependencies.len())); } - let rx = *receivers + let arc_arc_value = dependencies .pop() .unwrap() - .downcast::>>>() + .downcast::>() .map_err(|_| { format!( "Type mismatch: expected Arc>", std::any::type_name::() ) })?; - let arc_arc_value = rx - .await - .map_err(|_| "Channel closed before receiving value".to_string())?; // Return the inner Arc (clone is cheap - just refcount increment) Ok(Arc::clone(&*arc_arc_value)) } @@ -251,43 +233,32 @@ where macro_rules! impl_extract_tuple { ($($T:ident : $idx:tt),+) => { impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) { - async fn extract_from_channels(receivers: Vec>) -> Result { - // Verify we have the correct number of receivers + async fn extract_from_deps(dependencies: Vec>) -> Result { + // Verify we have the correct number of dependencies let expected_count = [$($idx),+].len(); - if receivers.len() != expected_count { - return Err(format!("Expected {} dependencies, got {}", expected_count, receivers.len())); + if dependencies.len() != expected_count { + return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len())); } // Convert Vec to iterator so we can consume it - let mut iter = receivers.into_iter(); + let mut iter = dependencies.into_iter(); - // Extract each receiver by consuming from the iterator - // Receivers now contain Arc-wrapped values for efficient fanout + // dependencies now contain Arc-wrapped values for efficient fanout // Dereference Box after downcasting to get the receiver itself // Use type names as variable names to avoid numeric suffix issues - $( - #[allow(non_snake_case)] - let $T = *iter.next() - .ok_or_else(|| format!("Missing receiver at index {}", $idx))? - .downcast::>>() - .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", - $idx, std::any::type_name::<$T>()))?; - )+ - - // Await all channels concurrently - this is a key optimization! - // We can start receiving as soon as ANY dependency completes, not waiting - // for them sequentially. - let arc_results = futures::join!( + let arc_results = ( $( - async move { - $T.await.map_err(|_| format!("Channel {} closed", $idx)) - } - ),+ + iter.next() + .ok_or_else(|| format!("Missing receiver at index {}", $idx))? + .downcast::<$T>() + .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", + $idx, std::any::type_name::<$T>()))?, + )+ ); // Clone inner values from Arc (cheap for small types, necessary for ownership) Ok(($( - (*arc_results.$idx?).clone() + (*arc_results.$idx).clone() ,)+)) } } diff --git a/src/lib.rs b/src/lib.rs index 5a4660d..903fe3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ //! let sum = dag.add_task(Add).depends_on((&x, &y)); //! //! // Execute and retrieve results -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! assert_eq!(dag.get(sum).unwrap(), 5); //! # }; //! ``` @@ -322,7 +322,7 @@ //! let plus1 = dag.add_task(Add(1)).depends_on(&base); //! let times2 = dag.add_task(Scale(2)).depends_on(&base); //! -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! //! assert_eq!(dag.get(plus1).unwrap(), 11); //! assert_eq!(dag.get(times2).unwrap(), 20); @@ -378,7 +378,7 @@ //! let active = dag.add_task(Active(true)); //! let result = dag.add_task(FormatUser).depends_on((&name, &age, &active)); //! -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! //! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true"); //! # }; @@ -430,7 +430,7 @@ //! // Layer 3: Final result //! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20 //! -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! //! assert_eq!(dag.get(total).unwrap(), 20); //! # }; @@ -473,7 +473,7 @@ //! let user = dag.add_task(CreateUser); //! let formatted = dag.add_task(FormatUser).depends_on(&user); //! -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! //! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old"); //! # }; @@ -539,11 +539,11 @@ //! # let dag = DagRunner::new(); //! # let node = dag.add_task(Value(42)); //! // Simple approach with .unwrap() -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! let result = dag.get(node).unwrap(); //! //! // Or handle errors explicitly -//! match dag.run(|fut| { tokio::spawn(fut); }).await { +//! match dag.run().await { //! Ok(_) => println!("DAG executed successfully"), //! Err(e) => eprintln!("DAG execution failed: {}", e), //! } @@ -656,7 +656,7 @@ //! let task2 = dag.add_task(ProcessData).depends_on(&data); //! let task3 = dag.add_task(ProcessData).depends_on(&data); //! -//! dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +//! dag.run().await.unwrap(); //! # }; //! ``` //! diff --git a/src/node.rs b/src/node.rs index ee0a24c..41d3b27 100644 --- a/src/node.rs +++ b/src/node.rs @@ -11,18 +11,10 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use futures::channel::oneshot; - use crate::error::DagError; use crate::task::Task; use crate::types::NodeId; -/// Type-erased channel endpoints (senders or receivers) -type ErasedChannels = Vec>; - -/// Type-erased channel pair (senders, receivers) -type ChannelPair = (ErasedChannels, ErasedChannels); - /// Type-erased async execution result (Arc-wrapped for efficient fanout) type ExecuteFuture = Pin, DagError>> + Send>>; @@ -38,21 +30,13 @@ type ExecuteFuture = /// /// This is an implementation detail and not part of the public API. pub(crate) trait ExecutableNode: Send { - /// Create type-erased output channels for this node's dependents. - /// - /// Returns (senders, receivers) where: - /// - senders: Type-erased senders that this node will use to send outputs - /// - receivers: Type-erased receivers that dependents will use to receive inputs - fn create_output_channels(&self, num_dependents: usize) -> ChannelPair; - /// Execute the task with type-erased input receivers and output senders. /// /// The TypedNode implementation knows the concrete types and can safely downcast. /// Consumes the node and returns the output value. - fn execute_with_channels( + fn execute_with_deps( self: Box, - receivers: ErasedChannels, - senders: ErasedChannels, + dependencies: Vec>, ) -> ExecuteFuture; } @@ -86,30 +70,15 @@ where T::Input: 'static + Clone, T::Output: 'static + Clone, { - fn create_output_channels(&self, num_dependents: usize) -> ChannelPair { - let mut senders = Vec::new(); - let mut receivers = Vec::new(); - - for _ in 0..num_dependents { - // Create channels that pass Arc-wrapped outputs (efficient for fanout) - let (tx, rx) = oneshot::channel::>(); - senders.push(Box::new(tx) as Box); - receivers.push(Box::new(rx) as Box); - } - - (senders, receivers) - } - - fn execute_with_channels( + fn execute_with_deps( self: Box, - receivers: ErasedChannels, - senders: ErasedChannels, + dependencies: Vec>, ) -> ExecuteFuture { Box::pin(async move { // Use the task's extract_and_run method which has inline extraction logic // generated by the #[task] macro. This allows ANY type to work without // requiring ExtractInput trait implementations. - let output = match self.task.extract_and_run(receivers).await { + let output = match self.task.extract_and_run(dependencies).await { Ok(output) => output, Err(_msg) => { // Failed to extract input - likely channel closed or type mismatch @@ -124,17 +93,6 @@ where // Downstream tasks receive &T after extraction from Arc. let arc_output = Arc::new(output); - // Send Arc to all dependents via their channels - // TypedNode knows T::Output, so downcast is safe - for sender_any in senders { - if let Ok(sender) = sender_any.downcast::>>() { - // Clone Arc (just atomic refcount increment, NOT data clone) - // This is the key performance optimization for fan-out patterns - // Ignore send errors - dependent might have been cancelled - let _ = sender.send(Arc::clone(&arc_output)); - } - } - // Return Arc-wrapped output (for storage by runner) Ok(arc_output as Arc) }) diff --git a/src/runner.rs b/src/runner.rs index 65258bb..c1ab82c 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -5,6 +5,7 @@ //! //! Uses Mutex for interior mutability to enable builder pattern (`&self` instead of `&mut self`). +use std::any::Any; use std::collections::{HashMap, HashSet, VecDeque}; use std::panic::AssertUnwindSafe; use std::sync::{ @@ -12,9 +13,9 @@ use std::sync::{ Arc, }; -use futures::channel::mpsc; -use futures::future::BoxFuture; -use futures::{FutureExt, StreamExt}; +use dashmap::DashMap; +use futures::stream::FuturesUnordered; +use futures::{FutureExt, StreamExt, TryFutureExt}; use parking_lot::Mutex; #[cfg(feature = "tracing")] @@ -82,7 +83,7 @@ impl<'a> Drop for RunGuard<'a> { /// let y = dag.add_task(LoadValue::new(3)); /// let sum = dag.add_task(Add).depends_on((&x, &y)); /// -/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +/// dag.run().await.unwrap(); /// /// assert_eq!(dag.get(sum).unwrap(), 5); /// # }; @@ -95,7 +96,7 @@ impl<'a> Drop for RunGuard<'a> { /// Outputs are Arc-wrapped and stored separately for retrieval via get(). /// Arc enables efficient sharing during fanout without cloning data. pub struct DagRunner { - pub(crate) nodes: Mutex>>>, + pub(crate) nodes: Mutex>>>, pub(crate) outputs: Mutex>>, pub(crate) edges: Mutex>>, // node -> dependencies pub(crate) dependents: Mutex>>, // node -> tasks that depend on it @@ -188,13 +189,13 @@ impl DagRunner { /// // Construct task with offset of 1 /// let inc = dag.add_task(AddOffset::new(1)).depends_on(&base); /// - /// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); + /// dag.run().await.unwrap(); /// assert_eq!(dag.get(&inc).unwrap(), 11); /// # }; /// ``` pub fn add_task(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending> where - Tk: Task + 'static, + Tk: Task + Sync + 'static, Tk::Input: 'static + Clone, Tk::Output: 'static + Clone, { @@ -272,7 +273,7 @@ impl DagRunner { /// let b = dag.add_task(Value(2)); /// let sum = dag.add_task(Add).depends_on((&a, &b)); /// - /// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); // Executes all tasks + /// dag.run().await.unwrap(); // Executes all tasks /// # }; /// ``` /// @@ -283,11 +284,8 @@ impl DagRunner { /// Type erasure occurs only at the ExecutableNode trait boundary - channels are created /// with full type information and type-erased just before passing to execute_with_channels. #[inline] - #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))] - pub async fn run(&self, spawner: S) -> DagResult<()> - where - S: Fn(BoxFuture<'static, ()>), - { + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + pub async fn run(&self) -> DagResult<()> { #[cfg(feature = "tracing")] info!("starting DAG execution"); // Acquire run lock to prevent concurrent executions @@ -320,47 +318,8 @@ impl DagRunner { let edges = self.edges.lock().clone(); - // Create all channels upfront - // Map: (producer_id, consumer_id) -> receiver index - // We store receivers in a Vec per consumer to maintain order - let mut consumer_receivers: HashMap>> = - HashMap::new(); - let mut producer_senders: HashMap>> = - HashMap::new(); - - // Create channels for each producer-consumer relationship - // We need to lock nodes temporarily to call create_output_channels - { - let nodes_lock = self.nodes.lock(); - - for (consumer_id, producer_ids) in &edges { - for &producer_id in producer_ids { - // Get the producer node (it's still in the vector at this point) - if let Some(Some(producer_node)) = nodes_lock.get(producer_id.0) { - // Ask the producer to create ONE channel for this consumer - let (mut senders, mut receivers) = producer_node.create_output_channels(1); - - // Store the sender for the producer - producer_senders - .entry(producer_id) - .or_default() - .push(senders.pop().unwrap()); - - // Store the receiver for the consumer (order matters!) - consumer_receivers - .entry(*consumer_id) - .or_default() - .push(receivers.pop().unwrap()); - } - } - } - } - - // Create a channel to collect Arc-wrapped outputs from all tasks - let (output_tx, mut output_rx) = mpsc::unbounded::<( - NodeId, - Result, DagError>, - )>(); + let outputs: DashMap> = DashMap::new(); + let mut first_error = None; for layer in layers { #[cfg(feature = "tracing")] @@ -402,8 +361,6 @@ impl DagRunner { "executing task inline (single-task layer optimization)" ); - let out_tx = output_tx.clone(); - // Take ownership of the node let node = { let mut nodes_lock = self.nodes.lock(); @@ -411,11 +368,11 @@ impl DagRunner { }; if let Some(node) = node { - // Take the receivers for this consumer - let receivers = consumer_receivers.remove(&node_id).unwrap_or_default(); - - // Take the senders for this producer - let senders = producer_senders.remove(&node_id).unwrap_or_default(); + let dependencies: Vec<_> = edges[&node_id] + .iter() + .flat_map(|dep| outputs.get(dep)) + .map(|value| value.clone()) + .collect(); // Execute inline with panic handling. // @@ -423,7 +380,7 @@ impl DagRunner { // DagError::TaskPanicked, matching the behavior of async runtimes when // they spawn tasks. This guarantees consistent error handling whether // a task executes inline (single-task layer) or spawned (multi-task layer). - let result = AssertUnwindSafe(node.execute_with_channels(receivers, senders)) + let result = AssertUnwindSafe(node.execute_with_deps(dependencies)) .catch_unwind() .await .unwrap_or_else(|panic_payload| { @@ -451,79 +408,94 @@ impl DagRunner { }); // Send the output (ignore send errors - receiver may be dropped) - let _ = out_tx.unbounded_send((node_id, result)); + match result { + Ok(output) => { + outputs.insert(node_id, output); + } + Err(e) => { + first_error.get_or_insert(e); + } + } } } else { // Slow path: Multiple tasks require spawning and coordination - // Create a channel to track task completion for this layer - let (tx, mut rx) = mpsc::channel::<()>(layer.len()); - // Spawn each task in this layer - for &node_id in &layer { - #[cfg(feature = "tracing")] - trace!(task_id = node_id.0, "spawning task"); - let mut task_tx = tx.clone(); - let out_tx = output_tx.clone(); + let outputs = &outputs; + let mut futures: FuturesUnordered<_> = FuturesUnordered::new(); + layer + .into_iter() + .flat_map(|node_id| { + #[cfg(feature = "tracing")] + trace!(task_id = node_id.0, "spawning task"); - // Take ownership of the node - let node = { - let mut nodes_lock = self.nodes.lock(); - nodes_lock[node_id.0].take() - }; - - if let Some(node) = node { - // Take the receivers for this consumer - let receivers = consumer_receivers.remove(&node_id).unwrap_or_default(); - - // Take the senders for this producer - let senders = producer_senders.remove(&node_id).unwrap_or_default(); - - // Create a 'static future by taking ownership - let inner_future = async move { - let task_future = node.execute_with_channels(receivers, senders); - - let result = task_future.await; - - // Send the output (ignore send errors - receiver may be dropped) - let _ = out_tx.unbounded_send((node_id, result)); - - // Signal completion (ignore send errors - receiver may be dropped) - let _ = task_tx.try_send(()); + // Take ownership of the node + let node = { + let mut nodes_lock = self.nodes.lock(); + nodes_lock[node_id.0].take() }; + if let Some(node) = node { + let dependencies: Vec<_> = edges[&node_id] + .iter() + .flat_map(|dep| outputs.get(dep)) + .map(|value| value.clone()) + .collect(); - // Spawn the task using the provided spawner - spawner(Box::pin(inner_future)); + // Spawn the task using the provided spawner + let inner_future = async move { + let result = node.execute_with_deps(dependencies).await?; + outputs.insert(node_id, result); + Ok(()) + }; + + Some( + AssertUnwindSafe(Box::pin(inner_future)) + .catch_unwind() + .unwrap_or_else(move |panic_payload| { + // Convert panic to error + let panic_message = + if let Some(s) = panic_payload.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = + panic_payload.downcast_ref::() + { + s.clone() + } else { + "unknown panic".to_string() + }; + + #[cfg(feature = "tracing")] + error!( + task_id = node_id.0, + panic_message = %panic_message, + "task panicked during inline execution" + ); + + Err(DagError::TaskPanicked { + task_id: node_id.0, + panic_message, + }) + }), + ) + } else { + None + } + }) + .for_each(|fut| futures.push(fut)); + + while let Some(out) = futures.next().await { + if let Err(e) = out { + first_error.get_or_insert(e); } } - - // Drop the original sender so the channel closes when all tasks complete - drop(tx); - - // Wait for all tasks in this layer to complete - while rx.next().await.is_some() {} - } - } - - // Drop the output sender so the channel closes - drop(output_tx); - - // Collect all outputs and check for errors - let mut collected = Vec::new(); - let mut first_error = None; - while let Some((node_id, result)) = output_rx.next().await { - match result { - Ok(output) => collected.push((node_id, output)), - Err(e) if first_error.is_none() => first_error = Some(e), - Err(_) => {} // Ignore subsequent errors } } // Insert all successful outputs (avoiding holding lock across await) - let mut outputs = self.outputs.lock(); - for (node_id, output) in collected { - outputs.insert(node_id, output); + let mut outputs_lock = self.outputs.lock(); + for (node_id, output) in outputs { + outputs_lock.insert(node_id, output); } - drop(outputs); + drop(outputs_lock); // Return first error if any if let Some(err) = first_error { @@ -575,7 +547,7 @@ impl DagRunner { /// // Construct task with specific setting value /// let task = dag.add_task(Configuration::new(42)); /// - /// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); + /// dag.run().await.unwrap(); /// /// assert_eq!(dag.get(task).unwrap(), 42); /// # }; diff --git a/src/task.rs b/src/task.rs index f362e87..54ee361 100644 --- a/src/task.rs +++ b/src/task.rs @@ -3,7 +3,7 @@ //! This module defines the core Task trait that all DAG nodes must implement, //! along with convenience functions for creating tasks from closures. -use std::future::Future; +use std::{future::Future, sync::Arc}; /// A unit of async work with typed inputs and outputs. /// @@ -106,7 +106,7 @@ pub trait Task: Send { #[doc(hidden)] fn extract_and_run( self, - receivers: Vec>, + dependencies: Vec>, ) -> impl Future> + Send; } @@ -149,10 +149,10 @@ where async fn extract_and_run( self, - receivers: Vec>, + dependencies: Vec>, ) -> Result { // For task_fn, we use ExtractInput since we don't have macro-generated extraction - let input = I::extract_from_channels(receivers) + let input = I::extract_from_deps(dependencies) .await .map_err(|e| format!("Failed to extract input: {}", e))?; diff --git a/src/types.rs b/src/types.rs index cca4e5d..7968d60 100644 --- a/src/types.rs +++ b/src/types.rs @@ -33,7 +33,7 @@ pub(crate) struct NodeId(pub usize); /// let dag = DagRunner::new(); /// let node = dag.add_task(LoadValue::new(42)); /// -/// dag.run(|fut| { tokio::spawn(fut); }).await.unwrap(); +/// dag.run().await.unwrap(); /// /// assert_eq!(dag.get(node).unwrap(), 42); /// # }; From f4160d2387565f0168ef39c3af73fbdd53f795fd Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Tue, 20 Jan 2026 21:03:07 +0100 Subject: [PATCH 2/7] Update docs --- README.md | 12 ++++--- dagx-macros/src/lib.rs | 4 +-- docs/DESIGN_PHILOSOPHY.md | 49 ++++++++++++++--------------- src/extract.rs | 13 +++----- src/extract/coverage_tests.rs | 18 ++++------- src/extract/tests.rs | 59 ++++++++--------------------------- src/lib.rs | 2 +- src/node.rs | 12 +++---- src/node/tests.rs | 36 +++++---------------- src/runner.rs | 20 +++++------- src/task.rs | 4 +-- tests/execution/basic.rs | 54 +++++++++----------------------- tests/parallelism/spawning.rs | 20 +++++++----- 13 files changed, 106 insertions(+), 197 deletions(-) diff --git a/README.md b/README.md index 2272c57..4fb48b9 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ dagx provides true parallel execution with sub-microsecond overhead per task. **Why is dagx so fast?** - **Inline fast-path**: Sequential chains execute inline without spawning (8.9-129x faster) -- **Primitives as scheduler**: No custom scheduler — channels coordinate execution - **Adaptive execution**: Inline for sequential work, true parallelism for concurrent work - **Zero-cost abstractions**: Generics and monomorphization eliminate overhead @@ -223,13 +222,18 @@ dagx works with any async runtime. Provide a spawner function to `run()`: ```rust // With Tokio -dag.run().await.unwrap(); +// The join handle result can be unwrapped because dagx catches panics internally +dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); // With async-std -dag.run().await.unwrap(); +dag.run(|fut| async_std::task::spawn(fut)).await.unwrap(); // With smol -dag.run().await.unwrap(); +dag.run(|fut| smol::spawn(fut)).await.unwrap(); + +// Single-threaded on the invoking runtime +// Faster for situations with many computationally light tasks +dag.run(|fut| fut).await.unwrap() ``` ## Tutorials & Examples diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index 4339bb3..a75bab1 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -394,10 +394,10 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut iter = dependencies.into_iter(); - // Extract each receiver + // Extract each parameter let arc_results = (#( iter.next() - .ok_or_else(|| format!("Missing receiver at index {}", #indices))? + .ok_or_else(|| format!("Missing parameter at index {}", #indices))? .downcast::<#param_types>() .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", #indices, std::any::type_name::<#param_types>()))?, diff --git a/docs/DESIGN_PHILOSOPHY.md b/docs/DESIGN_PHILOSOPHY.md index 59cf447..7c2d29d 100644 --- a/docs/DESIGN_PHILOSOPHY.md +++ b/docs/DESIGN_PHILOSOPHY.md @@ -8,28 +8,23 @@ Traditional DAG executors contain substantial scheduling code—algorithms that dagx eliminates this entirely: -1. **Wire up tasks with primitives**: During DAG construction, tasks are connected using channels and organized into topological layers based on their dependencies +1. **Wire up tasks with primitives**: During DAG construction, tasks are organized into topological layers based on their dependencies 2. **Start everything simultaneously**: When you call `run()`, all tasks spawn at once—there's no scheduler deciding when each task should start -3. **Let the primitives handle coordination**: Channels naturally enforce execution order—tasks wait on their input channels until upstream tasks send data. No custom orchestration needed. -4. **Runtime joins on completion**: The runtime simply spawns all tasks and waits for the completion channel to close. That's it. +3. **Let the primitives handle coordination**: Layers naturally enforce execution order—tasks wait on their dependencies until . No custom orchestration needed. +4. **Runtime joins on completion**: The runtime simply spawns all tasks and waits for them to finish. That's it. ## The Implementation Under the hood, dagx uses: -- **Oneshot channels**: Each edge in the DAG gets a `futures::channel::oneshot` - producer sends once, consumer receives once - **Ownership model**: Tasks take ownership (`self`) and are consumed during execution - no Mutex needed for task state -- **Direct data flow**: Outputs flow from producer to consumer via channels, never stored in shared memory during execution +- **Direct data flow**: Outputs flow from producer(s) to consumer(s) through a shared hash map - **Fast-path optimization**: Single-task layers execute inline without spawning overhead The core execution logic in `run()` is remarkably simple: ```rust -// Create oneshot channel for each edge -for (producer, consumer) in edges { - let (tx, rx) = oneshot::channel(); - // Wire tx to producer, rx to consumer -} +let outputs = HashMap::new(); // Execute layer by layer for layer in layers { @@ -40,25 +35,27 @@ for layer in layers { // Convert panic to error to match spawned task behavior } else { // Parallel path: Spawn all tasks in layer - for task in layer { - spawner(async { - let input = await_on_input_channels(); // Blocks until deps complete + let tasks = layers + .map(async { + let input = get_inputs(map); // Deps are guaranteed to be in the map let output = task.run(input); // Consumes task - send_to_output_channels(output); // Sends to dependents - }); + output + }) + .collect::>(); // Run all tasks in parallel + + while let Some(out) = tasks.next().await { + outputs.insert(out.task_id, out.output); // Make result available for dependents } - // Wait for layer completion } + // Wait for layer completion } -// Tasks coordinate themselves via channels - no scheduler needed +// FuturesUnordered coordinates task execution - no custom scheduler needed ``` -No state machines. No task queues. No wake-up logic. No Mutex locks. Just channels doing what channels do, with an inline fast-path for sequential execution. - ## Inline Execution Fast-Path -**Performance optimization for sequential workloads**: When a layer contains only a single task (common in deep chains and linear pipelines), dagx executes it inline rather than spawning it. This eliminates spawning overhead, context switching, and channel creation, resulting in 10-100x performance improvements for sequential patterns. +**Performance optimization for sequential workloads**: When a layer contains only a single task (common in deep chains and linear pipelines), dagx executes it inline rather than spawning it. This eliminates spawning overhead and context switching, resulting in a 10-100x performance improvements for sequential patterns. **Panic handling guarantee**: To maintain behavioral consistency between inline and spawned execution, panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to errors. This matches the behavior of all major async runtimes (Tokio, async-std, smol, embassy-rs), which catch panics in spawned tasks and convert them to `JoinError` or equivalent. @@ -72,19 +69,19 @@ This ensures your code behaves the same whether a task runs inline or spawned, m ## Benefits -**Simplicity**: The runtime is straightforward: create channels, spawn tasks, let them coordinate via awaiting. No complex scheduler code to maintain, debug, or optimize. +**Simplicity**: The runtime is straightforward: spawn tasks, coordinate via compile-time dependency guarantees. No complex scheduler code to maintain, debug, or optimize. -**Reliability**: Built on battle-tested primitives (oneshot channels, async/await) from Rust's standard library and the futures crate. These have been used in production by thousands of projects and are orders of magnitude more reliable than custom scheduling logic. +**Reliability**: Built on battle-tested primitives from Rust's standard library and the futures crate. These have been used in production by thousands of projects and are orders of magnitude more reliable than custom scheduling logic. -**Bug resistance**: Fewer moving parts means fewer places for bugs to hide. The type system enforces correct wiring at compile time. Channels handle synchronization. The ownership model prevents data races. What's left to break? +**Bug resistance**: Fewer moving parts means fewer places for bugs to hide. The type system enforces correct wiring at compile time. The ownership model prevents data races. What's left to break? -**Performance**: Near zero-overhead. No Mutex locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Oneshot channels are often lock-free. Tasks start as soon as their dependencies complete - maximum parallelism. +**Performance**: Near zero-overhead. No Mutex locks during execution. Arc reference counting for efficient fanout (atomic operations, not locks). Tasks start as soon as their dependencies complete - maximum parallelism. -**Auditability**: Want to verify correctness? Check the channel wiring, verify tasks await their inputs, done. No need to trace through complex state machine transitions or wake-up cascades. +**Auditability**: Want to verify correctness? Check the dependency wiring, verify tasks await their inputs, done. No need to trace through complex state machine transitions or wake-up cascades. ## The Insight -The key insight is that **dependencies ARE the schedule**. If task B depends on task A's output, a channel naturally enforces that B waits for A. The dependency graph already encodes all the scheduling information—we just need to wire up channels to match it. +The key insight is that **dependencies ARE the schedule**. If task B depends on task A's output, the topological sort naturally enforces that B waits for A. The dependency graph already encodes all the scheduling information—we just need to wire outputs to match it. This is dagx's core philosophy: leverage the type system for correctness, use primitives for coordination, and let the compiler optimize everything else away. diff --git a/src/extract.rs b/src/extract.rs index 7c9f06f..a338e12 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,4 +1,4 @@ -//! Input extraction for task dependencies via oneshot channels. +//! Input extraction for task dependencies via a list of dependencies. //! //! # Legacy Trait for Internal Use Only //! @@ -35,7 +35,7 @@ use std::any::Any; use std::sync::Arc; -/// Helper trait for async input extraction from oneshot channels. +/// Helper trait for async input extraction from list of dependencies. /// /// Type erasure occurs only at the ExecutableNode trait boundary - by the time /// we're in ExtractInput, we know the concrete type and can safely downcast. @@ -59,7 +59,7 @@ impl ExtractInput for () { // Implementations for common primitive and standard library types. // These allow tasks to receive these types as single inputs without wrapping in tuples. -// Receives Arc from channel, clones the inner value for task consumption. +// Receives Arc from runner, clones the inner value for task consumption. // For custom types, implement ExtractInput following the same pattern. macro_rules! impl_extract_single { ($($t:ty),+) => { @@ -217,9 +217,6 @@ where /// Macro to implement ExtractInput for tuple types. /// -/// Generates an implementation that extracts values from multiple dependency channels -/// and combines them into a tuple. All channels are awaited concurrently using join_all. -/// /// We need separate implementations for each tuple size (2-8) because Rust doesn't support /// variadic generics. This is standard practice when working with tuples in Rust. /// @@ -244,12 +241,12 @@ macro_rules! impl_extract_tuple { let mut iter = dependencies.into_iter(); // dependencies now contain Arc-wrapped values for efficient fanout - // Dereference Box after downcasting to get the receiver itself + // Dereference Box after downcasting to get the parameter itself // Use type names as variable names to avoid numeric suffix issues let arc_results = ( $( iter.next() - .ok_or_else(|| format!("Missing receiver at index {}", $idx))? + .ok_or_else(|| format!("Missing parameter at index {}", $idx))? .downcast::<$T>() .map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>", $idx, std::any::type_name::<$T>()))?, diff --git a/src/extract/coverage_tests.rs b/src/extract/coverage_tests.rs index 15ec72e..60ea9f8 100644 --- a/src/extract/coverage_tests.rs +++ b/src/extract/coverage_tests.rs @@ -2,7 +2,7 @@ //! //! These tests specifically target type mismatch error messages that aren't hit //! in normal usage because the #[task] macro generates type-safe code. -//! We directly call extract_from_channels with mismatched types. +//! We directly call extract_from_deps with mismatched types. use crate::extract::ExtractInput; use futures::channel::oneshot; @@ -141,19 +141,13 @@ async fn test_hashmap_type_mismatch_coverage() { #[tokio::test] async fn test_arc_type_mismatch_coverage() { // Tests line 211 in extract.rs - Arc type mismatch error message - // Arc is special: it expects Arc> from channel - // Create a channel that sends Arc instead of Arc> - let (tx, rx) = oneshot::channel::>(); - - // Send an Arc value (missing one level of Arc wrapping) - tokio::spawn(async move { - let _ = tx.send(Arc::new("not double arc".to_string())); - }); + // Arc is special: it expects Arc> from dependency + // Create a dependency that provides Arc instead of Arc> + let deps = vec![Arc::new("not double arc".to_string()) as Arc]; // Try to extract as Arc - this will fail with type mismatch - // because it expects Arc> in the channel - let deps = vec![Box::new(rx) as Box]; - let result = Arc::::extract_from_channels(deps).await; + // because it expects Arc> in the dependency + let result = Arc::::extract_from_deps(deps).await; // Verify the error message contains the expected type information assert!(result.is_err()); diff --git a/src/extract/tests.rs b/src/extract/tests.rs index dacbbe9..3102d59 100644 --- a/src/extract/tests.rs +++ b/src/extract/tests.rs @@ -4,7 +4,6 @@ use crate::extract::ExtractInput; use crate::task::Task; -use futures::channel::oneshot; use std::any::Any; use std::sync::Arc; @@ -206,16 +205,9 @@ async fn test_extract_channel_closed() { #[tokio::test] async fn test_extract_type_mismatch() { // Test type mismatch during downcast - let (tx, rx) = oneshot::channel::(); - - // Send a String - tokio::spawn(async move { - let _ = tx.send("hello".to_string()); - }); - - // Try to extract as wrong type - put wrong receiver type in deps - let deps = vec![Box::new(rx) as Box]; - let result = i32::extract_from_channels(deps).await; + // Try to extract as wrong type - put wrong parameter type in deps + let deps = vec![Arc::new("hello".to_string()) as Arc]; + let result = i32::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } @@ -312,13 +304,8 @@ async fn test_hashmap_extract_wrong_dependency_count() { #[tokio::test] async fn test_hashmap_extract_type_mismatch() { - let (tx, rx) = oneshot::channel::>(); - tokio::spawn(async move { - let _ = tx.send(Arc::new("wrong type".to_string())); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = HashMap::::extract_from_channels(deps).await; + let deps = vec![Arc::new("wrong type".to_string()) as Arc]; + let result = HashMap::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } @@ -336,39 +323,24 @@ async fn test_hashmap_extract_channel_closed() { #[tokio::test] async fn test_result_extract_downcast_failure() { - let (tx, rx) = oneshot::channel::>(); - tokio::spawn(async move { - let _ = tx.send(Arc::new("wrong".to_string())); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Result::::extract_from_channels(deps).await; + let deps = vec![Arc::new("wrong".to_string()) as Arc]; + let result = Result::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } #[tokio::test] async fn test_option_extract_downcast_failure() { - let (tx, rx) = oneshot::channel::>(); - tokio::spawn(async move { - let _ = tx.send(Arc::new("wrong".to_string())); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Option::::extract_from_channels(deps).await; + let deps = vec![Arc::new("wrong".to_string()) as Arc]; + let result = Option::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } #[tokio::test] async fn test_vec_extract_downcast_failure() { - let (tx, rx) = oneshot::channel::>(); - tokio::spawn(async move { - let _ = tx.send(Arc::new("wrong".to_string())); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Vec::::extract_from_channels(deps).await; + let deps = vec![Arc::new("wrong".to_string()) as Arc]; + let result = Vec::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } @@ -383,13 +355,8 @@ async fn test_arc_extract_wrong_dependency_count() { #[tokio::test] async fn test_arc_extract_downcast_failure() { - let (tx, rx) = oneshot::channel::>(); - tokio::spawn(async move { - let _ = tx.send(Arc::new("wrong".to_string())); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Arc::::extract_from_channels(deps).await; + let deps = vec![Arc::new("wrong".to_string()) as Arc]; + let result = Arc::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Type mismatch")); } diff --git a/src/lib.rs b/src/lib.rs index 903fe3b..7495f44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ //! dagx automatically optimizes execution for sequential workloads using an inline fast-path. //! When a layer contains only a single task (common in deep chains and linear pipelines), //! that task executes inline rather than being spawned. This eliminates spawning overhead, -//! context switching, and channel creation. +//! and context switching. //! //! **Panic handling**: To maintain consistent behavior between inline and spawned execution, //! panics in inline tasks are caught using `FutureExt::catch_unwind()` and converted to diff --git a/src/node.rs b/src/node.rs index 41d3b27..bd9da25 100644 --- a/src/node.rs +++ b/src/node.rs @@ -25,12 +25,12 @@ type ExecuteFuture = /// collection. While the public API (Task, Handle, Node) is fully type-safe, internally we /// need dynamic dispatch to execute tasks with different input/output types. /// -/// This is the ONLY place where type erasure occurs - channels are created with full type +/// This is the ONLY place where type erasure occurs - tasks are created with full type /// information and only erased at this trait boundary. /// /// This is an implementation detail and not part of the public API. pub(crate) trait ExecutableNode: Send { - /// Execute the task with type-erased input receivers and output senders. + /// Execute the task with type-erased inputs and output /// /// The TypedNode implementation knows the concrete types and can safely downcast. /// Consumes the node and returns the output value. @@ -50,10 +50,7 @@ pub(crate) trait ExecutableNode: Send { /// The task is owned directly (no Mutex needed) and consumed during execution. /// Each task executes exactly once, taking ownership and producing an output. /// -/// # Oneshot Channels -/// -/// Outputs are communicated via oneshot channels for inter-task communication. -/// The output is also returned from execute_with_channels for central storage. +/// The output is also returned from execute_with_deps for central storage. pub(crate) struct TypedNode { pub(crate) id: NodeId, pub(crate) task: T, @@ -81,7 +78,8 @@ where let output = match self.task.extract_and_run(dependencies).await { Ok(output) => output, Err(_msg) => { - // Failed to extract input - likely channel closed or type mismatch + // Failed to extract input - likely type mismatch. + // This should technically be infallible, so it might be worth removing in the future. return Err(DagError::InvalidDependency { task_id: self.id.0 }); } }; diff --git a/src/node/tests.rs b/src/node/tests.rs index c990071..c74a0b2 100644 --- a/src/node/tests.rs +++ b/src/node/tests.rs @@ -67,17 +67,13 @@ async fn test_execute_with_channels_missing_input() { } #[tokio::test] -async fn test_execute_with_channels_success() { +async fn test_execute_with_deps_success() { // Test successful execution with channels let dependency_node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 21 })); let dependent_node = Box::new(TypedNode::new(NodeId(1), TaskWithDependency)); - // Create a channel from dependency to dependent (Arc-wrapped) - let (tx, rx) = futures::channel::oneshot::channel::>(); - // Execute dependency node with sender - let dep_senders = vec![Box::new(tx) as Box]; - let dep_receivers = vec![]; + let dep_dependencies = vec![]; // Execute in background let dep_handle = tokio::spawn(async move { @@ -87,15 +83,9 @@ async fn test_execute_with_channels_success() { }); // Execute dependent node with receiver - let receivers = vec![Box::new(rx) as Box]; - let senders = vec![]; + let dependencies = vec![dep_result as Arc]; - let result = dependent_node - .execute_with_channels(receivers, senders) - .await; - - // Wait for dependency to complete - let _ = dep_handle.await; + let result = dependent_node.execute_with_deps(dependencies).await; assert!(result.is_ok()); } @@ -106,8 +96,7 @@ async fn test_execute_with_channels_sink_node() { let node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 42 })); // No senders means this is a sink node - let receivers = vec![]; - let senders = vec![]; + let dependencies = vec![]; let result = node.execute_with_channels(receivers, senders).await; assert!(result.is_ok()); @@ -119,13 +108,10 @@ async fn test_execute_with_channels_sink_node() { } #[tokio::test] -async fn test_execute_with_channels_non_sink_node() { +async fn test_execute_with_deps_non_sink_node() { // Test that non-sink nodes send output via channels let node = TypedNode::new(NodeId(0), TestTask { value: 42 }); - // Create output channels for one dependent - let (senders, mut receivers) = node.create_output_channels(1); - // Execute with senders (non-sink node) let node = Box::new(node); let exec_receivers = vec![]; @@ -133,15 +119,7 @@ async fn test_execute_with_channels_non_sink_node() { assert!(result.is_ok()); // Verify output was sent through channel (Arc-wrapped) - let rx = receivers - .pop() - .unwrap() - .downcast::>>() - .unwrap(); - - let received_arc = rx.await; - assert!(received_arc.is_ok()); - assert_eq!(*received_arc.unwrap(), 42); + let arc_value = result.unwrap().downcast::().unwrap(); // Also verify output was returned (Arc-wrapped) let output = result.unwrap(); diff --git a/src/runner.rs b/src/runner.rs index c1ab82c..cadbf78 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -231,10 +231,13 @@ impl DagRunner { /// /// # Parameters /// - /// - `spawner`: A function that spawns futures on the async runtime. Examples: - /// - Tokio: `|fut| { tokio::spawn(fut); }` - /// - Smol: `|fut| { smol::spawn(fut).detach(); }` - /// - Async-std: `|fut| { async_std::task::spawn(fut); }` + /// - `spawner`: A function that spawns futures on the async runtime + /// and returns a task handle. Examples: + /// - Tokio: `|fut| { tokio::spawn(fut).map(Result::unwrap) }` + /// - Smol: `|fut| { smol::spawn(fut) }` + /// - Async-std: `|fut| { async_std::task::spawn(fut) }` + /// - Single-threaded: `|fut| fut` + /// - For computationally light tasks, concurrency without parallelism can be significantly faster. /// /// # Errors /// @@ -276,13 +279,6 @@ impl DagRunner { /// dag.run().await.unwrap(); // Executes all tasks /// # }; /// ``` - /// - /// # Implementation Note - /// - /// Tasks communicate via oneshot channels created fresh during run(). - /// This eliminates Mutex contention on outputs and enables true streaming execution. - /// Type erasure occurs only at the ExecutableNode trait boundary - channels are created - /// with full type information and type-erased just before passing to execute_with_channels. #[inline] #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] pub async fn run(&self) -> DagResult<()> { @@ -334,7 +330,6 @@ impl DagRunner { // pipelines), we execute it inline rather than spawning it. This provides // 10-100x performance improvements for sequential workloads by eliminating: // - Task spawning overhead - // - Channel creation/destruction for layer coordination // - Context switching to/from the runtime // // CRITICAL: Panic handling is required to maintain behavioral consistency. @@ -407,7 +402,6 @@ impl DagRunner { }) }); - // Send the output (ignore send errors - receiver may be dropped) match result { Ok(output) => { outputs.insert(node_id, output); diff --git a/src/task.rs b/src/task.rs index 54ee361..32883b0 100644 --- a/src/task.rs +++ b/src/task.rs @@ -87,7 +87,7 @@ use std::{future::Future, sync::Arc}; /// # Output Wrapping /// /// **Task outputs are automatically wrapped in `Arc` internally** for efficient fan-out. -/// You define `Output = T`, but the framework wraps it in `Arc` before sending to channels. +/// You define `Output = T`, but the framework wraps it in `Arc` before sending to dependents. /// This enables O(1) sharing for 1:N dependencies - Arc is cloned (cheap), not your data. /// /// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent @@ -98,7 +98,7 @@ pub trait Task: Send { fn run(self, input: Self::Input) -> impl Future + Send; - /// Internal method: Extract input from type-erased channels and execute the task. + /// Internal method: Extract input from list of type-erased dependencies and execute the task. /// /// This method is implemented automatically by the #[task] macro with inline /// extraction logic based on the task's parameter signature. This allows ANY diff --git a/tests/execution/basic.rs b/tests/execution/basic.rs index b9e343d..565a803 100644 --- a/tests/execution/basic.rs +++ b/tests/execution/basic.rs @@ -6,9 +6,7 @@ use dagx::*; #[tokio::test] async fn test_empty_dag() { let dag = DagRunner::new(); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); // Should complete without error } @@ -17,9 +15,7 @@ async fn test_empty_dag() { async fn test_single_task() { let dag = DagRunner::new(); let task = dag.add_task(task_fn(|_: ()| async { 42 })); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); assert_eq!(dag.get(task).unwrap(), 42); @@ -43,9 +39,7 @@ async fn test_deep_chain() { .add_task(task_fn(|x: i32| async move { x + 1 })) .depends_on(d); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -62,9 +56,7 @@ async fn test_wide_parallel() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -88,9 +80,7 @@ async fn test_diamond_dependency() { .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .depends_on((&b, &c)); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -118,9 +108,7 @@ async fn test_different_output_types() { } })); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -153,9 +141,7 @@ async fn test_multiple_sinks() { .add_task(task_fn(|x: i32| async move { x * 3 })) .depends_on(branch2); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); // Should wait for both sinks @@ -177,9 +163,7 @@ async fn test_single_task_inline_path() { .add_task(task_fn(|x: i32| async move { x + 5 })) .depends_on(t2); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -204,9 +188,7 @@ async fn test_multi_task_spawn_path() { .add_task(task_fn(|x: i32| async move { x + 3 })) .depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -230,9 +212,7 @@ async fn test_producer_consumer_channels() { )) .depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -241,7 +221,7 @@ async fn test_producer_consumer_channels() { #[tokio::test] async fn test_multi_consumer_fanout() { - // Test fanout with multiple consumers from one producer (channel creation) + // Test fanout with multiple consumers from one producer let dag = DagRunner::new(); let producer = dag.add_task(task_fn(|_: ()| async { 42 })); @@ -257,9 +237,7 @@ async fn test_multi_consumer_fanout() { .add_task(task_fn(|x: i32| async move { x - 5 })) .depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -287,9 +265,7 @@ async fn test_compute_layers_with_dependents_tracking() { .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .depends_on((&left, &right)); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); @@ -337,9 +313,7 @@ async fn test_layer_execution_order() { })) .depends_on(t2); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run() .await .unwrap(); diff --git a/tests/parallelism/spawning.rs b/tests/parallelism/spawning.rs index 01a0bca..634ad45 100644 --- a/tests/parallelism/spawning.rs +++ b/tests/parallelism/spawning.rs @@ -16,16 +16,22 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { // Create 10 independent tasks let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| { + dag.add_task(task_fn({ + let value = spawn_count.clone(); + move |_: ()| { + let value = value.clone(); + async move { + value.fetch_add(1, Ordering::SeqCst); + i + } + } + })) + }) .collect(); // Custom spawner that counts invocations - let counter = spawn_count.clone(); - dag.run(move |fut| { - counter.fetch_add(1, Ordering::SeqCst); - tokio::spawn(fut); - }) - .await?; + dag.run().await?; // Verify all tasks were spawned for (i, task) in tasks.iter().enumerate() { From d7806043e1c251d5b78d8f7894ee7dd6866cc25f Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Tue, 20 Jan 2026 21:37:26 +0100 Subject: [PATCH 3/7] Use single-threaded hashmap instead of dashmap Address performance regressions --- Cargo.lock | 21 --------------------- Cargo.toml | 1 - src/runner.rs | 16 +++++++++------- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e680cbf..ce288a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,7 +442,6 @@ dependencies = [ "criterion", "dagrs", "dagx-macros", - "dashmap", "futures", "futures-executor", "parking_lot", @@ -462,20 +461,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "either" version = "1.15.0" @@ -656,12 +641,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hermit-abi" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index ed84368..1ec4078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ futures = "0.3" parking_lot = "0.12" dagx-macros = { version = "0.3.1", path = "./dagx-macros" } tracing = { version = "0.1", optional = true } -dashmap = "6.1.0" [dev-dependencies] tokio = { version = "1", features = [ diff --git a/src/runner.rs b/src/runner.rs index cadbf78..71f3c25 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -13,7 +13,6 @@ use std::sync::{ Arc, }; -use dashmap::DashMap; use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt, TryFutureExt}; use parking_lot::Mutex; @@ -314,7 +313,7 @@ impl DagRunner { let edges = self.edges.lock().clone(); - let outputs: DashMap> = DashMap::new(); + let mut outputs: HashMap> = HashMap::new(); let mut first_error = None; for layer in layers { @@ -414,7 +413,6 @@ impl DagRunner { } else { // Slow path: Multiple tasks require spawning and coordination // Spawn each task in this layer - let outputs = &outputs; let mut futures: FuturesUnordered<_> = FuturesUnordered::new(); layer .into_iter() @@ -437,8 +435,7 @@ impl DagRunner { // Spawn the task using the provided spawner let inner_future = async move { let result = node.execute_with_deps(dependencies).await?; - outputs.insert(node_id, result); - Ok(()) + Ok((node_id, result)) }; Some( @@ -477,8 +474,13 @@ impl DagRunner { .for_each(|fut| futures.push(fut)); while let Some(out) = futures.next().await { - if let Err(e) = out { - first_error.get_or_insert(e); + match out { + Ok(output) => { + outputs.insert(output.0, output.1); + } + Err(e) => { + first_error.get_or_insert(e); + } } } } From bacac903b3c906f621db596b049d0460f5fc7b85 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Wed, 21 Jan 2026 22:37:25 +0100 Subject: [PATCH 4/7] Reimplement spawner function --- src/runner.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/runner.rs b/src/runner.rs index 71f3c25..243d9d7 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -7,12 +7,14 @@ use std::any::Any; use std::collections::{HashMap, HashSet, VecDeque}; +use std::future::Future; use std::panic::AssertUnwindSafe; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; +use futures::future::BoxFuture; use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt, TryFutureExt}; use parking_lot::Mutex; @@ -280,7 +282,11 @@ impl DagRunner { /// ``` #[inline] #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] - pub async fn run(&self) -> DagResult<()> { + pub async fn run(&self, spawner: S) -> DagResult<()> + where + S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc)>>) -> F, + F: Future)>>, + { #[cfg(feature = "tracing")] info!("starting DAG execution"); // Acquire run lock to prevent concurrent executions @@ -413,10 +419,9 @@ impl DagRunner { } else { // Slow path: Multiple tasks require spawning and coordination // Spawn each task in this layer - let mut futures: FuturesUnordered<_> = FuturesUnordered::new(); - layer + let mut futures: FuturesUnordered<_> = layer .into_iter() - .flat_map(|node_id| { + .filter_map(|node_id| { #[cfg(feature = "tracing")] trace!(task_id = node_id.0, "spawning task"); @@ -439,7 +444,7 @@ impl DagRunner { }; Some( - AssertUnwindSafe(Box::pin(inner_future)) + AssertUnwindSafe(spawner(Box::pin(inner_future))) .catch_unwind() .unwrap_or_else(move |panic_payload| { // Convert panic to error @@ -471,7 +476,7 @@ impl DagRunner { None } }) - .for_each(|fut| futures.push(fut)); + .collect(); while let Some(out) = futures.next().await { match out { From 85ccd7e507516d39e265eff177c72b9a85ad98d5 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Mon, 19 Jan 2026 00:22:46 +0100 Subject: [PATCH 5/7] Update tests, examples, benches --- README.md | 4 +- benches/basic/execution.rs | 25 ++-- benches/basic/scaling.rs | 9 +- benches/comparisons/deep_chain.rs | 9 +- benches/comparisons/diamond_pattern.rs | 9 +- benches/comparisons/etl_pipeline.rs | 9 +- benches/comparisons/large_scale.rs | 9 +- benches/comparisons/linear_pipeline.rs | 9 +- benches/comparisons/wide_fanout.rs | 9 +- benches/patterns/breakdown.rs | 17 +-- benches/patterns/diamond.rs | 9 +- benches/patterns/fanout.rs | 25 ++-- benches/realistic/config_broadcast.rs | 9 +- benches/realistic/etl.rs | 5 +- benches/realistic/memory_efficiency.rs | 9 +- benches/realistic/mixed.rs | 9 +- docs/COMPARISONS.md | 4 +- docs/CYCLE_PREVENTION.md | 2 +- examples/01_basic.rs | 9 +- examples/02_fan_out.rs | 9 +- examples/03_fan_in.rs | 9 +- examples/04_parallel_computation.rs | 6 +- examples/circuit_breaker.rs | 57 +++----- examples/complex_dag.rs | 6 +- examples/conditional_workflow.rs | 6 +- examples/custom_types.rs | 9 +- examples/data_pipeline.rs | 9 +- examples/debug_tracing.rs | 17 +-- examples/error_handling.rs | 33 ++--- examples/large_dag.rs | 41 +++--- examples/parallelism_proof.rs | 9 +- examples/retry_strategies.rs | 41 +++--- examples/timeout.rs | 39 +++--- examples/tracing_example.rs | 25 ++-- src/builder.rs | 6 +- src/error.rs | 2 +- src/error/tests.rs | 2 +- src/extract/coverage_tests.rs | 156 +++++---------------- src/extract/tests.rs | 180 +++++++------------------ src/lib.rs | 27 ++-- src/node/tests.rs | 76 ++--------- src/runner.rs | 12 +- src/runner/coverage_tests.rs | 49 +++---- src/runner/tests.rs | 42 ++---- src/task/tests.rs | 3 +- src/types.rs | 7 +- src/types/coverage_tests.rs | 41 +++--- tests/arc_tests.rs | 145 ++++++++------------ tests/boundaries/deep_chains.rs | 46 ++----- tests/boundaries/large_scale.rs | 26 +--- tests/boundaries/type_limits.rs | 51 ++----- tests/boundaries/zero_tasks.rs | 56 ++------ tests/custom_type_test.rs | 33 ++--- tests/cycle_prevention.rs | 9 +- tests/dependencies/resolution.rs | 21 +-- tests/dependencies/single.rs | 26 +--- tests/dependencies/tuples.rs | 36 ++--- tests/errors/error_types.rs | 53 +++----- tests/errors/propagation.rs | 53 ++------ tests/errors/recovery.rs | 44 ++---- tests/execution/basic.rs | 79 +++++------ tests/execution/examples.rs | 33 ++--- tests/execution/integration.rs | 61 +++++---- tests/execution/results.rs | 38 ++---- tests/parallelism/concurrent.rs | 79 +++++------ tests/parallelism/execution.rs | 21 +-- tests/parallelism/proof.rs | 6 +- tests/parallelism/spawning.rs | 8 +- tests/parallelism/threads.rs | 31 +---- tests/parallelism/timing.rs | 11 +- tests/patterns/butterfly.rs | 21 +-- tests/patterns/complex.rs | 26 +--- tests/patterns/grid.rs | 21 +-- tests/patterns/tree.rs | 31 +---- tests/performance/memory.rs | 9 +- tests/performance/stress.rs | 17 +-- tests/runtimes/async_std.rs | 10 +- tests/runtimes/compatibility.rs | 6 +- tests/runtimes/quirky.rs | 152 +++++++-------------- tests/runtimes/smol.rs | 16 ++- tests/task_patterns/combined.rs | 25 ++-- tests/task_patterns/readonly.rs | 25 ++-- tests/task_patterns/stateless.rs | 65 ++++----- tests/timing/concurrent_access.rs | 36 ++--- tests/timing/interleaving.rs | 21 +-- tests/tracing/with_tracing.rs | 32 ++--- tests/tracing/without_tracing.rs | 17 +-- tests/type_safety/complex.rs | 97 +++++-------- tests/type_safety/handles.rs | 25 ++-- 89 files changed, 942 insertions(+), 1785 deletions(-) diff --git a/README.md b/README.md index 4fb48b9..704fa6c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ See [how it works](docs/CYCLE_PREVENTION.md). ```rust let sum = dag.add_task(Add).depends_on((&x, &y)); -dag.run().await?; +dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; ``` That's it. No trait boilerplate, no manual channels, no node IDs. @@ -90,7 +90,7 @@ async fn main() { let sum = dag.add_task(Add).depends_on((&x, &y)); // Execute with true parallelism - dag.run().await.unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); // Retrieve results assert_eq!(dag.get(sum).unwrap(), 5); diff --git a/benches/basic/execution.rs b/benches/basic/execution.rs index e6d6326..e7dafcb 100644 --- a/benches/basic/execution.rs +++ b/benches/basic/execution.rs @@ -2,6 +2,7 @@ use criterion::Criterion; use dagx::{task_fn, DagRunner, Task}; +use futures::FutureExt; pub fn bench_dag_execution(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -13,11 +14,9 @@ pub fn bench_dag_execution(c: &mut Criterion) { for i in 0..10 { dag.add_task(task_fn(move |_: ()| async move { i })); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); @@ -29,11 +28,9 @@ pub fn bench_dag_execution(c: &mut Criterion) { for i in 0..100 { dag.add_task(task_fn(move |_: ()| async move { i })); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); @@ -111,11 +108,9 @@ pub fn bench_dag_execution(c: &mut Criterion) { .add_task(task_fn(|x: i32| async move { x + 1 })) .depends_on(t18); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/basic/scaling.rs b/benches/basic/scaling.rs index b7f8719..7fd1501 100644 --- a/benches/basic/scaling.rs +++ b/benches/basic/scaling.rs @@ -2,6 +2,7 @@ use criterion::{BenchmarkId, Criterion}; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_dag_scaling(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -18,11 +19,9 @@ pub fn bench_dag_scaling(c: &mut Criterion) { for i in 0..size { dag.add_task(task_fn(move |_: ()| async move { i })); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/comparisons/deep_chain.rs b/benches/comparisons/deep_chain.rs index 428a5e8..a5993ab 100644 --- a/benches/comparisons/deep_chain.rs +++ b/benches/comparisons/deep_chain.rs @@ -3,6 +3,7 @@ //! Long sequential chain: 100 tasks in sequence use criterion::Criterion; +use futures::FutureExt; pub fn bench_deep_chain(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -27,11 +28,9 @@ pub fn bench_deep_chain(c: &mut Criterion) { .depends_on(prev); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); dag.get(prev).unwrap() }) }); diff --git a/benches/comparisons/diamond_pattern.rs b/benches/comparisons/diamond_pattern.rs index 509065a..c7c2e52 100644 --- a/benches/comparisons/diamond_pattern.rs +++ b/benches/comparisons/diamond_pattern.rs @@ -3,6 +3,7 @@ //! Classic diamond: A → B,C → D use criterion::Criterion; +use futures::FutureExt; pub fn bench_diamond_pattern(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -27,11 +28,9 @@ pub fn bench_diamond_pattern(c: &mut Criterion) { .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .depends_on((&b, &c)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); dag.get(d).unwrap() }) }); diff --git a/benches/comparisons/etl_pipeline.rs b/benches/comparisons/etl_pipeline.rs index fa068f3..73bcf5e 100644 --- a/benches/comparisons/etl_pipeline.rs +++ b/benches/comparisons/etl_pipeline.rs @@ -3,6 +3,7 @@ //! Extract → Transform → Validate → Load pattern use criterion::Criterion; +use futures::FutureExt; pub fn bench_etl_pipeline(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -66,11 +67,9 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { })) .depends_on(validate); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); dag.get(load).unwrap() }) }); diff --git a/benches/comparisons/large_scale.rs b/benches/comparisons/large_scale.rs index ef531a3..dd76cc0 100644 --- a/benches/comparisons/large_scale.rs +++ b/benches/comparisons/large_scale.rs @@ -3,6 +3,7 @@ //! 10,000 independent tasks to test scaling use criterion::Criterion; +use futures::FutureExt; pub fn bench_large_scale(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -21,11 +22,9 @@ pub fn bench_large_scale(c: &mut Criterion) { dag.add_task(task_fn(move |_: ()| async move { i * 2 })); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/comparisons/linear_pipeline.rs b/benches/comparisons/linear_pipeline.rs index bf8ffab..9df20c5 100644 --- a/benches/comparisons/linear_pipeline.rs +++ b/benches/comparisons/linear_pipeline.rs @@ -3,6 +3,7 @@ //! Simple chain of tasks: A → B → C → D → E use criterion::Criterion; +use futures::FutureExt; pub fn bench_linear_pipeline(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -30,11 +31,9 @@ pub fn bench_linear_pipeline(c: &mut Criterion) { .add_task(task_fn(|x: i32| async move { x * 3 })) .depends_on(d); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); dag.get(e).unwrap() }) }); diff --git a/benches/comparisons/wide_fanout.rs b/benches/comparisons/wide_fanout.rs index 67b4ee9..c80f063 100644 --- a/benches/comparisons/wide_fanout.rs +++ b/benches/comparisons/wide_fanout.rs @@ -3,6 +3,7 @@ //! One source task feeding 100 dependent tasks use criterion::Criterion; +use futures::FutureExt; pub fn bench_wide_fanout(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -23,11 +24,9 @@ pub fn bench_wide_fanout(c: &mut Criterion) { .depends_on(&source); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/patterns/breakdown.rs b/benches/patterns/breakdown.rs index 7c59a6b..d887449 100644 --- a/benches/patterns/breakdown.rs +++ b/benches/patterns/breakdown.rs @@ -2,6 +2,7 @@ use criterion::Criterion; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_10k_breakdown(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -26,11 +27,9 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { for i in 0..10_000 { dag.add_task(task_fn(move |_: ()| async move { i })); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); @@ -45,11 +44,9 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { b.iter(|| { rt.block_on(async { - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/patterns/diamond.rs b/benches/patterns/diamond.rs index 4e32441..f129cc1 100644 --- a/benches/patterns/diamond.rs +++ b/benches/patterns/diamond.rs @@ -2,6 +2,7 @@ use criterion::Criterion; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_diamond(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -25,11 +26,9 @@ pub fn bench_diamond(c: &mut Criterion) { .depends_on((&left, &right)); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/patterns/fanout.rs b/benches/patterns/fanout.rs index 6cf4848..7a06f4e 100644 --- a/benches/patterns/fanout.rs +++ b/benches/patterns/fanout.rs @@ -2,6 +2,7 @@ use criterion::Criterion; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_fanout(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -18,11 +19,9 @@ pub fn bench_fanout(c: &mut Criterion) { .depends_on(&source); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); @@ -45,11 +44,9 @@ pub fn bench_fanout(c: &mut Criterion) { .depends_on(&source); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); @@ -72,11 +69,9 @@ pub fn bench_fanout(c: &mut Criterion) { .depends_on(&source); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/benches/realistic/config_broadcast.rs b/benches/realistic/config_broadcast.rs index e3cc5e6..2bce80f 100644 --- a/benches/realistic/config_broadcast.rs +++ b/benches/realistic/config_broadcast.rs @@ -2,6 +2,7 @@ use criterion::{BenchmarkId, Criterion}; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; use std::collections::HashMap; use std::hint::black_box; @@ -51,11 +52,9 @@ pub fn bench_config_broadcast(c: &mut Criterion) { .depends_on(&config); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }, diff --git a/benches/realistic/etl.rs b/benches/realistic/etl.rs index 3de4a3f..d0a39b8 100644 --- a/benches/realistic/etl.rs +++ b/benches/realistic/etl.rs @@ -2,6 +2,7 @@ use criterion::{BenchmarkId, Criterion}; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_etl_pipeline(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -90,9 +91,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { )) .depends_on((&validate, &scores, &names)); - dag.run(|fut| { - tokio::spawn(fut); - }) + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await .unwrap(); }) diff --git a/benches/realistic/memory_efficiency.rs b/benches/realistic/memory_efficiency.rs index a5c6345..1d6a548 100644 --- a/benches/realistic/memory_efficiency.rs +++ b/benches/realistic/memory_efficiency.rs @@ -2,6 +2,7 @@ use criterion::{BenchmarkId, Criterion}; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; use std::hint::black_box; pub fn bench_memory_efficiency(c: &mut Criterion) { @@ -38,11 +39,9 @@ pub fn bench_memory_efficiency(c: &mut Criterion) { .depends_on(&source); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }, diff --git a/benches/realistic/mixed.rs b/benches/realistic/mixed.rs index 1561ffa..c1110c1 100644 --- a/benches/realistic/mixed.rs +++ b/benches/realistic/mixed.rs @@ -2,6 +2,7 @@ use criterion::Criterion; use dagx::{task_fn, DagRunner}; +use futures::FutureExt; pub fn bench_mixed_patterns(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -50,11 +51,9 @@ pub fn bench_mixed_patterns(c: &mut Criterion) { .depends_on(summary); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }); diff --git a/docs/COMPARISONS.md b/docs/COMPARISONS.md index 2c53ccc..0386632 100644 --- a/docs/COMPARISONS.md +++ b/docs/COMPARISONS.md @@ -19,7 +19,7 @@ let dag = DagRunner::new(); let x = dag.add_task(Value(2)); let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); -dag.run().await?; +dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; ``` **dagrs** (Complex: traits + channels + IDs + manual wiring): @@ -227,7 +227,7 @@ impl Task for MyTask { let mut dag = Dag::default(); dag.add_vertex("one", || async move { /* task */ }); dag.add_edge("one", "two"); -dag.run().await?; +dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; ``` ### tasksitter (Dynamic Workflows) diff --git a/docs/CYCLE_PREVENTION.md b/docs/CYCLE_PREVENTION.md index 839609c..1e26b9c 100644 --- a/docs/CYCLE_PREVENTION.md +++ b/docs/CYCLE_PREVENTION.md @@ -13,7 +13,7 @@ let a = dag.add_task(TaskA); let b = dag.add_task(TaskB); a.depends_on(b); b.depends_on(a); // Compiles fine... -dag.run() +dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) ``` **dagx prevents this at compile time:** diff --git a/examples/01_basic.rs b/examples/01_basic.rs index f58d42f..6e9ec5d 100644 --- a/examples/01_basic.rs +++ b/examples/01_basic.rs @@ -31,6 +31,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; // Step 1: Define a source task (no dependencies) // @@ -105,11 +106,9 @@ async fn main() { // 1. Tasks with no dependencies run first (x and y in parallel) // 2. Once x and y complete, sum runs (it depends on both) println!("Running DAG...\n"); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Step 7: Retrieve results // diff --git a/examples/02_fan_out.rs b/examples/02_fan_out.rs index 838d410..d7b9569 100644 --- a/examples/02_fan_out.rs +++ b/examples/02_fan_out.rs @@ -56,6 +56,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; // Source task with a value field struct Source { @@ -140,11 +141,9 @@ async fn main() { // Run the DAG println!("Running fan-out DAG...\n"); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Print all results println!("\nResults:"); diff --git a/examples/03_fan_in.rs b/examples/03_fan_in.rs index 8d9d9d0..5390fc5 100644 --- a/examples/03_fan_in.rs +++ b/examples/03_fan_in.rs @@ -62,6 +62,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; struct FetchName; @@ -133,11 +134,9 @@ async fn main() { // Run the DAG println!("Running fan-in DAG...\n"); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Print result println!("\n{}", dag.get(profile).unwrap()); diff --git a/examples/04_parallel_computation.rs b/examples/04_parallel_computation.rs index db03e6b..7329591 100644 --- a/examples/04_parallel_computation.rs +++ b/examples/04_parallel_computation.rs @@ -53,6 +53,7 @@ //! ``` use dagx::{task, DagResult, DagRunner, Task}; +use futures::FutureExt; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -126,10 +127,7 @@ async fn main() -> DagResult<()> { .add_task(Aggregate) .depends_on((&sum1, &sum2, &sum3, &sum4)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(total)?; let elapsed = start.elapsed(); diff --git a/examples/circuit_breaker.rs b/examples/circuit_breaker.rs index 8fb8938..23b5d1a 100644 --- a/examples/circuit_breaker.rs +++ b/examples/circuit_breaker.rs @@ -151,6 +151,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use std::sync::Arc; use std::time::Instant; use tokio::sync::Mutex; @@ -389,11 +390,9 @@ async fn main() { false, // success )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 1: {:?}\n", dag.get(call1).unwrap()); @@ -405,11 +404,9 @@ async fn main() { true, // fail )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 2: {:?}\n", dag.get(call2).unwrap()); @@ -421,11 +418,9 @@ async fn main() { true, // fail )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 3: {:?}\n", dag.get(call3).unwrap()); @@ -437,11 +432,9 @@ async fn main() { true, // fail )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 4: {:?}\n", dag.get(call4).unwrap()); @@ -457,11 +450,9 @@ async fn main() { .add_task(FallbackService::new("ServiceCall-5")) .depends_on(&call5); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 5: {:?}\n", dag.get(fallback5).unwrap()); @@ -478,11 +469,9 @@ async fn main() { false, // success )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 6: {:?}\n", dag.get(call6).unwrap()); @@ -494,11 +483,9 @@ async fn main() { false, // success )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result 7: {:?}\n", dag.get(call7).unwrap()); } diff --git a/examples/complex_dag.rs b/examples/complex_dag.rs index 339a71a..5bc4d18 100644 --- a/examples/complex_dag.rs +++ b/examples/complex_dag.rs @@ -67,6 +67,7 @@ //! ``` use dagx::{task, DagResult, DagRunner, Task}; +use futures::FutureExt; // Extract: Load data from different sources struct LoadData { @@ -201,10 +202,7 @@ async fn main() -> DagResult<()> { // Execute the entire ETL pipeline println!("Executing pipeline...\n"); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Display final report println!("\n{}", dag.get(report)?); diff --git a/examples/conditional_workflow.rs b/examples/conditional_workflow.rs index 9d1d1e3..5d719ac 100644 --- a/examples/conditional_workflow.rs +++ b/examples/conditional_workflow.rs @@ -56,6 +56,7 @@ //! ``` use dagx::{task, DagResult, DagRunner, Task}; +use futures::FutureExt; // Task that validates and processes input struct ValidateAndDouble { @@ -137,10 +138,7 @@ async fn main() -> DagResult<()> { .depends_on((&cat1, &cat2, &cat3)); // Execute - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Display results println!("\nFinal summary: {}", dag.get(summary)?); diff --git a/examples/custom_types.rs b/examples/custom_types.rs index f98914b..35f33ed 100644 --- a/examples/custom_types.rs +++ b/examples/custom_types.rs @@ -37,6 +37,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; // Custom type - just derive Clone! #[derive(Clone, Debug)] @@ -107,11 +108,9 @@ async fn main() { let validation = dag.add_task(ValidateUser).depends_on(&user_task); // Execute the DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Print results println!("Greeting: {}", dag.get(greeting).unwrap()); diff --git a/examples/data_pipeline.rs b/examples/data_pipeline.rs index 0c27fe8..d96798e 100644 --- a/examples/data_pipeline.rs +++ b/examples/data_pipeline.rs @@ -76,6 +76,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; // Data source tasks struct LoadValue1; @@ -220,11 +221,9 @@ async fn main() { // Execute the pipeline println!("Executing pipeline...\n"); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Print final report println!("{}", dag.get(report).unwrap()); diff --git a/examples/debug_tracing.rs b/examples/debug_tracing.rs index 119d8c9..126a620 100644 --- a/examples/debug_tracing.rs +++ b/examples/debug_tracing.rs @@ -115,6 +115,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use std::time::Instant; use tokio::time::{sleep, Duration}; @@ -400,11 +401,9 @@ async fn main() { .depends_on((&inventory, &payment, &shipping)); // Run the DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Print summary dag.get(summary).unwrap().print(); @@ -442,11 +441,9 @@ async fn main() { true, // will fail )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Check results match dag.get(payment).unwrap() { diff --git a/examples/error_handling.rs b/examples/error_handling.rs index 158cbab..6b0e272 100644 --- a/examples/error_handling.rs +++ b/examples/error_handling.rs @@ -115,6 +115,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; // Source task that provides a string input struct StringSource(&'static str); @@ -222,11 +223,9 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); match dag.get(logged).unwrap() { Ok(result) => println!("Final result: {}\n", result), @@ -250,11 +249,9 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); match dag.get(logged).unwrap() { Ok(result) => println!("Final result: {}\n", result), @@ -278,11 +275,9 @@ async fn main() { let processed = dag.add_task(ProcessData).depends_on(validated); let logged = dag.add_task(LogError).depends_on(processed); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); match dag.get(logged).unwrap() { Ok(result) => println!("Final result: {}\n", result), @@ -303,11 +298,9 @@ async fn main() { .add_task(WithFallback { fallback: 0 }) .depends_on(parsed); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result = dag.get(with_fallback).unwrap(); println!("Recovered with fallback: {}\n", result); diff --git a/examples/large_dag.rs b/examples/large_dag.rs index b6ba2f7..d0baec5 100644 --- a/examples/large_dag.rs +++ b/examples/large_dag.rs @@ -110,6 +110,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use std::time::Instant; // Simple computation task @@ -181,11 +182,9 @@ async fn main() { println!(" Build time: {:?}", build_time); let exec_start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = exec_start.elapsed(); println!(" Execution time: {:?}", exec_time); @@ -223,11 +222,9 @@ async fn main() { println!(" Build time: {:?}", build_time); let exec_start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = exec_start.elapsed(); println!(" Execution time: {:?}", exec_time); @@ -276,11 +273,9 @@ async fn main() { println!(" Tree depth: {}", level); let exec_start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = exec_start.elapsed(); println!(" Execution time: {:?}", exec_time); @@ -336,11 +331,9 @@ async fn main() { println!(" Build time: {:?}", build_time); let exec_start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = exec_start.elapsed(); println!(" Execution time: {:?}", exec_time); @@ -362,11 +355,9 @@ async fn main() { .map(|i| dag.add_task(FormatNumber { id: i })) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Verify results are accessible let i32_sum: i32 = small_tasks diff --git a/examples/parallelism_proof.rs b/examples/parallelism_proof.rs index 1be2ba8..8c62fd2 100644 --- a/examples/parallelism_proof.rs +++ b/examples/parallelism_proof.rs @@ -48,6 +48,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -86,11 +87,9 @@ async fn main() { // Measure execution time let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let elapsed = start.elapsed(); // Verify all tasks completed correctly diff --git a/examples/retry_strategies.rs b/examples/retry_strategies.rs index 278ad42..05d9510 100644 --- a/examples/retry_strategies.rs +++ b/examples/retry_strategies.rs @@ -153,6 +153,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use tokio::time::{sleep, Duration}; // Retry strategy configuration @@ -355,11 +356,9 @@ async fn main() { ], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {:?}\n", dag.get(api_call).unwrap()); } @@ -379,11 +378,9 @@ async fn main() { ], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {:?}\n", dag.get(api_call).unwrap()); } @@ -407,11 +404,9 @@ async fn main() { ], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {:?}\n", dag.get(api_call).unwrap()); } @@ -428,11 +423,9 @@ async fn main() { vec![ApiError::AuthenticationFailed], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {:?}\n", dag.get(api_call).unwrap()); } @@ -457,11 +450,9 @@ async fn main() { ], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {:?}\n", dag.get(api_call).unwrap()); } diff --git a/examples/timeout.rs b/examples/timeout.rs index 5f5c246..7f68205 100644 --- a/examples/timeout.rs +++ b/examples/timeout.rs @@ -24,7 +24,7 @@ //! //! **2. DAG-Level Timeout** - Entire workflow with timeout //! ```rust -//! match timeout(dag_timeout, dag.run(spawner)).await { +//! match timeout(dag_timeout, dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await { //! Ok(result) => { /* DAG completed */ }, //! Err(_) => { /* DAG timed out */ }, //! } @@ -99,6 +99,7 @@ //! ``` use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use tokio::time::{sleep, timeout, Duration}; // Fast task that completes quickly @@ -177,11 +178,9 @@ async fn main() { timeout_ms: 500, }); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Fast task: {:?}", dag.get(fast).unwrap()); println!("Timed task: {:?}\n", dag.get(timed).unwrap()); @@ -199,11 +198,9 @@ async fn main() { let processed = dag.add_task(ProcessResult).depends_on(&timed); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Result: {}\n", dag.get(processed).unwrap()); } @@ -228,11 +225,9 @@ async fn main() { timeout_ms: 200, }); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Fast task 1: {:?}", dag.get(fast1).unwrap()); println!("Fast task 2: {:?}", dag.get(fast2).unwrap()); @@ -254,11 +249,9 @@ async fn main() { let processed1 = dag.add_task(ProcessResult).depends_on(&source); let processed2 = dag.add_task(ProcessResult).depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("Processed 1: {}", dag.get(processed1).unwrap()); println!("Processed 2: {}\n", dag.get(processed2).unwrap()); @@ -277,9 +270,7 @@ async fn main() { let dag_timeout = Duration::from_millis(500); match timeout( dag_timeout, - dag.run(|fut| { - tokio::spawn(fut); - }), + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)), ) .await { diff --git a/examples/tracing_example.rs b/examples/tracing_example.rs index 87924eb..30bc713 100644 --- a/examples/tracing_example.rs +++ b/examples/tracing_example.rs @@ -34,6 +34,7 @@ //! use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use tracing_subscriber::{fmt, EnvFilter}; // Simple source task @@ -95,11 +96,9 @@ async fn main() { // Layer 2: Combine results let result = dag.add_task(Add).depends_on((&left, &right)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("\nResult: {}", dag.get(result).unwrap()); println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n"); @@ -118,11 +117,9 @@ async fn main() { let c = dag.add_task(Multiply).depends_on((&b, &b)); let d = dag.add_task(Add).depends_on((&c, &c)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("\nResult: {}", dag.get(d).unwrap()); println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n"); @@ -149,11 +146,9 @@ async fn main() { let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23)); let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); println!("\nResult: {}", dag.get(final_result).unwrap()); println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n"); diff --git a/src/builder.rs b/src/builder.rs index b4e6b40..8818db3 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -31,6 +31,7 @@ impl IsUnitType for () {} /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; +/// # use futures::FutureExt; /// // Using tuple struct for simple constants /// struct Constant(i32); /// @@ -65,7 +66,7 @@ impl IsUnitType for () {} /// let b = b.depends_on(&a); /// // Now b is a TaskHandle /// -/// dag.run().await.unwrap(); +/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// assert_eq!(dag.get(&b).unwrap(), 20); /// # }; /// ``` @@ -89,6 +90,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; + /// # use futures::FutureExt; /// // Tuple struct /// struct Value(i32); /// @@ -125,7 +127,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// // Multiple dependencies: tuple form /// let sum = dag.add_task(Add).depends_on((&x, &y)); /// - /// dag.run().await.unwrap(); + /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// # }; /// ``` pub fn depends_on(self, deps: D) -> TaskHandle diff --git a/src/error.rs b/src/error.rs index 7cbb01a..8529bec 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,7 +63,7 @@ impl std::fmt::Display for DagError { f, "Result not found for task #{}.\n\ \n\ - Call dag.run().", + Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).", task_id ) } diff --git a/src/error/tests.rs b/src/error/tests.rs index 64a2b8d..cb2e65a 100644 --- a/src/error/tests.rs +++ b/src/error/tests.rs @@ -51,7 +51,7 @@ fn test_dag_error_display_result_not_found() { assert!(display.contains("Result not found")); assert!(display.contains("task #7")); - assert!(display.contains("Call dag.run()")); + assert!(display.contains("Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))")); } #[test] diff --git a/src/extract/coverage_tests.rs b/src/extract/coverage_tests.rs index 60ea9f8..40768ba 100644 --- a/src/extract/coverage_tests.rs +++ b/src/extract/coverage_tests.rs @@ -5,7 +5,6 @@ //! We directly call extract_from_deps with mismatched types. use crate::extract::ExtractInput; -use futures::channel::oneshot; use std::any::Any; use std::collections::HashMap; use std::sync::Arc; @@ -13,17 +12,10 @@ use std::sync::Arc; #[tokio::test] async fn test_result_type_mismatch_coverage() { // Tests line 95 in extract.rs - Result type mismatch error message - // Create a channel that sends the WRONG type (String instead of Result) - let (tx, rx) = oneshot::channel::>(); - - // Send a String value - tokio::spawn(async move { - let _ = tx.send(Arc::new("hello".to_string())); - }); - + // Create a dependency that provides the WRONG type (String instead of Result) // Try to extract as Result - this will fail with type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = Result::::extract_from_channels(deps).await; + let deps = vec![Arc::new("hello") as Arc]; + let result = Result::::extract_from_deps(deps).await; // Verify the error message contains the expected type information assert!(result.is_err()); @@ -45,17 +37,10 @@ async fn test_result_type_mismatch_coverage() { #[tokio::test] async fn test_option_type_mismatch_coverage() { // Tests line 124 in extract.rs - Option type mismatch error message - // Create a channel that sends the WRONG type (i32 instead of Option) - let (tx, rx) = oneshot::channel::>(); - - // Send an i32 value - tokio::spawn(async move { - let _ = tx.send(Arc::new(42)); - }); - + // Create a dependency that provides the WRONG type (i32 instead of Option) // Try to extract as Option - this will fail with type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = Option::::extract_from_channels(deps).await; + let deps = vec![Arc::new(42) as Arc]; + let result = Option::::extract_from_deps(deps).await; // Verify the error message contains the expected type information assert!(result.is_err()); @@ -77,17 +62,10 @@ async fn test_option_type_mismatch_coverage() { #[tokio::test] async fn test_vec_type_mismatch_coverage() { // Tests line 152 in extract.rs - Vec type mismatch error message - // Create a channel that sends the WRONG type (String instead of Vec) - let (tx, rx) = oneshot::channel::>(); - - // Send a String value - tokio::spawn(async move { - let _ = tx.send(Arc::new("not a vec".to_string())); - }); - + // Create a dependency that provides the WRONG type (String instead of Vec) // Try to extract as Vec - this will fail with type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = Vec::::extract_from_channels(deps).await; + let deps = vec![Arc::new("not a vec".to_string()) as Arc]; + let result = Vec::::extract_from_deps(deps).await; // Verify the error message contains the expected type information assert!(result.is_err()); @@ -109,17 +87,10 @@ async fn test_vec_type_mismatch_coverage() { #[tokio::test] async fn test_hashmap_type_mismatch_coverage() { // Tests line 181 in extract.rs - HashMap type mismatch error message - // Create a channel that sends the WRONG type (Vec instead of HashMap) - let (tx, rx) = oneshot::channel::>>(); - - // Send a Vec value - tokio::spawn(async move { - let _ = tx.send(Arc::new(vec![1, 2, 3])); - }); - + // Create a dependency that provides the WRONG type (Vec instead of HashMap) // Try to extract as HashMap - this will fail with type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = HashMap::::extract_from_channels(deps).await; + let deps = vec![Arc::new(vec![1, 2, 3]) as Arc]; + let result = HashMap::::extract_from_deps(deps).await; // Verify the error message contains the expected type information assert!(result.is_err()); @@ -141,12 +112,12 @@ async fn test_hashmap_type_mismatch_coverage() { #[tokio::test] async fn test_arc_type_mismatch_coverage() { // Tests line 211 in extract.rs - Arc type mismatch error message - // Arc is special: it expects Arc> from dependency + // Arc is special: it expects Arc> from channel // Create a dependency that provides Arc instead of Arc> let deps = vec![Arc::new("not double arc".to_string()) as Arc]; // Try to extract as Arc - this will fail with type mismatch - // because it expects Arc> in the dependency + // because it expects Arc> in the channel let result = Arc::::extract_from_deps(deps).await; // Verify the error message contains the expected type information @@ -171,15 +142,9 @@ async fn test_arc_type_mismatch_coverage() { #[tokio::test] async fn test_result_with_wrong_ok_type() { // Test Result with wrong Ok type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(Ok("test".to_string()))); - }); - // Try to extract as Result - wrong Ok type - let deps = vec![Box::new(rx) as Box]; - let result = Result::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Ok::<_, String>("test".to_string())) as Arc]; + let result = Result::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -193,15 +158,9 @@ async fn test_result_with_wrong_ok_type() { #[tokio::test] async fn test_result_with_wrong_err_type() { // Test Result with wrong Err type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(Err(42))); - }); - // Try to extract as Result - wrong Err type - let deps = vec![Box::new(rx) as Box]; - let result = Result::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Err::(42)) as Arc]; + let result = Result::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -215,15 +174,9 @@ async fn test_result_with_wrong_err_type() { #[tokio::test] async fn test_option_with_wrong_inner_type() { // Test Option with wrong inner type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(Some("test".to_string()))); - }); - // Try to extract as Option - wrong inner type - let deps = vec![Box::new(rx) as Box]; - let result = Option::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Some("test".to_string())) as Arc]; + let result = Option::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -237,15 +190,9 @@ async fn test_option_with_wrong_inner_type() { #[tokio::test] async fn test_vec_with_wrong_element_type() { // Test Vec with wrong element type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(vec!["a".to_string(), "b".to_string()])); - }); - // Try to extract as Vec - wrong element type - let deps = vec![Box::new(rx) as Box]; - let result = Vec::::extract_from_channels(deps).await; + let deps = vec![Arc::new(vec!["a".to_string(), "b".to_string()]) as Arc]; + let result = Vec::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -259,17 +206,11 @@ async fn test_vec_with_wrong_element_type() { #[tokio::test] async fn test_hashmap_with_wrong_key_type() { // Test HashMap with wrong key type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let mut map = HashMap::new(); - map.insert(1, "one".to_string()); - let _ = tx.send(Arc::new(map)); - }); - + let mut map = HashMap::new(); + map.insert(1, "one".to_string()); // Try to extract as HashMap - wrong key type - let deps = vec![Box::new(rx) as Box]; - let result = HashMap::::extract_from_channels(deps).await; + let deps = vec![Arc::new(map) as Arc]; + let result = HashMap::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -283,17 +224,11 @@ async fn test_hashmap_with_wrong_key_type() { #[tokio::test] async fn test_hashmap_with_wrong_value_type() { // Test HashMap with wrong value type - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let mut map = HashMap::new(); - map.insert("key".to_string(), 42); - let _ = tx.send(Arc::new(map)); - }); - + let mut map = HashMap::new(); + map.insert("key".to_string(), 42); // Try to extract as HashMap - wrong value type - let deps = vec![Box::new(rx) as Box]; - let result = HashMap::::extract_from_channels(deps).await; + let deps = vec![Arc::new(map) as Arc]; + let result = HashMap::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -308,15 +243,9 @@ async fn test_hashmap_with_wrong_value_type() { async fn test_arc_with_wrong_inner_type() { // Test Arc with completely wrong inner type // Send Arc but expect Arc - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(Arc::new(42))); - }); - // Try to extract as Arc - wrong inner type - let deps = vec![Box::new(rx) as Box]; - let result = Arc::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Arc::new(Arc::new(42))) as Arc]; + let result = Arc::::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -330,15 +259,9 @@ async fn test_arc_with_wrong_inner_type() { #[tokio::test] async fn test_primitive_type_mismatch_for_completeness() { // Test primitive type mismatch (already covered in existing tests but good for completeness) - let (tx, rx) = oneshot::channel::>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new("not an i32".to_string())); - }); - // Try to extract as i32 - type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = i32::extract_from_channels(deps).await; + let deps = vec![Arc::new("not an i32".to_string()) as Arc]; + let result = i32::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); @@ -352,15 +275,10 @@ async fn test_primitive_type_mismatch_for_completeness() { #[tokio::test] async fn test_complex_nested_result_option() { // Test complex nested type: send Result, expect Result, String> - let (tx, rx) = oneshot::channel::>>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new(Ok("test".to_string()))); - }); - // Try to extract as Result, String> - nested type mismatch - let deps = vec![Box::new(rx) as Box]; - let result = Result::, String>::extract_from_channels(deps).await; + let deps = + vec![Arc::new(Ok::("test".to_string())) as Arc]; + let result = Result::, String>::extract_from_deps(deps).await; assert!(result.is_err()); let err_msg = result.unwrap_err(); diff --git a/src/extract/tests.rs b/src/extract/tests.rs index 3102d59..2967b61 100644 --- a/src/extract/tests.rs +++ b/src/extract/tests.rs @@ -38,11 +38,11 @@ impl TestTaskWithInput { async fn test_extract_result_type_single_dependency_error() { // Test Result with wrong dependency count let deps = vec![ - Box::new(oneshot::channel::>().1) as Box, - Box::new(oneshot::channel::>().1) as Box, + Arc::new(Ok::(5)) as Arc, + Arc::new(Err::("test".to_string())) as Arc, ]; // Two dependencies when expecting 1 - let result = Result::::extract_from_channels(deps).await; + let result = Result::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); } @@ -50,15 +50,9 @@ async fn test_extract_result_type_single_dependency_error() { #[tokio::test] async fn test_extract_result_type_success() { // Test Result successful extraction - let (tx, rx) = oneshot::channel::>>(); - // Send value in background - tokio::spawn(async move { - let _ = tx.send(Arc::new(Ok(42))); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Result::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Ok::(42)) as Arc]; + let result = Result::::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), Ok(42)); } @@ -67,12 +61,12 @@ async fn test_extract_result_type_success() { async fn test_extract_option_type_single_dependency_error() { // Test Option with wrong dependency count let deps = vec![ - Box::new(oneshot::channel::>().1) as Box, - Box::new(oneshot::channel::>().1) as Box, - Box::new(oneshot::channel::>().1) as Box, + Arc::new(None::) as Arc, + Arc::new(None::) as Arc, + Arc::new(None::) as Arc, ]; // Three dependencies when expecting 1 - let result = Option::::extract_from_channels(deps).await; + let result = Option::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 3")); } @@ -80,15 +74,8 @@ async fn test_extract_option_type_single_dependency_error() { #[tokio::test] async fn test_extract_option_type_success() { // Test Option successful extraction - let (tx, rx) = oneshot::channel::>>(); - - // Send value in background - tokio::spawn(async move { - let _ = tx.send(Arc::new(Some(99))); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Option::::extract_from_channels(deps).await; + let deps = vec![Arc::new(Some(99)) as Arc]; + let result = Option::::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), Some(99)); } @@ -98,7 +85,7 @@ async fn test_extract_vec_type_single_dependency_error() { // Test Vec with wrong dependency count let deps = vec![]; // Zero dependencies when expecting 1 - let result = Vec::::extract_from_channels(deps).await; + let result = Vec::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 0")); } @@ -106,15 +93,8 @@ async fn test_extract_vec_type_single_dependency_error() { #[tokio::test] async fn test_extract_vec_type_success() { // Test Vec successful extraction - let (tx, rx) = oneshot::channel::>>(); - - // Send value in background - tokio::spawn(async move { - let _ = tx.send(Arc::new(vec![1, 2, 3])); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Vec::::extract_from_channels(deps).await; + let deps = vec![Arc::new(vec![1, 2, 3]) as Arc]; + let result = Vec::::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), vec![1, 2, 3]); } @@ -123,29 +103,29 @@ async fn test_extract_vec_type_success() { async fn test_extract_primitive_single_dependency_error() { // Test primitive types with wrong dependency count let deps = vec![ - Box::new(oneshot::channel::().1) as Box, - Box::new(oneshot::channel::().1) as Box, + Arc::new(0) as Arc, + Arc::new(0) as Arc, ]; // Two dependencies when expecting 1 - let result = i32::extract_from_channels(deps).await; + let result = i32::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); // Test with String let deps = vec![ - Box::new(oneshot::channel::().1) as Box, - Box::new(oneshot::channel::().1) as Box, + Arc::new(String::default()) as Arc, + Arc::new(String::default()) as Arc, ]; - let result = String::extract_from_channels(deps).await; + let result = String::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); // Test with f64 let deps = vec![ - Box::new(oneshot::channel::().1) as Box, - Box::new(oneshot::channel::().1) as Box, + Arc::new(0.0) as Arc, + Arc::new(0.0) as Arc, ]; - let result = f64::extract_from_channels(deps).await; + let result = f64::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); } @@ -154,58 +134,36 @@ async fn test_extract_primitive_single_dependency_error() { async fn test_extract_unit_type() { // Test that unit type always succeeds regardless of dependencies let deps = vec![]; - let result = <()>::extract_from_channels(deps).await; + let result = <()>::extract_from_deps(deps).await; assert!(result.is_ok()); // Even with dependencies (ignored), unit type succeeds let deps = vec![ - Box::new(oneshot::channel::().1) as Box, - Box::new(oneshot::channel::().1) as Box, + Arc::new(0) as Arc, + Arc::new(0) as Arc, ]; - let result = <()>::extract_from_channels(deps).await; + let result = <()>::extract_from_deps(deps).await; assert!(result.is_ok()); } #[tokio::test] async fn test_extract_tuple_basic() { - // Test tuple extraction with channels - let (tx1, rx1) = oneshot::channel::>(); - let (tx2, rx2) = oneshot::channel::>(); - - // Send values in background - tokio::spawn(async move { - let _ = tx1.send(Arc::new(42)); - let _ = tx2.send(Arc::new(100)); - }); - + // Test tuple extraction let deps = vec![ - Box::new(rx1) as Box, - Box::new(rx2) as Box, + Arc::new(42) as Arc, + Arc::new(100) as Arc, ]; // Extract a tuple (i32, i32) - let result = <(i32, i32)>::extract_from_channels(deps).await; + let result = <(i32, i32)>::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), (42, 100)); } -#[tokio::test] -async fn test_extract_channel_closed() { - // Test error when channel is closed - let (tx, rx) = oneshot::channel::>(); - // Explicitly drop tx to close the channel - drop(tx); - - let deps = vec![Box::new(rx) as Box]; - let result = i32::extract_from_channels(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Channel closed")); -} - #[tokio::test] async fn test_extract_type_mismatch() { // Test type mismatch during downcast - // Try to extract as wrong type - put wrong parameter type in deps + // Try to extract as wrong type - put wrong receiver type in deps let deps = vec![Arc::new("hello".to_string()) as Arc]; let result = i32::extract_from_deps(deps).await; assert!(result.is_err()); @@ -217,14 +175,8 @@ async fn test_extract_all_primitive_types() { // Test that all primitive types in impl_extract_single! work correctly macro_rules! test_type { ($t:ty, $val:expr) => { - let (tx, rx) = oneshot::channel::>(); - - tokio::spawn(async move { - let _ = tx.send(Arc::new($val)); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = <$t>::extract_from_channels(deps).await; + let deps = vec![Arc::new($val) as Arc]; + let result = <$t>::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), $val); }; @@ -240,31 +192,16 @@ async fn test_extract_all_primitive_types() { #[tokio::test] async fn test_extract_larger_tuples() { // Test extraction for larger tuple sizes - let (tx1, rx1) = oneshot::channel::>(); - let (tx2, rx2) = oneshot::channel::>(); - let (tx3, rx3) = oneshot::channel::>(); - let (tx4, rx4) = oneshot::channel::>(); - let (tx5, rx5) = oneshot::channel::>(); - - // Send values in background - tokio::spawn(async move { - let _ = tx1.send(Arc::new(0)); - let _ = tx2.send(Arc::new(1)); - let _ = tx3.send(Arc::new(2)); - let _ = tx4.send(Arc::new(3)); - let _ = tx5.send(Arc::new(4)); - }); - let deps = vec![ - Box::new(rx1) as Box, - Box::new(rx2) as Box, - Box::new(rx3) as Box, - Box::new(rx4) as Box, - Box::new(rx5) as Box, + Arc::new(0) as Arc, + Arc::new(1) as Arc, + Arc::new(2) as Arc, + Arc::new(3) as Arc, + Arc::new(4) as Arc, ]; // Extract a 5-element tuple - let result = <(i32, i32, i32, i32, i32)>::extract_from_channels(deps).await; + let result = <(i32, i32, i32, i32, i32)>::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(result.unwrap(), (0, 1, 2, 3, 4)); } @@ -272,17 +209,11 @@ async fn test_extract_larger_tuples() { #[tokio::test] async fn test_extract_arc_type() { // Test Arc extraction - let (tx, rx) = oneshot::channel::>>(); - let test_arc = Arc::new("test".to_string()); let test_arc_clone = test_arc.clone(); - tokio::spawn(async move { - let _ = tx.send(Arc::new(test_arc)); - }); - - let deps = vec![Box::new(rx) as Box]; - let result = Arc::::extract_from_channels(deps).await; + let deps = vec![Arc::new(test_arc) as Arc]; + let result = Arc::::extract_from_deps(deps).await; assert!(result.is_ok()); assert_eq!(*result.unwrap(), *test_arc_clone); } @@ -293,11 +224,11 @@ use std::collections::HashMap; #[tokio::test] async fn test_hashmap_extract_wrong_dependency_count() { let deps = vec![ - Box::new(oneshot::channel::>>().1) as Box, - Box::new(oneshot::channel::>>().1) as Box, + Arc::new(HashMap::::default()) as Arc, + Arc::new(HashMap::::default()) as Arc, ]; - let result = HashMap::::extract_from_channels(deps).await; + let result = HashMap::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); } @@ -310,17 +241,6 @@ async fn test_hashmap_extract_type_mismatch() { assert!(result.unwrap_err().contains("Type mismatch")); } -#[tokio::test] -async fn test_hashmap_extract_channel_closed() { - let (tx, rx) = oneshot::channel::>>(); - drop(tx); - - let deps = vec![Box::new(rx) as Box]; - let result = HashMap::::extract_from_channels(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Channel closed")); -} - #[tokio::test] async fn test_result_extract_downcast_failure() { let deps = vec![Arc::new("wrong".to_string()) as Arc]; @@ -348,7 +268,7 @@ async fn test_vec_extract_downcast_failure() { #[tokio::test] async fn test_arc_extract_wrong_dependency_count() { let deps = vec![]; - let result = Arc::::extract_from_channels(deps).await; + let result = Arc::::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected 1 dependency, got 0")); } @@ -364,12 +284,12 @@ async fn test_arc_extract_downcast_failure() { #[tokio::test] async fn test_tuple_wrong_dependency_count() { let deps = vec![ - Box::new(oneshot::channel::>().1) as Box, - Box::new(oneshot::channel::>().1) as Box, - Box::new(oneshot::channel::>().1) as Box, + Arc::new(0) as Arc, + Arc::new(0) as Arc, + Arc::new(0) as Arc, ]; - let result = <(i32, i32)>::extract_from_channels(deps).await; + let result = <(i32, i32)>::extract_from_deps(deps).await; assert!(result.is_err()); assert!(result .unwrap_err() diff --git a/src/lib.rs b/src/lib.rs index 7495f44..492cc60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ //! //! ```no_run //! use dagx::{task, DagRunner, Task}; +//! use futures::FutureExt; // for FutureExt::map //! //! // Source task with read-only state (tuple struct) //! struct Value(i32); @@ -52,7 +53,7 @@ //! let sum = dag.add_task(Add).depends_on((&x, &y)); //! //! // Execute and retrieve results -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! assert_eq!(dag.get(sum).unwrap(), 5); //! # }; //! ``` @@ -290,6 +291,7 @@ //! //! ```no_run //! use dagx::{task, DagRunner, Task}; +//! use futures::FutureExt; //! //! // Source task (tuple struct) //! struct Value(i32); @@ -322,7 +324,7 @@ //! let plus1 = dag.add_task(Add(1)).depends_on(&base); //! let times2 = dag.add_task(Scale(2)).depends_on(&base); //! -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! //! assert_eq!(dag.get(plus1).unwrap(), 11); //! assert_eq!(dag.get(times2).unwrap(), 20); @@ -335,6 +337,7 @@ //! //! ```no_run //! use dagx::{task, DagRunner, Task}; +//! use futures::FutureExt; //! //! // Source task for String (tuple struct) //! struct Name(String); @@ -378,7 +381,7 @@ //! let active = dag.add_task(Active(true)); //! let result = dag.add_task(FormatUser).depends_on((&name, &age, &active)); //! -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! //! assert_eq!(dag.get(result).unwrap(), "User: Alice, Age: 30, Active: true"); //! # }; @@ -390,6 +393,7 @@ //! //! ```no_run //! use dagx::{task, DagRunner, Task}; +//! use futures::FutureExt; //! //! // Source task (tuple struct) //! struct Value(i32); @@ -430,7 +434,7 @@ //! // Layer 3: Final result //! let total = dag.add_task(Add).depends_on((&sum_xy, &prod_yz)); // 5 + 15 = 20 //! -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! //! assert_eq!(dag.get(total).unwrap(), 20); //! # }; @@ -442,7 +446,8 @@ //! the `#[task]` macro generates the necessary extraction logic: //! //! ```no_run -//! use dagx::{task, DagRunner, Task}; +//! # use dagx::{task, DagRunner, Task}; +//! # use futures::FutureExt; //! //! // Just derive Clone - that's all you need! //! #[derive(Clone)] @@ -473,7 +478,7 @@ //! let user = dag.add_task(CreateUser); //! let formatted = dag.add_task(FormatUser).depends_on(&user); //! -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! //! assert_eq!(dag.get(formatted).unwrap(), "Alice is 30 years old"); //! # }; @@ -530,6 +535,7 @@ //! //! ```no_run //! # use dagx::{task, DagRunner, Task}; +//! # use futures::FutureExt; //! # struct Value(i32); //! # #[task] //! # impl Value { @@ -539,11 +545,11 @@ //! # let dag = DagRunner::new(); //! # let node = dag.add_task(Value(42)); //! // Simple approach with .unwrap() -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! let result = dag.get(node).unwrap(); //! //! // Or handle errors explicitly -//! match dag.run().await { +//! match dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await { //! Ok(_) => println!("DAG executed successfully"), //! Err(e) => eprintln!("DAG execution failed: {}", e), //! } @@ -626,7 +632,8 @@ //! You output `T`, the framework handles the Arc wrapping: //! //! ``` -//! use dagx::{task, DagRunner, Task}; +//! # use dagx::{task, DagRunner, Task}; +//! # use futures::FutureExt; //! //! // ✅ CORRECT: Just output Vec, framework wraps in Arc internally //! struct FetchData; @@ -656,7 +663,7 @@ //! let task2 = dag.add_task(ProcessData).depends_on(&data); //! let task3 = dag.add_task(ProcessData).depends_on(&data); //! -//! dag.run().await.unwrap(); +//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); //! # }; //! ``` //! diff --git a/src/node/tests.rs b/src/node/tests.rs index c74a0b2..8f3b351 100644 --- a/src/node/tests.rs +++ b/src/node/tests.rs @@ -1,5 +1,7 @@ //! Unit tests for node module +use std::sync::Arc; + use crate::node::{ExecutableNode, TypedNode}; use crate::task::Task; use crate::types::NodeId; @@ -16,28 +18,6 @@ impl TestTask { } } -#[test] -fn test_create_output_channels() { - let node = TypedNode::new(NodeId(0), TestTask { value: 42 }); - - // Create channels for 3 dependents - let (senders, receivers) = node.create_output_channels(3); - - assert_eq!(senders.len(), 3); - assert_eq!(receivers.len(), 3); -} - -#[test] -fn test_create_output_channels_zero_dependents() { - let node = TypedNode::new(NodeId(0), TestTask { value: 42 }); - - // Create channels for 0 dependents (sink node) - let (senders, receivers) = node.create_output_channels(0); - - assert_eq!(senders.len(), 0); - assert_eq!(receivers.len(), 0); -} - struct TaskWithDependency; #[crate::task] @@ -48,15 +28,14 @@ impl TaskWithDependency { } #[tokio::test] -async fn test_execute_with_channels_missing_input() { +async fn test_execute_with_deps_missing_input() { // Test when task expects input but doesn't receive it let node = Box::new(TypedNode::new(NodeId(0), TaskWithDependency)); - // TaskWithDependency expects an i32 input, but we provide no receivers - let receivers = vec![]; - let senders = vec![]; + // TaskWithDependency expects an i32 input, but we provide no dependencies + let dependencies = vec![]; - let result = node.execute_with_channels(receivers, senders).await; + let result = node.execute_with_deps(dependencies).await; assert!(result.is_err()); if let Err(crate::error::DagError::InvalidDependency { task_id }) = result { @@ -76,11 +55,11 @@ async fn test_execute_with_deps_success() { let dep_dependencies = vec![]; // Execute in background - let dep_handle = tokio::spawn(async move { - dependency_node - .execute_with_channels(dep_receivers, dep_senders) + let dep_result = + tokio::spawn(async move { dependency_node.execute_with_deps(dep_dependencies).await }) .await - }); + .unwrap() + .unwrap(); // Execute dependent node with receiver let dependencies = vec![dep_result as Arc]; @@ -91,14 +70,14 @@ async fn test_execute_with_deps_success() { } #[tokio::test] -async fn test_execute_with_channels_sink_node() { +async fn test_execute_with_deps_sink_node() { // Test that sink nodes (no dependents) return their output let node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 42 })); // No senders means this is a sink node let dependencies = vec![]; - let result = node.execute_with_channels(receivers, senders).await; + let result = node.execute_with_deps(dependencies).await; assert!(result.is_ok()); // Check output was returned (Arc-wrapped) @@ -114,41 +93,12 @@ async fn test_execute_with_deps_non_sink_node() { // Execute with senders (non-sink node) let node = Box::new(node); - let exec_receivers = vec![]; - let result = node.execute_with_channels(exec_receivers, senders).await; + let result = node.execute_with_deps(vec![]).await; assert!(result.is_ok()); // Verify output was sent through channel (Arc-wrapped) let arc_value = result.unwrap().downcast::().unwrap(); // Also verify output was returned (Arc-wrapped) - let output = result.unwrap(); - let arc_value = output.downcast::().unwrap(); assert_eq!(*arc_value, 42); } - -#[tokio::test] -async fn test_execute_with_multiple_dependents() { - // Test that output is sent to multiple dependents - let node = TypedNode::new(NodeId(0), TestTask { value: 100 }); - - // Create output channels for three dependents - let (senders, receivers) = node.create_output_channels(3); - - // Execute node - let node = Box::new(node); - let exec_receivers = vec![]; - let result = node.execute_with_channels(exec_receivers, senders).await; - assert!(result.is_ok()); - - // Verify all three dependents receive the output (Arc-wrapped) - for receiver in receivers { - let rx = receiver - .downcast::>>() - .unwrap(); - - let received_arc = rx.await; - assert!(received_arc.is_ok()); - assert_eq!(*received_arc.unwrap(), 100); - } -} diff --git a/src/runner.rs b/src/runner.rs index 243d9d7..c3b8d6b 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -56,6 +56,7 @@ impl<'a> Drop for RunGuard<'a> { /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; +/// # use futures::FutureExt; /// // Task with state constructed via ::new() /// struct LoadValue { value: i32 } /// @@ -84,7 +85,7 @@ impl<'a> Drop for RunGuard<'a> { /// let y = dag.add_task(LoadValue::new(3)); /// let sum = dag.add_task(Add).depends_on((&x, &y)); /// -/// dag.run().await.unwrap(); +/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// /// assert_eq!(dag.get(sum).unwrap(), 5); /// # }; @@ -149,6 +150,7 @@ impl DagRunner { /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; + /// # use futures::FutureExt; /// // Task with state - shows you construct with specific value /// struct LoadValue { /// initial: i32, @@ -190,7 +192,7 @@ impl DagRunner { /// // Construct task with offset of 1 /// let inc = dag.add_task(AddOffset::new(1)).depends_on(&base); /// - /// dag.run().await.unwrap(); + /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// assert_eq!(dag.get(&inc).unwrap(), 11); /// # }; /// ``` @@ -254,6 +256,7 @@ impl DagRunner { /// /// ``` /// # use dagx::{task, DagRunner, Task}; + /// # use futures::FutureExt; /// // Tuple struct /// struct Value(i32); /// @@ -277,7 +280,7 @@ impl DagRunner { /// let b = dag.add_task(Value(2)); /// let sum = dag.add_task(Add).depends_on((&a, &b)); /// - /// dag.run().await.unwrap(); // Executes all tasks + /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); // Executes all tasks /// # }; /// ``` #[inline] @@ -527,6 +530,7 @@ impl DagRunner { /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; + /// # use futures::FutureExt; /// struct Configuration { /// setting: i32, /// } @@ -548,7 +552,7 @@ impl DagRunner { /// // Construct task with specific setting value /// let task = dag.add_task(Configuration::new(42)); /// - /// dag.run().await.unwrap(); + /// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// /// assert_eq!(dag.get(task).unwrap(), 42); /// # }; diff --git a/src/runner/coverage_tests.rs b/src/runner/coverage_tests.rs index 7ad7bb8..1e6ed6e 100644 --- a/src/runner/coverage_tests.rs +++ b/src/runner/coverage_tests.rs @@ -6,6 +6,7 @@ use crate::error::DagError; use crate::runner::DagRunner; use crate::task::Task; use crate::types::TaskHandle; +use futures::FutureExt; use std::collections::HashMap; #[tokio::test] @@ -20,11 +21,9 @@ async fn test_get_type_mismatch_error() { // Task outputs i32 let source = dag.add_task(task_fn(|_: ()| async { 42i32 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get as String - TYPE MISMATCH! // Create a handle with the same ID but wrong type @@ -56,11 +55,9 @@ async fn test_get_type_mismatch_i32_to_vec() { // Task outputs i32 let int_task = dag.add_task(task_fn(|_: ()| async { 123i32 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get as Vec let wrong_handle: TaskHandle> = TaskHandle { @@ -102,11 +99,9 @@ async fn test_get_type_mismatch_struct_to_hashmap() { let dag = DagRunner::new(); let task_handle = dag.add_task(CustomTask); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get as HashMap let wrong_handle: TaskHandle> = TaskHandle { @@ -136,11 +131,9 @@ async fn test_get_type_mismatch_vec_different_types() { // Task outputs Vec let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get as Vec let wrong_handle: TaskHandle> = TaskHandle { @@ -170,11 +163,9 @@ async fn test_get_type_mismatch_option_types() { // Task outputs Option let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get as Option let wrong_handle: TaskHandle> = TaskHandle { @@ -206,11 +197,9 @@ async fn test_multiple_type_mismatches_same_dag() { let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let bool_task = dag.add_task(task_fn(|_: ()| async { true })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get int_task as String let wrong_int_handle: TaskHandle = TaskHandle { diff --git a/src/runner/tests.rs b/src/runner/tests.rs index c4d0671..f48e0d6 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -5,6 +5,8 @@ use crate::runner::DagRunner; use crate::task::Task; use crate::types::TaskHandle; +use futures::FutureExt; + // Initialize tracing subscriber for tests (idempotent) #[cfg(feature = "tracing")] fn init_tracing() { @@ -68,11 +70,9 @@ async fn test_get_wrong_type() { let dag = DagRunner::new(); let handle = dag.add_task(TestTask { value: 42 }); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Try to get with wrong type - downcast will fail let fake_handle: TaskHandle = TaskHandle { @@ -145,18 +145,12 @@ async fn test_concurrent_run_protection() { // Start two runs concurrently let handle1 = tokio::spawn(async move { barrier1.wait().await; - dag1.run(|fut| { - tokio::spawn(fut); - }) - .await + dag1.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await }); let handle2 = tokio::spawn(async move { barrier2.wait().await; - dag2.run(|fut| { - tokio::spawn(fut); - }) - .await + dag2.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await }); // One should succeed, one should fail (concurrent execution not supported) @@ -205,11 +199,9 @@ async fn test_multiple_get_calls() { let dag = DagRunner::new(); let handle = dag.add_task(TestTask { value: 100 }); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Multiple get calls should all return the same value let result1 = dag.get(&handle).unwrap(); @@ -233,11 +225,9 @@ async fn test_invalid_node_id_in_get() { _phantom: std::marker::PhantomData, }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result = dag.get(invalid_handle); assert!(result.is_err()); @@ -288,11 +278,7 @@ async fn test_task_panic_in_multi_task_layer() { let _good_task = dag.add_task(GoodTask).depends_on(&source); // Run the DAG - the panic should be caught - let result = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // The run should fail due to the panic // Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime diff --git a/src/task/tests.rs b/src/task/tests.rs index ee2b74b..901ded5 100644 --- a/src/task/tests.rs +++ b/src/task/tests.rs @@ -3,6 +3,7 @@ use crate::task::{task_fn, Task}; use std::future::Future; use std::pin::Pin; +use std::sync::Arc; // Test the task_fn function and TaskFn struct #[tokio::test] @@ -87,7 +88,7 @@ impl Task for CustomTask { #[allow(refining_impl_trait)] fn extract_and_run( self, - _receivers: Vec>, + _dependencies: Vec>, ) -> Pin> + Send>> { Box::pin(async move { // This is a test helper - just return an error diff --git a/src/types.rs b/src/types.rs index 7968d60..70a1467 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,9 +6,9 @@ use crate::builder::{IsUnitType, TaskBuilder}; use crate::task::Task; -/// Opaque node identifier (internal use) +/// Opaque node identifier #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub(crate) struct NodeId(pub usize); +pub struct NodeId(pub usize); /// Opaque, typed token for a node's output. /// @@ -23,6 +23,7 @@ pub(crate) struct NodeId(pub usize); /// /// ```no_run /// # use dagx::{task, DagRunner, Task}; +/// # use futures::FutureExt; /// # struct LoadValue { value: i32 } /// # impl LoadValue { pub fn new(v: i32) -> Self { Self { value: v } } } /// # #[task] @@ -33,7 +34,7 @@ pub(crate) struct NodeId(pub usize); /// let dag = DagRunner::new(); /// let node = dag.add_task(LoadValue::new(42)); /// -/// dag.run().await.unwrap(); +/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); /// /// assert_eq!(dag.get(node).unwrap(), 42); /// # }; diff --git a/src/types/coverage_tests.rs b/src/types/coverage_tests.rs index e5b5dd2..37f4a67 100644 --- a/src/types/coverage_tests.rs +++ b/src/types/coverage_tests.rs @@ -4,6 +4,7 @@ use crate::types::TaskHandle; use crate::{task, DagRunner, Task}; +use futures::FutureExt; #[tokio::test] async fn test_unit_type_taskbuilder_to_handle_conversion() { @@ -31,11 +32,9 @@ async fn test_unit_type_taskbuilder_to_handle_conversion() { // specifically covering line 71 let handle: TaskHandle = builder.into(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Verify the task ran and we can get its output assert_eq!(dag.get(handle).unwrap(), 42); @@ -81,11 +80,9 @@ async fn test_unit_type_conversion_with_multiple_tasks() { let second_handle: TaskHandle = second_builder.into(); let third_handle: TaskHandle = third_builder.into(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Verify all outputs assert_eq!(dag.get(first_handle).unwrap(), "first"); @@ -122,11 +119,9 @@ async fn test_unit_struct_with_state() { // Explicit conversion - triggers line 71 let handle: TaskHandle = builder.into(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(handle).unwrap(), 42); } @@ -151,11 +146,9 @@ async fn test_taskbuilder_conversion_assignment() { let handle: TaskHandle> = builder.into(); let id = handle.id; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result = dag.get(handle).unwrap(); assert_eq!(result, vec![1, 2, 3]); @@ -197,11 +190,9 @@ async fn test_mixed_unit_and_dependent_tasks() { // Doubler needs dependencies, so we use it differently let doubler = dag.add_task(Doubler).depends_on(source_handle); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(source_handle).unwrap(), 10); assert_eq!(dag.get(doubler).unwrap(), 20); diff --git a/tests/arc_tests.rs b/tests/arc_tests.rs index 98d3490..eb21e24 100644 --- a/tests/arc_tests.rs +++ b/tests/arc_tests.rs @@ -10,6 +10,7 @@ //! These tests serve as both validation and documentation for proper Arc usage. use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -43,11 +44,9 @@ async fn test_arc_basic_pipeline() { let producer = dag.add_task(Producer); let consumer = dag.add_task(Consumer).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: producer is not a sink (consumer depends on it), so we can't get() its value // Only sink nodes (nodes with no dependents) store their outputs for retrieval @@ -101,11 +100,9 @@ async fn test_arc_with_primitives() { .add_task(AllConsumer) .depends_on((&i32_prod, &f64_prod, &bool_prod)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: producers are not sinks (consumer depends on them), so we can't get() their values // The consumer output verifies all three producers worked correctly @@ -177,11 +174,9 @@ async fn test_arc_with_collections() { .add_task(CollectionConsumer) .depends_on((&vec_prod, &map_prod, &set_prod)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: producers are not sinks (consumer depends on them), so we can't get() their values // The consumer output verifies all three collection producers worked correctly @@ -250,11 +245,9 @@ async fn test_arc_with_custom_structs() { let age_ext = dag.add_task(AgeExtractor).depends_on(&person_prod); let email_ext = dag.add_task(EmailCountExtractor).depends_on(&person_prod); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: person_prod is not a sink (3 extractors depend on it), so we can't get() its value // The extractor outputs verify the person was created and shared correctly @@ -318,11 +311,9 @@ async fn test_arc_fan_out_three_consumers() { let c2 = dag.add_task(FirstConsumer).depends_on(&producer); let c3 = dag.add_task(JoinConsumer).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(c1).unwrap(), 5); assert_eq!(dag.get(c2).unwrap(), "item1"); @@ -394,11 +385,9 @@ async fn test_arc_fan_out_five_consumers_large_data() { let c4 = dag.add_task(PatternCounter).depends_on(&producer); let c5 = dag.add_task(TotalBytesComputer).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: producer is not a sink (has 5 consumers), so we can't get() its value // We verify correctness through the consumer outputs instead @@ -486,11 +475,9 @@ async fn test_arc_nested_fan_out_tree() { let l2_negate = dag.add_task(Level2Negate).depends_on(l1_prod); let l2_mod = dag.add_task(Level2Mod10).depends_on(l1_prod); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: l1_sum and l1_prod are not sinks (they have level 2 tasks depending on them) // Only the level 2 tasks are sinks and can be retrieved @@ -556,11 +543,9 @@ async fn test_arc_performance_vs_clone() { let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let elapsed = start.elapsed(); @@ -658,11 +643,9 @@ async fn test_arc_with_result() { let consumer3 = dag.add_task(ArcResultConsumer2).depends_on(&arc_result_err); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(consumer1).unwrap(), "Got Ok: Success"); assert_eq!(dag.get(consumer2).unwrap(), "Got Arc: Success"); @@ -767,11 +750,9 @@ async fn test_arc_with_option() { let c3 = dag.add_task(OptionArcConsumer1).depends_on(&opt_arc_some); let c4 = dag.add_task(OptionArcConsumer2).depends_on(&opt_arc_none); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(c1).unwrap(), "Some(42)"); assert_eq!(dag.get(c2).unwrap(), "None"); @@ -824,11 +805,9 @@ async fn test_arc_diamond_dependency() { let right = dag.add_task(RightBranch).depends_on(&top); let bottom = dag.add_task(Bottom).depends_on((&left, &right)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(bottom).unwrap(), "Sum: 15, Product: 120"); } @@ -894,11 +873,9 @@ async fn test_arc_empty_collections() { .add_task(EmptyConsumer) .depends_on((&vec_task, &string_task, &map_task)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!( dag.get(consumer).unwrap(), @@ -967,11 +944,9 @@ async fn test_arc_very_large_data() { let last = dag.add_task(LastSampler).depends_on(&producer); let middle = dag.add_task(MiddleSampler).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(counter).unwrap(), SIZE); assert!(dag.get(first).unwrap().contains("Item number 0")); @@ -1006,11 +981,9 @@ async fn test_arc_of_arc() { let producer = dag.add_task(NestedProducer); let consumer = dag.add_task(NestedConsumer).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(consumer).unwrap(), "Value: Nested Arc"); } @@ -1072,11 +1045,9 @@ async fn test_arc_mixed_with_non_arc() { let unwrap = dag.add_task(Unwrapper).depends_on(&arc_prod); let wrap = dag.add_task(Wrapper).depends_on(&non_arc_prod); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!( dag.get(mixed).unwrap(), @@ -1142,11 +1113,9 @@ async fn test_arc_parallel_execution() { let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let elapsed = start.elapsed(); @@ -1193,11 +1162,9 @@ async fn test_arc_reference_counting() { let producer = dag.add_task(RefCountProducer); let checker = dag.add_task(RefCountChecker).depends_on(&producer); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: producer is not a sink (checker depends on it), so we can't get() its value // The checker task verifies the Arc was passed correctly @@ -1271,9 +1238,7 @@ async fn test_arc_massive_fanout_performance_comparison() { let start_without_arc = Instant::now(); dag_without_arc - .run(|fut| { - tokio::spawn(fut); - }) + .run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await .unwrap(); let duration_without_arc = start_without_arc.elapsed(); @@ -1325,9 +1290,7 @@ async fn test_arc_massive_fanout_performance_comparison() { let start_with_arc = Instant::now(); dag_with_arc - .run(|fut| { - tokio::spawn(fut); - }) + .run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await .unwrap(); let duration_with_arc = start_with_arc.elapsed(); diff --git a/tests/boundaries/deep_chains.rs b/tests/boundaries/deep_chains.rs index 7fc790c..28fb521 100644 --- a/tests/boundaries/deep_chains.rs +++ b/tests/boundaries/deep_chains.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -19,10 +20,7 @@ async fn test_1000_level_deep_chain() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(current)?, 999); Ok(()) @@ -52,10 +50,7 @@ async fn test_deep_chain_with_branches() -> DagResult<()> { } } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(main_chain)?, 499); @@ -113,10 +108,7 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { &chain_ends[7], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Each chain: start + 500 // Chain 0: 0 + 500 = 500 @@ -161,10 +153,7 @@ async fn test_deep_chain_execution_order() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(order.load(Ordering::SeqCst), 100); assert_eq!(dag.get(current)?, 99); @@ -192,10 +181,7 @@ async fn test_deep_binary_tree() -> DagResult<()> { let root = build_tree(&dag, 10, 1); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Sum of all values in a complete binary tree with values 1024..2047 // This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552 @@ -225,10 +211,7 @@ async fn test_fibonacci_chain() -> DagResult<()> { prev1 = next; } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // 50th Fibonacci number (0-indexed) = 7778742049 assert_eq!(dag.get(prev1)?, 7778742049); @@ -251,10 +234,7 @@ async fn test_deep_chain_with_accumulator() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(current)?; assert_eq!(result, 20100); // sum of 1..=200 @@ -292,10 +272,7 @@ async fn test_zigzag_dependencies() -> DagResult<()> { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((&chain_a, &chain_b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(final_task)?; assert!(result > 100); @@ -318,10 +295,7 @@ async fn test_10000_level_chain_stress() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(current)?, 9_999); Ok(()) diff --git a/tests/boundaries/large_scale.rs b/tests/boundaries/large_scale.rs index e3b5768..520974d 100644 --- a/tests/boundaries/large_scale.rs +++ b/tests/boundaries/large_scale.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -25,10 +26,7 @@ async fn test_10000_independent_tasks() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all tasks completed assert_eq!(counter.load(Ordering::Relaxed), 10_000); @@ -103,10 +101,7 @@ async fn test_5000_node_pyramid() -> DagResult<()> { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((¤t_layer[0], ¤t_layer[1])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // We created 1024 ones and 1024 zeros at the bottom // The sum should propagate up to 1024 @@ -139,10 +134,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify some sinks completed correctly let result0 = dag.get(sinks[0])?; @@ -195,10 +187,7 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> { chain_ends.push(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // All 10,000 tasks should have executed assert_eq!(counter.load(Ordering::Relaxed), 10_000); @@ -239,10 +228,7 @@ async fn test_100000_nodes_stress() -> DagResult<()> { .collect(); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(completed.load(Ordering::Relaxed), 100_000); Ok(()) diff --git a/tests/boundaries/type_limits.rs b/tests/boundaries/type_limits.rs index 7de122c..173aa62 100644 --- a/tests/boundaries/type_limits.rs +++ b/tests/boundaries/type_limits.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::marker::PhantomData; #[tokio::test] @@ -38,10 +39,7 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> { })) .depends_on(&t2); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t1)?, Empty); assert_eq!(dag.get(t2)?, 42); @@ -76,10 +74,7 @@ async fn test_large_value_types() -> DagResult<()> { })) .depends_on(&t1); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t2)?, 1042); @@ -98,10 +93,7 @@ async fn test_unit_type_chains() -> DagResult<()> { .add_task(task_fn(|_: ()| async { "done" })) .depends_on(&t3); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.get(t1)?; // Verify no error dag.get(t2)?; @@ -132,10 +124,7 @@ async fn test_nested_option_result_types() -> DagResult<()> { })) .depends_on(&t1); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t2)?, 55); @@ -159,10 +148,7 @@ async fn test_tuple_of_tuples() -> DagResult<()> { )) .depends_on((&t1, &t2)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t3)?, 36); @@ -196,10 +182,7 @@ async fn test_string_types_variety() -> DagResult<()> { )) .depends_on((&t1, &t2, &t3, &t4, &t5, &t6)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(combined)?; assert!(result.contains("static str")); @@ -228,10 +211,7 @@ async fn test_generic_type_constraints() -> DagResult<()> { )) .depends_on((&t1, &t2, &t3)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(combined)?; assert!(result.contains("Wrapper(42)")); @@ -257,10 +237,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> { .add_task(task_fn(|val: i32| async move { val * 2 })) .depends_on(&t1); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t2)?, 200); @@ -285,10 +262,7 @@ async fn test_array_types() -> DagResult<()> { })) .depends_on((&t1, &t2)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(t3)?, 55); @@ -327,10 +301,7 @@ async fn test_enum_variants() -> DagResult<()> { )) .depends_on((&t1, &t2, &t3, &t4)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert!(dag.get(combined)?); diff --git a/tests/boundaries/zero_tasks.rs b/tests/boundaries/zero_tasks.rs index 974cc37..c745ceb 100644 --- a/tests/boundaries/zero_tasks.rs +++ b/tests/boundaries/zero_tasks.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{task, DagResult, DagRunner, Task}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -10,10 +11,7 @@ async fn test_empty_dag_execution() -> DagResult<()> { // Test that an empty DAG can be created and run without issues let dag = DagRunner::new(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Should complete successfully with no tasks Ok(()) @@ -26,10 +24,7 @@ async fn test_single_task_no_deps() -> DagResult<()> { let task = dag.add_task(task_fn(|_: ()| async { 42 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(task)?, 42); Ok(()) @@ -50,10 +45,7 @@ async fn test_single_task_unit_input_output() -> DagResult<()> { } })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.get(task)?; // Just verify it doesn't error assert_eq!(executed.load(Ordering::SeqCst), 1); @@ -70,10 +62,7 @@ async fn test_zero_sized_type_task() -> DagResult<()> { let task = dag.add_task(task_fn(|_: ()| async { ZeroSized })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(&task)?, ZeroSized); Ok(()) @@ -85,10 +74,7 @@ async fn test_empty_dag_multiple_runs() -> DagResult<()> { let dag = DagRunner::new(); for _ in 0..10 { - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; } Ok(()) @@ -103,10 +89,7 @@ async fn test_dag_with_only_source_tasks() -> DagResult<()> { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task)?, i); @@ -132,10 +115,7 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> { let dag = DagRunner::new(); let task = dag.add_task(Counter { value: 21 }); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(task)?, 42); Ok(()) @@ -149,17 +129,11 @@ async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> { if i % 2 == 0 { // Empty DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; } else { // DAG with a task let task = dag.add_task(task_fn(move |_: ()| async move { i })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(task)?, i); } } @@ -179,10 +153,7 @@ async fn test_dag_with_never_type_simulation() -> DagResult<()> { // This task completes immediately but returns a Result containing "never" type let task = dag.add_task(task_fn(|_: ()| async { Result::::Ok(42) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(&task)?, Ok(42)); Ok(()) @@ -199,10 +170,7 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> { 1337 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(task)?, 1337); Ok(()) diff --git a/tests/custom_type_test.rs b/tests/custom_type_test.rs index 623d330..f953ca5 100644 --- a/tests/custom_type_test.rs +++ b/tests/custom_type_test.rs @@ -1,5 +1,6 @@ // Test that custom types work without any trait implementations use dagx::{task, DagRunner, Task}; +use futures::FutureExt; #[derive(Clone, Debug, PartialEq)] struct Person { @@ -56,11 +57,9 @@ async fn test_custom_type_single_dependency() { let fetch = dag.add_task(FetchPerson); let process = dag.add_task(ProcessPerson).depends_on(&fetch); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result = dag.get(process).unwrap(); assert_eq!(result, "Alice is 30 years old"); @@ -74,11 +73,9 @@ async fn test_custom_type_passthrough() { let transform = dag.add_task(PersonToCompany).depends_on(&fetch); let process = dag.add_task(ProcessPerson).depends_on(&fetch); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let person_result = dag.get(process).unwrap(); let company_result = dag.get(transform).unwrap(); @@ -99,11 +96,9 @@ async fn test_custom_type_diamond_pattern() { let path_a = dag.add_task(ProcessPerson).depends_on(&fetch); let path_b = dag.add_task(PersonToCompany).depends_on(&fetch); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result_a = dag.get(path_a).unwrap(); let result_b = dag.get(path_b).unwrap(); @@ -121,11 +116,9 @@ async fn test_custom_type_fan_out() { let process2 = dag.add_task(ProcessPerson).depends_on(&fetch); let process3 = dag.add_task(ProcessPerson).depends_on(&fetch); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let result1 = dag.get(process1).unwrap(); let result2 = dag.get(process2).unwrap(); diff --git a/tests/cycle_prevention.rs b/tests/cycle_prevention.rs index d6cf1a7..7aab098 100644 --- a/tests/cycle_prevention.rs +++ b/tests/cycle_prevention.rs @@ -4,6 +4,7 @@ //! prevents it through the type-state pattern. use dagx::{task, DagRunner, Task, TaskHandle}; +use futures::FutureExt; /// Demonstrates that TaskBuilder is consumed by depends_on, /// preventing you from wiring the same task multiple times @@ -270,11 +271,9 @@ async fn test_type_safety_prevents_invalid_graphs() { let result = dag.add_task(Aggregate).depends_on((&t1, &t2)); // Execute and verify - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(result).unwrap(), 40); // (10*2) + (10*2) diff --git a/tests/dependencies/resolution.rs b/tests/dependencies/resolution.rs index 6461e47..c68317b 100644 --- a/tests/dependencies/resolution.rs +++ b/tests/dependencies/resolution.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::{Arc, Mutex}; #[tokio::test] @@ -81,10 +82,7 @@ async fn test_dependency_resolution_order() -> DagResult<()> { .depends_on(d) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(e)?, 5); @@ -118,10 +116,7 @@ async fn test_transitive_dependencies() -> DagResult<()> { .add_task(task_fn(|x: i32| async move { x + 5 })) .depends_on(b); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(&a)?, 10); assert_eq!(dag.get(b)?, 20); @@ -144,10 +139,7 @@ async fn test_deep_chain_50_levels() -> DagResult<()> { handles.push(next); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(handles.last().unwrap())?, 50); Ok(()) @@ -166,10 +158,7 @@ async fn test_deep_chain_200_levels() -> DagResult<()> { .depends_on(current); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(current)?, 201); Ok(()) diff --git a/tests/dependencies/single.rs b/tests/dependencies/single.rs index 26fa742..cff5237 100644 --- a/tests/dependencies/single.rs +++ b/tests/dependencies/single.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; #[tokio::test] async fn test_single_dependency() -> DagResult<()> { @@ -12,10 +13,7 @@ async fn test_single_dependency() -> DagResult<()> { .add_task(task_fn(|x: i32| async move { x + 1 })) .depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(dependent)?, 43); Ok(()) @@ -42,10 +40,7 @@ async fn test_dependency_data_flow() -> DagResult<()> { .add_task(task_fn(|(s, p): (i32, i32)| async move { s + p })) .depends_on((&sum, &product)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Note: sum and product are not sinks (final_result depends on them) // Only final_result is a sink and can be retrieved @@ -69,10 +64,7 @@ async fn test_dependencies_with_different_types() -> DagResult<()> { })) .depends_on((&int_source, &string_source, &bool_source)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(combined)?, "hello: 42 (true)"); Ok(()) @@ -93,10 +85,7 @@ async fn test_shared_dependencies() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all dependents got the correct value from shared for (i, handle) in dependents.iter().enumerate() { @@ -115,10 +104,7 @@ async fn test_multiple_source_nodes() -> DagResult<()> { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 10 }))) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; for (i, source) in sources.iter().enumerate() { assert_eq!(dag.get(source)?, i as i32 * 10); diff --git a/tests/dependencies/tuples.rs b/tests/dependencies/tuples.rs index 513c21d..1126334 100644 --- a/tests/dependencies/tuples.rs +++ b/tests/dependencies/tuples.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; #[tokio::test] async fn test_two_dependencies() -> DagResult<()> { @@ -13,10 +14,7 @@ async fn test_two_dependencies() -> DagResult<()> { .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .depends_on((&a, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 30); Ok(()) @@ -35,10 +33,7 @@ async fn test_three_dependencies() -> DagResult<()> { )) .depends_on((&a, &b, &c)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 6); Ok(()) @@ -58,10 +53,7 @@ async fn test_four_dependencies() -> DagResult<()> { })) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 10); // 1+2+3+4 Ok(()) @@ -81,10 +73,7 @@ async fn test_five_dependencies() -> DagResult<()> { )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 15); // 1+2+3+4+5 Ok(()) @@ -106,10 +95,7 @@ async fn test_six_dependencies() -> DagResult<()> { &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5] )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 21); // 1+2+3+4+5+6 Ok(()) @@ -133,10 +119,7 @@ async fn test_seven_dependencies() -> DagResult<()> { &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 28); // 1+2+3+4+5+6+7 Ok(()) @@ -160,10 +143,7 @@ async fn test_eight_dependencies() -> DagResult<()> { &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5], &deps[6], &deps[7], )); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 36); // Sum of 1..=8 Ok(()) diff --git a/tests/errors/error_types.rs b/tests/errors/error_types.rs index 9842286..68e9fc1 100644 --- a/tests/errors/error_types.rs +++ b/tests/errors/error_types.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagError, DagRunner}; +use futures::FutureExt; #[tokio::test] async fn test_result_not_found_error() { @@ -14,11 +15,9 @@ async fn test_result_not_found_error() { assert!(matches!(result, Err(DagError::ResultNotFound { .. }))); // Run and verify it works after - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(&t1).unwrap(), 42); } @@ -34,11 +33,9 @@ async fn test_nested_result_error_handling() { let task2 = dag.add_task(task_fn(|_: ()| async { Result::::Ok(42) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Both tasks complete successfully (they return Results) assert_eq!(dag.get(&task1).unwrap(), Err("Internal error".to_string())); @@ -51,11 +48,9 @@ async fn test_option_none_not_error() { let task = dag.add_task(task_fn(|_: ()| async { None:: })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // None is a valid result, not an error assert_eq!(dag.get(&task).unwrap(), None); @@ -82,11 +77,9 @@ async fn test_custom_error_types() { }) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Check custom errors are preserved match dag.get(&task1).unwrap() { @@ -113,11 +106,7 @@ async fn test_error_in_tuple_dependency() { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((&ok_task, &err_task)); - let result = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; assert!(result.is_err()); assert!(dag.get(dependent).is_err()); @@ -135,11 +124,7 @@ async fn test_string_panic_vs_structured_panic() { panic!("Formatted panic: value={}, code={}", 42, "ERROR"); })); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // Both should result in errors assert!(dag.get(&string_panic).is_err()); @@ -159,11 +144,9 @@ async fn test_anyhow_like_error_chain() { outer })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Get the chained error let result = dag.get(&task).unwrap(); diff --git a/tests/errors/propagation.rs b/tests/errors/propagation.rs index 3e4a713..be3c01a 100644 --- a/tests/errors/propagation.rs +++ b/tests/errors/propagation.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::DagRunner; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -22,11 +23,7 @@ async fn test_linear_error_propagation() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(t3); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // t1 succeeds assert_eq!(dag.get(&t1).unwrap(), 1); @@ -65,11 +62,7 @@ async fn test_diamond_error_propagation() { .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .depends_on((&left, &right)); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; assert_eq!(dag.get(&source).unwrap(), 100); assert_eq!(dag.get(left).unwrap(), 50); @@ -119,11 +112,7 @@ async fn test_error_stops_at_boundary() { } })); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // Error branch: source runs but dependent doesn't assert!(dag.get(&error_source).is_err()); @@ -156,11 +145,7 @@ async fn test_multiple_error_sources_convergence() { .add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 })) .depends_on((&error1, &error2, &success)); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // All error sources fail assert!(dag.get(&error1).is_err()); @@ -189,11 +174,7 @@ async fn test_error_in_wide_fanout() { }) .collect(); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // Source fails assert!(dag.get(&source).is_err()); @@ -234,11 +215,9 @@ async fn test_selective_error_propagation() { })) .depends_on(handler); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Source returns an Err variant (not a panic) assert_eq!(dag.get(&source).unwrap(), Err("Source error".to_string())); @@ -294,11 +273,7 @@ async fn test_error_propagation_timing() { })) .depends_on(&t2); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; let order = propagation_order.lock().clone(); @@ -373,11 +348,9 @@ async fn test_partial_branch_failure_propagation() { )) .depends_on((&a3, &b3, &c3)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .ok(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .ok(); // Branch A succeeds assert_eq!(dag.get(a3).unwrap(), 106); diff --git a/tests/errors/recovery.rs b/tests/errors/recovery.rs index 33f741d..84e63a5 100644 --- a/tests/errors/recovery.rs +++ b/tests/errors/recovery.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; @@ -23,11 +24,7 @@ async fn test_fallback_on_error() -> DagResult<()> { let _selector = dag.add_task(task_fn(|_: ()| async {})); // Run DAG - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // Primary should fail assert!(dag.get(&primary).is_err()); @@ -63,10 +60,7 @@ async fn test_retry_pattern_simulation() -> DagResult<()> { } })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Task completes with error on first "attempt" assert_eq!(dag.get(&task)?, Err("Simulated failure")); @@ -109,10 +103,7 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // First few tasks fail and open circuit for task in &tasks[0..3] { @@ -144,11 +135,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> { vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks })); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; let result = dag.get(&aggregator)?; assert_eq!(result.len(), 10); @@ -178,10 +165,7 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check accumulated errors let error_list = errors.lock().clone(); @@ -215,10 +199,7 @@ async fn test_graceful_degradation() -> DagResult<()> { })) .depends_on(&critical); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Critical path works assert_eq!(dag.get(final_task)?, 15); @@ -249,10 +230,7 @@ async fn test_error_boundary_isolation() -> DagResult<()> { .add_task(task_fn(|x: i32| async move { x + 5 })) .depends_on(b2); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Group A fails (returns Err) assert_eq!(dag.get(a2)?, Err("Group A error")); @@ -287,11 +265,7 @@ async fn test_compensation_action() -> DagResult<()> { } })); - let _ = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; // Action failed assert!(dag.get(&action).is_err()); diff --git a/tests/execution/basic.rs b/tests/execution/basic.rs index 565a803..f94185d 100644 --- a/tests/execution/basic.rs +++ b/tests/execution/basic.rs @@ -1,23 +1,24 @@ // Basic execution tests for DagRunner use crate::common::task_fn; +use futures::FutureExt; use dagx::*; #[tokio::test] async fn test_empty_dag() { let dag = DagRunner::new(); - dag.run() - .await - .unwrap(); // Should complete without error + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Should complete without error } #[tokio::test] async fn test_single_task() { let dag = DagRunner::new(); let task = dag.add_task(task_fn(|_: ()| async { 42 })); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(task).unwrap(), 42); } @@ -39,9 +40,9 @@ async fn test_deep_chain() { .add_task(task_fn(|x: i32| async move { x + 1 })) .depends_on(d); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: a, b, c, d are not sinks (they have dependents in the chain) // Only e is a sink and can be retrieved @@ -56,9 +57,9 @@ async fn test_wide_parallel() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i * 2); @@ -80,9 +81,9 @@ async fn test_diamond_dependency() { .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) .depends_on((&b, &c)); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Note: a, b, and c are not sinks (they have dependents) // Only d is a sink and can be retrieved @@ -108,9 +109,9 @@ async fn test_different_output_types() { } })); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(&dag.get(string_task).unwrap(), "hello"); assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]); @@ -141,9 +142,9 @@ async fn test_multiple_sinks() { .add_task(task_fn(|x: i32| async move { x * 3 })) .depends_on(branch2); - dag.run() - .await - .unwrap(); // Should wait for both sinks + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Should wait for both sinks assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1 assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3 @@ -163,9 +164,9 @@ async fn test_single_task_inline_path() { .add_task(task_fn(|x: i32| async move { x + 5 })) .depends_on(t2); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5 } @@ -188,9 +189,9 @@ async fn test_multi_task_spawn_path() { .add_task(task_fn(|x: i32| async move { x + 3 })) .depends_on(&source); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(t1).unwrap(), 101); assert_eq!(dag.get(t2).unwrap(), 102); @@ -212,9 +213,9 @@ async fn test_producer_consumer_channels() { )) .depends_on(&producer); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(consumer).unwrap(), 15); } @@ -237,9 +238,9 @@ async fn test_multi_consumer_fanout() { .add_task(task_fn(|x: i32| async move { x - 5 })) .depends_on(&producer); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(c1).unwrap(), 84); assert_eq!(dag.get(c2).unwrap(), 52); @@ -265,9 +266,9 @@ async fn test_compute_layers_with_dependents_tracking() { .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .depends_on((&left, &right)); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5 } @@ -313,9 +314,9 @@ async fn test_layer_execution_order() { })) .depends_on(t2); - dag.run() - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let order = execution_order.lock().clone(); assert_eq!(order, vec![1, 2, 3]); diff --git a/tests/execution/examples.rs b/tests/execution/examples.rs index 88e4f04..79b8abb 100644 --- a/tests/execution/examples.rs +++ b/tests/execution/examples.rs @@ -3,6 +3,7 @@ use crate::common::task_fn; use crate::common::tasks::*; use dagx::*; +use futures::FutureExt; #[tokio::test] async fn test_example_a_fan_out() { @@ -12,11 +13,9 @@ async fn test_example_a_fan_out() { let plus1 = dag.add_task(Inc::new()).depends_on(&base); // 11 let plus2 = dag.add_task(Inc::new()).depends_on(&base); // 11 - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); // SPEC API with spawner + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // SPEC API with spawner assert_eq!(dag.get(plus1).unwrap(), 11); assert_eq!(dag.get(plus2).unwrap(), 11); @@ -32,11 +31,9 @@ async fn test_example_b_fan_in() { let output = dag.add_task(OutputResult::new()).depends_on((&s, &n, &f)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(&dag.get(output).unwrap(), "hi:42:true"); } @@ -52,11 +49,9 @@ async fn test_example_c_many_to_many() { let prod_yz = dag.add_task(Mul::new()).depends_on((&y, &z)); // 15 let total = dag.add_task(Add::new()).depends_on((&sum_xy, &prod_yz)); // 20 - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(total).unwrap(), 20); } @@ -76,11 +71,9 @@ async fn test_single_input_forms() { .add_task(task_fn(|s: String| async move { s.to_uppercase() })) .depends_on((&s,)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(&dag.get(upper1).unwrap(), "HELLO"); assert_eq!(&dag.get(upper2).unwrap(), "HELLO"); diff --git a/tests/execution/integration.rs b/tests/execution/integration.rs index 7f539dc..1aa7955 100644 --- a/tests/execution/integration.rs +++ b/tests/execution/integration.rs @@ -1,7 +1,8 @@ //! Integration tests for end-to-end functionality -use dagx::{DagResult, DagRunner, Task}; use crate::common::task_fn; +use dagx::{DagResult, DagRunner, Task}; +use futures::FutureExt; use std::time::Instant; use tokio::time::{sleep, Duration}; @@ -18,7 +19,7 @@ impl LoadValue { #[dagx::task] impl LoadValue { async fn run(&self) -> i32 { - self.value // Read-only state: use &self + self.value // Read-only state: use &self } } @@ -33,7 +34,7 @@ impl Multiply { #[dagx::task] impl Multiply { async fn run(&self, input: &i32) -> i32 { - input * self.factor // Read-only state: use &self + input * self.factor // Read-only state: use &self } } @@ -41,7 +42,7 @@ struct Add; #[dagx::task] impl Add { async fn run(a: &i32, b: &i32) -> i32 { - a + b // Stateless: no self needed + a + b // Stateless: no self needed } } @@ -67,7 +68,7 @@ async fn integration_test_etl_pipeline() -> DagResult<()> { let final_result = dag.add_task(Add).depends_on((&combine12, &transform3)); // Execute pipeline - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify let result = dag.get(final_result)?; @@ -97,7 +98,7 @@ async fn integration_test_multi_layer_dag() -> DagResult<()> { // Layer 4: Final result let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc)); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(final_result)?; // (1+2)*2 + (2+3)*2 = 6 + 10 = 16 @@ -118,7 +119,7 @@ async fn integration_test_multiple_sinks() -> DagResult<()> { let branch2 = dag.add_task(Multiply::new(3)).depends_on(&source); let branch3 = dag.add_task(Multiply::new(4)).depends_on(&source); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // All branches should complete assert_eq!(dag.get(branch1)?, 20); @@ -144,7 +145,7 @@ async fn integration_test_diamond_cascade() -> DagResult<()> { let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1); let sink2 = dag.add_task(Add).depends_on((&left2, &right2)); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(sink2)?; // Diamond 1: (10*2) + (10*3) = 50 @@ -175,7 +176,7 @@ async fn integration_test_wide_and_deep() -> DagResult<()> { final_nodes.push(step3); } - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify each chain for (i, node) in final_nodes.iter().enumerate() { @@ -194,7 +195,7 @@ async fn integration_test_error_recovery() -> DagResult<()> { let source = dag.add_task(LoadValue::new(42)); let transform = dag.add_task(Multiply::new(2)).depends_on(&source); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Should be able to access results assert_eq!(dag.get(transform)?, 84); @@ -230,7 +231,7 @@ async fn integration_test_large_scale() -> DagResult<()> { }) .collect(); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify some results assert_eq!(dag.get(aggregations[0])?, 1); // First two @@ -254,7 +255,7 @@ async fn integration_test_reused_values() -> DagResult<()> { // Combine some of the results let combined = dag.add_task(Add).depends_on((&double, &triple)); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(double)?, 200); assert_eq!(dag.get(triple)?, 300); @@ -283,7 +284,7 @@ async fn integration_test_parallel_execution_timing() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let elapsed = start.elapsed(); // Verify all tasks completed @@ -322,23 +323,29 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> { // Layer 2: Sequential chain of 3 tasks depending on the first parallel task // (should add ~150ms) - let seq1 = dag.add_task(task_fn(move |x: i32| async move { - sleep(sleep_duration).await; - x + 1 - })).depends_on(¶llel_tasks[0]); + let seq1 = dag + .add_task(task_fn(move |x: i32| async move { + sleep(sleep_duration).await; + x + 1 + })) + .depends_on(¶llel_tasks[0]); - let seq2 = dag.add_task(task_fn(move |x: i32| async move { - sleep(sleep_duration).await; - x + 1 - })).depends_on(seq1); + let seq2 = dag + .add_task(task_fn(move |x: i32| async move { + sleep(sleep_duration).await; + x + 1 + })) + .depends_on(seq1); - let seq3 = dag.add_task(task_fn(move |x: i32| async move { - sleep(sleep_duration).await; - x + 1 - })).depends_on(seq2); + let seq3 = dag + .add_task(task_fn(move |x: i32| async move { + sleep(sleep_duration).await; + x + 1 + })) + .depends_on(seq2); let start = Instant::now(); - dag.run(|fut| { tokio::spawn(fut); }).await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let elapsed = start.elapsed(); // Verify results @@ -361,4 +368,4 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> { ); Ok(()) -} \ No newline at end of file +} diff --git a/tests/execution/results.rs b/tests/execution/results.rs index 269d877..e32f4b3 100644 --- a/tests/execution/results.rs +++ b/tests/execution/results.rs @@ -3,6 +3,7 @@ use crate::common::task_fn; use crate::common::tasks::Counter; use dagx::*; +use futures::FutureExt; #[tokio::test] async fn test_result_type() { @@ -13,11 +14,9 @@ async fn test_result_type() { Err::("error".to_string()) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(success_task).unwrap(), Ok(42)); assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string())); @@ -30,10 +29,7 @@ async fn test_dag_result_type_works() { // Test that DagResult<()> works with ? operator pattern async fn run_dag(dag: &DagRunner) -> DagResult<()> { - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; Ok(()) } @@ -68,7 +64,7 @@ async fn test_error_messages_are_helpful() { let err_msg = format!("{}", result.unwrap_err()); assert!(err_msg.contains("Result not found")); assert!(err_msg.contains("task #0")); // Task ID - assert!(err_msg.contains("Call dag.run()")); + assert!(err_msg.contains("Call dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))")); } #[tokio::test] @@ -77,11 +73,7 @@ async fn test_successful_execution_returns_ok() { let task = dag.add_task(task_fn(|_: ()| async { 42 })); // Should return Ok(()) - let result = dag - .run(|fut| { - tokio::spawn(fut); - }) - .await; + let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; assert!(result.is_ok()); // Should be able to get value @@ -95,11 +87,9 @@ async fn test_stateful_tasks() { let input = dag.add_task(task_fn(|_: ()| async { 5 })); let counter = dag.add_task(Counter::new(10)).depends_on(&input); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5 } @@ -117,11 +107,9 @@ async fn test_task_fn_with_captured_state() { .add_task(task_fn(move |x: i32| async move { x * multiplier })) .depends_on(&base); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(scaled).unwrap(), 35); } diff --git a/tests/parallelism/concurrent.rs b/tests/parallelism/concurrent.rs index c0055de..3119e4b 100644 --- a/tests/parallelism/concurrent.rs +++ b/tests/parallelism/concurrent.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::DagRunner; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -14,7 +15,12 @@ async fn test_concurrent_run_calls() { // Add some tasks let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| { + dag.add_task(task_fn(move |_: ()| async move { + tokio::time::sleep(Duration::from_millis(5)).await; + i + })) + }) .collect(); // Try to run the DAG multiple times concurrently @@ -22,12 +28,7 @@ async fn test_concurrent_run_calls() { for _ in 0..5 { let dag = Arc::clone(&dag); - handles.spawn(async move { - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - }); + handles.spawn(async move { dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await }); } // Only one run should succeed, others should fail with concurrent execution error @@ -62,11 +63,9 @@ async fn test_concurrent_execution_and_building() { .collect(); // Execute the initial DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Now try to add more tasks while also reading results let mut handles = JoinSet::new(); @@ -93,11 +92,9 @@ async fn test_concurrent_execution_and_building() { } // Execute again with the new tasks - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); } #[tokio::test] @@ -139,11 +136,9 @@ async fn test_dag_builder_thread_safety() { assert_eq!(counter.load(Ordering::SeqCst), 10); // Execute the combined DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Verify each pipeline computed correctly for task in final_tasks.iter() { @@ -163,11 +158,9 @@ async fn test_concurrent_get_operations() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Now access results concurrently let mut handles = JoinSet::new(); @@ -217,11 +210,9 @@ async fn test_no_race_in_dependency_tracking() { } // Execute - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // All dependents should have correct values for dep in dependents.iter() { @@ -277,11 +268,9 @@ async fn test_high_concurrency_stress() { assert_eq!(completed.load(Ordering::SeqCst), 1000); // The DAG should still be executable - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); } #[tokio::test] @@ -369,11 +358,9 @@ async fn test_atomic_dag_operations() { assert_eq!(deps.len(), 50); // Execute and verify - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); for dep in deps.iter() { let result = dag.get(dep).unwrap(); @@ -424,9 +411,7 @@ async fn test_no_deadlock_on_circular_waiting() { assert!(result.is_ok(), "Deadlock detected - operation timed out"); // Should be able to execute the DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); } diff --git a/tests/parallelism/execution.rs b/tests/parallelism/execution.rs index 8320939..cb63cb8 100644 --- a/tests/parallelism/execution.rs +++ b/tests/parallelism/execution.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -62,10 +63,7 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> { .depends_on((&layer1[0], &layer1[1], &layer1[2])) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) @@ -185,10 +183,7 @@ async fn test_completion_order_matches_dependencies() -> DagResult<()> { .depends_on(c) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2 @@ -271,10 +266,7 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> { .depends_on(c) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify results assert_eq!(dag.get(a)?, 1); @@ -344,10 +336,7 @@ async fn test_independent_tasks_dont_wait() -> DagResult<()> { })) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check results assert_eq!(dag.get(fast1)?, "fast1"); diff --git a/tests/parallelism/proof.rs b/tests/parallelism/proof.rs index b593239..98a56e3 100644 --- a/tests/parallelism/proof.rs +++ b/tests/parallelism/proof.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -24,10 +25,7 @@ async fn test_parallelism_proof_1000_tasks() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let elapsed = start.elapsed(); // Verify all tasks completed diff --git a/tests/parallelism/spawning.rs b/tests/parallelism/spawning.rs index 634ad45..0bebe59 100644 --- a/tests/parallelism/spawning.rs +++ b/tests/parallelism/spawning.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::sync::Mutex; @@ -31,7 +32,7 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { .collect(); // Custom spawner that counts invocations - dag.run().await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all tasks were spawned for (i, task) in tasks.iter().enumerate() { @@ -77,10 +78,7 @@ async fn test_tasks_not_running_inline() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify results for (i, task) in tasks.iter().enumerate() { diff --git a/tests/parallelism/threads.rs b/tests/parallelism/threads.rs index b81172a..0b7e5d1 100644 --- a/tests/parallelism/threads.rs +++ b/tests/parallelism/threads.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -40,10 +41,7 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all tasks completed for task in tasks.iter() { @@ -112,10 +110,7 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all tasks completed for (i, task) in tasks.iter().enumerate() { @@ -186,10 +181,7 @@ async fn test_diamond_parallel_execution() -> DagResult<()> { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((¶llel1, ¶llel2)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3) @@ -276,10 +268,7 @@ async fn test_parallel_branches_with_dependencies() -> DagResult<()> { .depends_on(b2_1) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify results assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1 @@ -371,10 +360,7 @@ async fn test_wide_fanout_parallel_execution() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify results for (i, task) in dependents.iter().enumerate() { @@ -445,10 +431,7 @@ async fn test_massive_parallel_fanout() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let elapsed = start.elapsed(); // All should complete diff --git a/tests/parallelism/timing.rs b/tests/parallelism/timing.rs index 52b48c3..600eba6 100644 --- a/tests/parallelism/timing.rs +++ b/tests/parallelism/timing.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner, Task}; +use futures::FutureExt; use std::sync::Arc; use std::sync::Mutex; use std::time::{Duration, Instant}; @@ -26,10 +27,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { .collect(); let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let elapsed = start.elapsed(); // Verify all tasks completed @@ -113,10 +111,7 @@ async fn test_sync_tasks_dont_block_async_runtime() -> DagResult<()> { })) }; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify results assert_eq!(dag.get(sync1)?, 20); diff --git a/tests/patterns/butterfly.rs b/tests/patterns/butterfly.rs index bec71f3..81aa07a 100644 --- a/tests/patterns/butterfly.rs +++ b/tests/patterns/butterfly.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -44,10 +45,7 @@ async fn test_butterfly_network() -> DagResult<()> { .depends_on((&layer1[3], &layer1[7])), ]; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify some outputs exist for task in &layer2 { @@ -100,10 +98,7 @@ async fn test_recursive_butterfly() -> DagResult<()> { let outputs = build_butterfly(&dag, inputs); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(outputs.len(), 4); for output in outputs { @@ -143,10 +138,7 @@ async fn test_benes_network() -> DagResult<()> { .depends_on((&stage1[1], &stage1[3])), ]; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; for output in outputs { let _ = dag.get(output)?; @@ -209,10 +201,7 @@ async fn test_shuffle_exchange_network() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(counter.load(Ordering::Relaxed), 8); diff --git a/tests/patterns/complex.rs b/tests/patterns/complex.rs index 58b92b2..abb6c86 100644 --- a/tests/patterns/complex.rs +++ b/tests/patterns/complex.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -59,10 +60,7 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> { })) .depends_on(combine); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(process)?, 84); let result = dag.get(final_result)?; @@ -140,10 +138,7 @@ async fn test_map_reduce_pattern() -> DagResult<()> { )) .depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(final_task)?; assert!(result > 0); @@ -200,10 +195,7 @@ async fn test_pipeline_with_feedback() -> DagResult<()> { })) .depends_on(stage3); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(stage1)?, 2); assert_eq!(dag.get(stage3)?, 12); @@ -247,10 +239,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> { )) .depends_on((&workers[0], &workers[1], &workers[2], &workers[3])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Sum of 10+20+30+40+50+60+70+80 = 360 assert_eq!(dag.get(gather)?, 360); @@ -339,10 +328,7 @@ async fn test_fork_join_with_barriers() -> DagResult<()> { })) .depends_on(barrier); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let _ = dag.get(join)?; assert_eq!(phase_counter.load(Ordering::SeqCst), 2); diff --git a/tests/patterns/grid.rs b/tests/patterns/grid.rs index 6c40f18..e15735d 100644 --- a/tests/patterns/grid.rs +++ b/tests/patterns/grid.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; #[tokio::test] async fn test_2d_grid_pattern() -> DagResult<()> { @@ -33,10 +34,7 @@ async fn test_2d_grid_pattern() -> DagResult<()> { } } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check corner values let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?; @@ -99,10 +97,7 @@ async fn test_3d_grid_pattern() -> DagResult<()> { } } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check that all cells have values for row in &grid { @@ -178,10 +173,7 @@ async fn test_hexagonal_grid() -> DagResult<()> { hex_grid.insert((q, r), task); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify center and some ring values assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100); @@ -230,10 +222,7 @@ async fn test_toroidal_grid() -> DagResult<()> { } } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify all torus tasks completed for task in torus_tasks { diff --git a/tests/patterns/tree.rs b/tests/patterns/tree.rs index dc72a28..448b004 100644 --- a/tests/patterns/tree.rs +++ b/tests/patterns/tree.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; #[tokio::test] async fn test_binary_tree_reduction() -> DagResult<()> { @@ -35,10 +36,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> { let root = current_level[0]; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255 assert_eq!(dag.get(root)?, 255); @@ -69,10 +67,7 @@ async fn test_n_ary_tree() -> DagResult<()> { let root = build_tree(&dag, 3, 1); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(root)?; assert!(result > 0); @@ -119,10 +114,7 @@ async fn test_unbalanced_tree() -> DagResult<()> { .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) .depends_on((&a, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(&f)?, 1); assert_eq!(dag.get(c)?, 2); @@ -170,10 +162,7 @@ async fn test_trie_like_structure() -> DagResult<()> { .add_task(task_fn(|v: String| async move { format!("{}/log", v) })) .depends_on(var); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(usr_bin)?, "root/usr/bin"); assert_eq!(dag.get(usr_lib)?, "root/usr/lib"); @@ -230,10 +219,7 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> { let root = create_balanced_tree(&dag, DEPTH, K, 0); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(root)?; assert!(result > 0); @@ -280,10 +266,7 @@ async fn test_merkle_tree_pattern() -> DagResult<()> { let root_hash = current_level[0]; - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; let result = dag.get(root_hash)?; assert!(result.starts_with("hash")); diff --git a/tests/performance/memory.rs b/tests/performance/memory.rs index 62850cd..6528461 100644 --- a/tests/performance/memory.rs +++ b/tests/performance/memory.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::DagRunner; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -23,11 +24,9 @@ async fn test_memory_usage_10000_nodes() { println!("Memory for 10k nodes: ~{} bytes", build_memory); // Execute - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let after_exec = memory_usage_hint(); let exec_memory = after_exec.saturating_sub(after_build); diff --git a/tests/performance/stress.rs b/tests/performance/stress.rs index 2e2579c..1fa63a1 100644 --- a/tests/performance/stress.rs +++ b/tests/performance/stress.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::DagRunner; +use futures::FutureExt; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -26,11 +27,9 @@ async fn test_deep_chain_100_levels() { // Execute let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = start.elapsed(); println!("Executed 100-level chain in {:?}", exec_time); @@ -89,11 +88,9 @@ async fn test_stress_mixed_patterns() { // Execute let start = Instant::now(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let exec_time = start.elapsed(); println!("Executed mixed pattern stress test in {:?}", exec_time); diff --git a/tests/runtimes/async_std.rs b/tests/runtimes/async_std.rs index edb55b7..9e45153 100644 --- a/tests/runtimes/async_std.rs +++ b/tests/runtimes/async_std.rs @@ -1,7 +1,7 @@ //! Runtime compatibility tests for async-std -use dagx::{DagRunner, Task}; use crate::common::task_fn; +use dagx::{DagRunner, Task}; struct Value(i32); #[dagx::task] @@ -27,7 +27,7 @@ async fn test_basic_dag_async_std() { let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); - dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); } @@ -40,7 +40,7 @@ async fn test_parallel_execution_async_std() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i * 2); @@ -58,7 +58,7 @@ async fn test_complex_dependencies_async_std() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(sum); - dag.run(|fut| { async_std::task::spawn(fut); }).await.unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); assert_eq!(dag.get(double).unwrap(), 60); -} \ No newline at end of file +} diff --git a/tests/runtimes/compatibility.rs b/tests/runtimes/compatibility.rs index b3ec541..f77e871 100644 --- a/tests/runtimes/compatibility.rs +++ b/tests/runtimes/compatibility.rs @@ -1,6 +1,7 @@ //! Runtime integration tests - verify dagx works correctly across different runtimes use dagx::{DagResult, DagRunner, Task}; +use futures::FutureExt; struct Value(i32); #[dagx::task] @@ -26,10 +27,7 @@ async fn runtime_integration_tokio() -> DagResult<()> { let b = dag.add_task(Value(20)); let sum = dag.add_task(Add).depends_on((&a, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(sum)?, 30); Ok(()) diff --git a/tests/runtimes/quirky.rs b/tests/runtimes/quirky.rs index eccc8f7..42ef655 100644 --- a/tests/runtimes/quirky.rs +++ b/tests/runtimes/quirky.rs @@ -15,6 +15,8 @@ use crate::common::task_fn; use dagx::DagRunner; +use futures::task::SpawnExt; +use futures_executor::ThreadPool; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -36,11 +38,7 @@ fn test_async_executor_basic() { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((&x, &y)); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(sum).unwrap(), 30); }; @@ -70,11 +68,7 @@ fn test_async_executor_complex_dag() { .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) .depends_on((&left, &right)); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25 }; @@ -104,11 +98,7 @@ fn test_async_executor_parallel_tasks() { }) .collect(); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); // All tasks should have run assert_eq!(counter.load(Ordering::SeqCst), 10); @@ -132,11 +122,7 @@ fn test_async_executor_with_local_spawn() { let x = dag.add_task(task_fn(|_: ()| async { 42 })); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(x).unwrap(), 42); }; @@ -163,11 +149,7 @@ fn test_async_executor_deep_chain() { .depends_on(current); } - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(current).unwrap(), 50); }; @@ -190,13 +172,9 @@ fn test_pollster_basic() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(&x); - dag.run(|fut| { - // pollster blocks, so we need to spawn on a different executor - // Use futures::executor for spawning - futures_executor::block_on(fut); - }) - .await - .unwrap(); + dag.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); assert_eq!(dag.get(doubled).unwrap(), 200); }); @@ -217,11 +195,9 @@ fn test_pollster_complex() { }) .collect(); - dag.run(|fut| { - futures_executor::block_on(fut); - }) - .await - .unwrap(); + dag.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), 7 * i as i32); @@ -237,11 +213,9 @@ fn test_pollster_error_handling() { // Task that returns Result let task = dag.add_task(task_fn(|_: ()| async { Ok::(42) })); - dag.run(|fut| { - futures_executor::block_on(fut); - }) - .await - .unwrap(); + dag.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); assert_eq!(dag.get(task).unwrap(), Ok(42)); }); @@ -262,13 +236,11 @@ fn test_futures_executor_basic() { .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) .depends_on((&x, &y)); - dag.run(|fut| { - // Use ThreadPool for spawning - let pool = futures_executor::ThreadPool::new().unwrap(); - pool.spawn_ok(fut); - }) - .await - .unwrap(); + let spawner = ThreadPool::new().unwrap(); + + dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) + .await + .unwrap(); assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25 }); @@ -276,8 +248,8 @@ fn test_futures_executor_basic() { #[test] fn test_futures_executor_threadpool() { - let pool = std::sync::Arc::new(futures_executor::ThreadPool::new().unwrap()); - let pool_clone = pool.clone(); + let pool = futures_executor::ThreadPool::new().unwrap(); + let spawner = pool.clone(); pool.spawn_ok(async move { let dag = DagRunner::new(); @@ -287,11 +259,9 @@ fn test_futures_executor_threadpool() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * i }))) .collect(); - dag.run(|fut| { - pool_clone.spawn_ok(fut); - }) - .await - .unwrap(); + dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) + .await + .unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), (i * i) as i32); @@ -325,12 +295,11 @@ fn test_futures_executor_diamond_pattern() { .add_task(task_fn(|(x, y): (i32, i32)| async move { x * y })) .depends_on((&b, &c)); - let pool = futures_executor::ThreadPool::new().unwrap(); - dag.run(|fut| { - pool.spawn_ok(fut); - }) - .await - .unwrap(); + let spawner = ThreadPool::new().unwrap(); + + dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) + .await + .unwrap(); assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6 }); @@ -350,7 +319,7 @@ fn test_all_runtimes_compatibility() { executor.run(async { let dag = DagRunner::new(); let x = dag.add_task(task_fn(|_: ()| async { 123 })); - dag.run(|fut| executor.spawn(fut).detach()).await.unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(x).unwrap(), 123); }); } @@ -360,7 +329,9 @@ fn test_all_runtimes_compatibility() { pollster::block_on(async { let dag = DagRunner::new(); let x = dag.add_task(task_fn(|_: ()| async { 123 })); - dag.run(futures_executor::block_on).await.unwrap(); + dag.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); assert_eq!(dag.get(x).unwrap(), 123); }); } @@ -370,8 +341,10 @@ fn test_all_runtimes_compatibility() { futures_executor::block_on(async { let dag = DagRunner::new(); let x = dag.add_task(task_fn(|_: ()| async { 123 })); - let pool = futures_executor::ThreadPool::new().unwrap(); - dag.run(|fut| pool.spawn_ok(fut)).await.unwrap(); + let spawner = ThreadPool::new().unwrap(); + dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) + .await + .unwrap(); std::thread::sleep(std::time::Duration::from_millis(10)); assert_eq!(dag.get(x).unwrap(), 123); }); @@ -393,11 +366,7 @@ fn test_runtime_edge_cases() { 42 })); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(x).unwrap(), 42); }); @@ -424,11 +393,7 @@ fn test_async_executor_stress() { }) .collect(); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i as i32); @@ -443,13 +408,9 @@ fn test_pollster_nested_blocks() { let dag1 = DagRunner::new(); let x = dag1.add_task(task_fn(|_: ()| async { 10 })); - dag1.run(|fut| { - pollster::block_on(async { - futures_executor::block_on(fut); - }); - }) - .await - .unwrap(); + dag1.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); assert_eq!(dag1.get(x).unwrap(), 10); }); @@ -459,19 +420,16 @@ fn test_pollster_nested_blocks() { fn test_futures_executor_multi_pool() { // Test with multiple thread pools let pool1 = futures_executor::ThreadPool::new().unwrap(); - let pool2 = futures_executor::ThreadPool::new().unwrap(); + let spawner = pool1.clone(); pool1.spawn_ok(async move { let dag = DagRunner::new(); let x = dag.add_task(task_fn(|_: ()| async { 50 })); - dag.run(|fut| { - // Spawn on different pool - pool2.spawn_ok(fut); - }) - .await - .unwrap(); + dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) + .await + .unwrap(); std::thread::sleep(std::time::Duration::from_millis(50)); assert_eq!(dag.get(x).unwrap(), 50); @@ -507,11 +465,7 @@ fn test_sync_tasks_on_async_executor() { let y = dag.add_task(task_fn(|_: ()| async { 7 })); let sum = dag.add_task(SyncAdd).depends_on((&x, &y)); - dag.run(|fut| { - executor.spawn(fut).detach(); - }) - .await - .unwrap(); + dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(sum).unwrap(), 12); }); @@ -536,11 +490,9 @@ fn test_sync_tasks_on_pollster() { let x = dag.add_task(task_fn(|_: ()| async { 21 })); let doubled = dag.add_task(SyncDouble).depends_on(&x); - dag.run(|fut| { - futures_executor::block_on(fut); - }) - .await - .unwrap(); + dag.run(|fut| async { pollster::block_on(fut) }) + .await + .unwrap(); assert_eq!(dag.get(doubled).unwrap(), 42); }); diff --git a/tests/runtimes/smol.rs b/tests/runtimes/smol.rs index e4e4c21..3f41694 100644 --- a/tests/runtimes/smol.rs +++ b/tests/runtimes/smol.rs @@ -1,7 +1,7 @@ //! Runtime compatibility tests for smol -use dagx::{DagRunner, Task}; use crate::common::task_fn; +use dagx::{DagRunner, Task}; struct Value(i32); #[dagx::task] @@ -28,7 +28,9 @@ fn test_basic_dag_smol() { let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); - dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); + dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); }); @@ -43,7 +45,9 @@ fn test_parallel_execution_smol() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); + dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i * 2); @@ -63,8 +67,10 @@ fn test_complex_dependencies_smol() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(sum); - dag.run(|fut| { smol::spawn(fut).detach(); }).await.unwrap(); + dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(double).unwrap(), 60); }); -} \ No newline at end of file +} diff --git a/tests/task_patterns/combined.rs b/tests/task_patterns/combined.rs index 2feb4a5..e7126f4 100644 --- a/tests/task_patterns/combined.rs +++ b/tests/task_patterns/combined.rs @@ -3,6 +3,7 @@ use crate::common::task_fn; // Comprehensive test showing all three patterns working together use dagx::*; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -52,11 +53,9 @@ async fn test_all_three_task_patterns() { // Pattern 3: Mutable accumulation let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26 - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 8); assert_eq!(dag.get(doubled).unwrap(), 16); @@ -97,11 +96,9 @@ async fn test_parallel_execution() { } })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // All three tasks should have run assert_eq!(counter.load(Ordering::SeqCst), 3); @@ -120,11 +117,9 @@ async fn test_task_fn_with_captured_state() { .add_task(task_fn(move |x: i32| async move { x * multiplier })) .depends_on(&base); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(scaled).unwrap(), 35); } diff --git a/tests/task_patterns/readonly.rs b/tests/task_patterns/readonly.rs index 56753b8..f2407b1 100644 --- a/tests/task_patterns/readonly.rs +++ b/tests/task_patterns/readonly.rs @@ -3,6 +3,7 @@ use crate::common::task_fn; // These tests verify the read-only state pattern using &self use dagx::*; +use futures::FutureExt; // Read-only state tasks struct ReadOnlyValue(i32); @@ -42,11 +43,9 @@ async fn test_readonly_state_basic() { let value = dag.add_task(ReadOnlyValue(42)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(value).unwrap(), 42); } @@ -58,11 +57,9 @@ async fn test_readonly_state_with_input() { let input = dag.add_task(task_fn(|_: ()| async { 5 })); let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(&input); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(result).unwrap(), 35); } @@ -87,11 +84,9 @@ async fn test_readonly_state_multiple_uses() { }; let result2 = dag.add_task(config2).depends_on(&input2); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5 assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5 } diff --git a/tests/task_patterns/stateless.rs b/tests/task_patterns/stateless.rs index 29a9c55..9b7fc84 100644 --- a/tests/task_patterns/stateless.rs +++ b/tests/task_patterns/stateless.rs @@ -1,5 +1,6 @@ // Stateless task pattern tests use crate::common::task_fn; +use futures::FutureExt; use dagx::*; @@ -85,11 +86,9 @@ async fn test_stateless_basic() { let y = dag.add_task(task_fn(|_: ()| async { 20 })); let sum = dag.add_task(StatelessAdd).depends_on((&x, &y)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 30); } @@ -101,11 +100,9 @@ async fn test_stateless_single_input() { let input = dag.add_task(task_fn(|_: ()| async { 21 })); let doubled = dag.add_task(StatelessDouble).depends_on(&input); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(doubled).unwrap(), 42); } @@ -116,11 +113,9 @@ async fn test_stateless_no_input() { let source = dag.add_task(StatelessSource); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(source).unwrap(), 42); } @@ -137,11 +132,9 @@ async fn test_stateless_multiple_inputs() { .add_task(StatelessFormat) .depends_on((&num, &text, &flag)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(result).unwrap(), "test: 42 (true)"); } @@ -178,11 +171,9 @@ async fn test_stateless_mixed_with_stateful() { // Stateful counter let counted = dag.add_task(Counter::new(10)).depends_on(product); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(product).unwrap(), 15); assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15 @@ -198,11 +189,9 @@ async fn test_stateless_chain() { let step2 = dag.add_task(StatelessInc).depends_on(step1); let step3 = dag.add_task(StatelessInc).depends_on(step2); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(step1).unwrap(), 1); assert_eq!(dag.get(step2).unwrap(), 2); @@ -221,11 +210,9 @@ async fn test_stateless_parallel() { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(tasks[0]).unwrap(), 1); assert_eq!(dag.get(tasks[1]).unwrap(), 4); @@ -244,11 +231,9 @@ async fn test_stateless_with_string_types() { let greeting = dag.add_task(StatelessConcat).depends_on((&hello, &world)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(greeting).unwrap(), "Hello, World!"); } diff --git a/tests/timing/concurrent_access.rs b/tests/timing/concurrent_access.rs index 4ff250f..641ea9e 100644 --- a/tests/timing/concurrent_access.rs +++ b/tests/timing/concurrent_access.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use tokio::sync::RwLock; @@ -23,10 +24,7 @@ async fn test_concurrent_result_retrieval() -> DagResult<()> { .collect(); // Execute the DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Now try to get results concurrently from multiple threads let mut get_handles = Vec::new(); @@ -77,10 +75,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> { tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; // Execute the DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Get the additional tasks let additional_tasks = build_handle.await.unwrap(); @@ -94,10 +89,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> { // (they need another run cycle) if building_complete.load(Ordering::SeqCst) { // Run again for the new tasks - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Now they should have results for (i, task) in additional_tasks.iter().enumerate() { @@ -141,10 +133,7 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check all completed assert_eq!(resolution_counter.load(Ordering::SeqCst), 101); @@ -198,10 +187,7 @@ async fn test_read_write_race_patterns() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Verify readers saw their writer's data for reader in &readers { @@ -257,10 +243,7 @@ async fn test_atomic_counter_races() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Final counter should be 200 * 100 = 20000 assert_eq!(counter.load(Ordering::SeqCst), 20000); @@ -296,10 +279,7 @@ async fn test_multiple_dag_runners_interaction() -> DagResult<()> { }) .collect(); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Collect results let mut results = Vec::new(); diff --git a/tests/timing/interleaving.rs b/tests/timing/interleaving.rs index bc74806..4cf8d50 100644 --- a/tests/timing/interleaving.rs +++ b/tests/timing/interleaving.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use tokio::sync::Barrier; @@ -66,10 +67,7 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> { )) .depends_on((&t1, &t2, &t3)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(collector)?, 6); @@ -168,10 +166,7 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { .depends_on(chain_b); } - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Check alternating pattern let event_list = events.lock().clone(); @@ -239,10 +234,7 @@ async fn test_layered_interleaving() -> DagResult<()> { })) .depends_on((&layer2[0], &layer2[1])); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; // Sum of 0+1+2+3 = 6 assert_eq!(dag.get(layer3)?, 6); @@ -335,10 +327,7 @@ async fn test_cross_branch_synchronization() -> DagResult<()> { .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) .depends_on((&a2, &b2)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await?; + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(merge)?, 13); // 2 + 11 diff --git a/tests/tracing/with_tracing.rs b/tests/tracing/with_tracing.rs index 3cb962e..5690824 100644 --- a/tests/tracing/with_tracing.rs +++ b/tests/tracing/with_tracing.rs @@ -44,11 +44,9 @@ async fn test_tracing_with_subscriber() { let b = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&a, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); } @@ -69,11 +67,9 @@ async fn test_tracing_with_complex_dag() { let right = dag.add_task(Multiply).depends_on((&source, &source)); let sink = dag.add_task(Add).depends_on((&left, &right)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // source = 10 // left = 10 + 10 = 20 @@ -97,11 +93,9 @@ async fn test_tracing_inline_execution() { let b = dag.add_task(Add).depends_on((&a, &a)); let c = dag.add_task(Multiply).depends_on((&b, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // a = 1 // b = 1 + 1 = 2 @@ -133,11 +127,9 @@ async fn test_tracing_multiple_layers() { // Layer 2 let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // v1 = 1, v2 = 2, v3 = 3 // sum12 = 1 + 2 = 3 diff --git a/tests/tracing/without_tracing.rs b/tests/tracing/without_tracing.rs index a6712f2..9a8bd8c 100644 --- a/tests/tracing/without_tracing.rs +++ b/tests/tracing/without_tracing.rs @@ -1,6 +1,7 @@ //! Tests that the library works correctly without tracing feature use dagx::{task, DagRunner, Task}; +use futures::FutureExt; struct Value(i32); @@ -28,11 +29,9 @@ async fn test_basic_dag_without_tracing() { let b = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&a, &b)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); } @@ -51,11 +50,9 @@ async fn test_complex_dag_without_tracing() { let sum34 = dag.add_task(Add).depends_on((&v3, &v4)); let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(final_sum).unwrap(), 10); } diff --git a/tests/type_safety/complex.rs b/tests/type_safety/complex.rs index 160a2b0..19bf16b 100644 --- a/tests/type_safety/complex.rs +++ b/tests/type_safety/complex.rs @@ -2,6 +2,7 @@ use crate::common::task_fn; use dagx::*; +use futures::FutureExt; #[tokio::test] async fn test_complex_type_safety() { @@ -21,11 +22,9 @@ async fn test_complex_type_safety() { } })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Correct type works let custom = dag.get(&custom_task).unwrap(); @@ -47,11 +46,9 @@ async fn test_generic_type_safety() { vec!["a".to_string(), "b".to_string()] })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Correct types work assert_eq!(dag.get(&vec_i32).unwrap(), vec![1, 2, 3]); @@ -69,11 +66,9 @@ async fn test_option_result_type_safety() { // Task returning Result let result_task = dag.add_task(task_fn(|_: ()| async { Ok::(42) })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Correct wrapped types work assert_eq!(dag.get(&opt_task).unwrap(), Some(42)); @@ -94,11 +89,9 @@ async fn test_zero_sized_type_safety() { let zst1_task = dag.add_task(task_fn(|_: ()| async { Zst1 })); let _zst2_task = dag.add_task(task_fn(|_: ()| async { Zst2 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Correct ZSTs work assert!(dag.get(&zst1_task).is_ok()); @@ -117,11 +110,9 @@ async fn test_type_safety_with_many_types() { let string_task = dag.add_task(task_fn(|_: ()| async { "42".to_string() })); let bool_task = dag.add_task(task_fn(|_: ()| async { true })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Each type works correctly assert_eq!(dag.get(&i32_task).unwrap(), 42i32); @@ -145,11 +136,9 @@ async fn test_type_safety_single_dependency() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(transform).unwrap(), 84); } @@ -169,11 +158,9 @@ async fn test_type_safety_tuple_dependencies() { })) .depends_on((&a, &b, &c)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(combine).unwrap(), "hello: 10 (true)"); } @@ -186,11 +173,9 @@ async fn test_type_safety_different_output_types() { let str_source = dag.add_task(task_fn(|_: ()| async { "test".to_string() })); let vec_source = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(&int_source).unwrap(), 42); assert_eq!(dag.get(&str_source).unwrap(), "test"); @@ -214,11 +199,9 @@ async fn test_type_safety_complex_types() { vec![Some(1), None, Some(2), Some(3)] })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); let tuple_result = dag.get(tuple_task).unwrap(); assert_eq!(tuple_result.0, 42); @@ -245,11 +228,9 @@ async fn test_type_safety_with_generics() { )) .depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(transform).unwrap(), Some(84)); @@ -261,11 +242,9 @@ async fn test_type_safety_with_generics() { )) .depends_on(&source2); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(transform2).unwrap(), None); } @@ -285,11 +264,9 @@ async fn test_large_tuple_input() { )) .depends_on((&a, &b, &c, &d, &e)); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(sum).unwrap(), 15); } diff --git a/tests/type_safety/handles.rs b/tests/type_safety/handles.rs index 011ccdc..9372107 100644 --- a/tests/type_safety/handles.rs +++ b/tests/type_safety/handles.rs @@ -1,5 +1,6 @@ // Type safety tests for TaskHandle use crate::common::task_fn; +use futures::FutureExt; // These tests verify dagx's compile-time type safety guarantees. // Tests cover both runtime behavior (wrong types return None) and // compile-time checking (type mismatches fail to compile). @@ -15,11 +16,9 @@ async fn test_get_with_wrong_type_returns_none() { let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); // Run DAG - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Verify correct type works assert_eq!(dag.get(&int_task), Ok(42i32)); @@ -38,11 +37,9 @@ async fn test_handle_preserves_type() { let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); // Get with correct types assert_eq!(dag.get(&string_task).unwrap(), "hello".to_string()); @@ -66,11 +63,9 @@ async fn test_type_safety_handle_reuse() { .add_task(task_fn(|x: i32| async move { x * 3 })) .depends_on(&source); - dag.run(|fut| { - tokio::spawn(fut); - }) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); assert_eq!(dag.get(double).unwrap(), 200); assert_eq!(dag.get(triple).unwrap(), 300); From ee3c9f4417c87efb66f9f378bd0261613a42b8a6 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Wed, 21 Jan 2026 23:48:12 +0100 Subject: [PATCH 6/7] Update non-default feature code --- Cargo.toml | 2 +- src/runner.rs | 2 +- tests/runtimes/async_std.rs | 6 +++--- tests/runtimes/mod.rs | 3 +++ tests/runtimes/smol.rs | 12 +++--------- tests/tracing/with_tracing.rs | 1 + 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ec4078..1466677 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ ] } [dependencies] -futures = "0.3" +futures = { version = "0.3", default-features = false, features = ["std"] } parking_lot = "0.12" dagx-macros = { version = "0.3.1", path = "./dagx-macros" } tracing = { version = "0.1", optional = true } diff --git a/src/runner.rs b/src/runner.rs index c3b8d6b..eb3aa7e 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -284,7 +284,7 @@ impl DagRunner { /// # }; /// ``` #[inline] - #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))] + #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))] pub async fn run(&self, spawner: S) -> DagResult<()> where S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc)>>) -> F, diff --git a/tests/runtimes/async_std.rs b/tests/runtimes/async_std.rs index 9e45153..fac48dc 100644 --- a/tests/runtimes/async_std.rs +++ b/tests/runtimes/async_std.rs @@ -27,7 +27,7 @@ async fn test_basic_dag_async_std() { let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); + dag.run(|fut| async_std::task::spawn(fut)).await.unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); } @@ -40,7 +40,7 @@ async fn test_parallel_execution_async_std() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); + dag.run(|fut| async_std::task::spawn(fut)).await.unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i * 2); @@ -58,7 +58,7 @@ async fn test_complex_dependencies_async_std() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(sum); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap(); + dag.run(|fut| async_std::task::spawn(fut)).await.unwrap(); assert_eq!(dag.get(double).unwrap(), 60); } diff --git a/tests/runtimes/mod.rs b/tests/runtimes/mod.rs index 01c39a8..25bcf9e 100644 --- a/tests/runtimes/mod.rs +++ b/tests/runtimes/mod.rs @@ -2,3 +2,6 @@ pub mod compatibility; pub mod quirky; + +mod async_std; +mod smol; diff --git a/tests/runtimes/smol.rs b/tests/runtimes/smol.rs index 3f41694..d9ad4d1 100644 --- a/tests/runtimes/smol.rs +++ b/tests/runtimes/smol.rs @@ -28,9 +28,7 @@ fn test_basic_dag_smol() { let y = dag.add_task(Value(3)); let sum = dag.add_task(Add).depends_on((&x, &y)); - dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); + dag.run(|fut| smol::spawn(fut)).await.unwrap(); assert_eq!(dag.get(sum).unwrap(), 5); }); @@ -45,9 +43,7 @@ fn test_parallel_execution_smol() { .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) .collect(); - dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); + dag.run(|fut| smol::spawn(fut)).await.unwrap(); for (i, task) in tasks.iter().enumerate() { assert_eq!(dag.get(task).unwrap(), i * 2); @@ -67,9 +63,7 @@ fn test_complex_dependencies_smol() { .add_task(task_fn(|x: i32| async move { x * 2 })) .depends_on(sum); - dag.run(|fut| smol::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); + dag.run(|fut| smol::spawn(fut)).await.unwrap(); assert_eq!(dag.get(double).unwrap(), 60); }); diff --git a/tests/tracing/with_tracing.rs b/tests/tracing/with_tracing.rs index 5690824..69d59cb 100644 --- a/tests/tracing/with_tracing.rs +++ b/tests/tracing/with_tracing.rs @@ -1,6 +1,7 @@ //! Tests with tracing feature enabled use dagx::{task, DagRunner, Task}; +use futures::FutureExt; use tracing_subscriber::{fmt, EnvFilter}; struct Value(i32); From a8e39804d2b95543091db8c832cdc08006a32027 Mon Sep 17 00:00:00 2001 From: TechnoPorg Date: Thu, 22 Jan 2026 00:23:28 +0100 Subject: [PATCH 7/7] Test single-threaded runtime --- src/runner.rs | 2 +- tests/boundaries/large_scale.rs | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/runner.rs b/src/runner.rs index eb3aa7e..20e26a7 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -235,7 +235,7 @@ impl DagRunner { /// # Parameters /// /// - `spawner`: A function that spawns futures on the async runtime - /// and returns a task handle. Examples: + /// and returns a handle to the task. This is the only way to run tasks on separate threads. Examples: /// - Tokio: `|fut| { tokio::spawn(fut).map(Result::unwrap) }` /// - Smol: `|fut| { smol::spawn(fut) }` /// - Async-std: `|fut| { async_std::task::spawn(fut) }` diff --git a/tests/boundaries/large_scale.rs b/tests/boundaries/large_scale.rs index 520974d..36c5e01 100644 --- a/tests/boundaries/large_scale.rs +++ b/tests/boundaries/large_scale.rs @@ -110,6 +110,77 @@ async fn test_5000_node_pyramid() -> DagResult<()> { Ok(()) } +#[tokio::test] +async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> { + // Create a pyramid structure with ~5000 nodes + // Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1 + let dag = DagRunner::new(); + + // Bottom layer: 2048 nodes - convert to TaskHandles + let mut current_layer: Vec> = (0..2048) + .map(|i| { + let task = dag.add_task(task_fn(move |_: ()| async move { + if i < 1024 { + 1 + } else { + 0 + } + })); + (&task).into() + }) + .collect(); + + let layer_sizes = vec![512, 128, 32, 8, 2]; + + for layer_size in layer_sizes { + let mut next_layer = Vec::with_capacity(layer_size); + let group_size = current_layer.len() / layer_size; + + for i in 0..layer_size { + let start = i * group_size; + let deps: Vec<_> = current_layer[start..start + group_size.min(4)] + .iter() + .take(4) + .collect(); + + let task = match deps.len() { + 1 => dag + .add_task(task_fn(|a: i32| async move { a })) + .depends_on(deps[0]), + 2 => dag + .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .depends_on((deps[0], deps[1])), + 3 => dag + .add_task(task_fn( + |(a, b, c): (i32, i32, i32)| async move { a + b + c }, + )) + .depends_on((deps[0], deps[1], deps[2])), + 4 => dag + .add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move { + a + b + c + d + })) + .depends_on((deps[0], deps[1], deps[2], deps[3])), + _ => unreachable!(), + }; + next_layer.push(task); + } + current_layer = next_layer; + } + + // Final layer combines the last 2 + let root = dag + .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .depends_on((¤t_layer[0], ¤t_layer[1])); + + dag.run(|fut| fut).await?; + + // We created 1024 ones and 1024 zeros at the bottom + // The sum should propagate up to 1024 + let result = dag.get(root)?; + assert!(result > 0, "Expected positive result, got {}", result); + Ok(()) +} + #[tokio::test] async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> { // Test a very wide DAG with 1000 sources and 1000 sinks