Merge pull request #1 from TechnoPorg/push-mmkvnrvwpuzm

Replace channel mechanism of dependency passing with a shared registry
This commit is contained in:
swaits authored and GitHub committed 2026-01-27 23:23:53 -07:00
commit 8f6472ea22
96 files changed
+1278 -2246

No files matched your search

+11 -3
View File
@@ -22,16 +22,24 @@ 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"
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 }
[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"
+10 -6
View File
@@ -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(|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(|fut| { tokio::spawn(fut); }).await.unwrap();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
// Retrieve results
assert_eq!(dag.get(sum).unwrap(), 5);
@@ -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(|fut| { tokio::spawn(fut); }).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(|fut| { async_std::task::spawn(fut); }).await.unwrap();
dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
// With smol
dag.run(|fut| { smol::spawn(fut).detach(); }).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
+10 -15
View File
@@ -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();
})
});
});
+4 -5
View File
@@ -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();
})
});
});
+4 -5
View File
@@ -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()
})
});
+4 -5
View File
@@ -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()
})
});
+4 -5
View File
@@ -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()
})
});
+4 -5
View File
@@ -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();
})
});
});
+4 -5
View File
@@ -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()
})
});
+4 -5
View File
@@ -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();
})
});
});
+7 -10
View File
@@ -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();
})
});
});
+4 -5
View File
@@ -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();
})
});
});
+10 -15
View File
@@ -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();
})
});
});
+4 -5
View File
@@ -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();
})
});
},
+2 -3
View File
@@ -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();
})
+4 -5
View File
@@ -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();
})
});
},
+4 -5
View File
@@ -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();
})
});
});
+18 -37
View File
@@ -335,7 +335,7 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
quote! {
fn extract_and_run(
self,
_receivers: Vec<Box<dyn std::any::Any + Send>>,
_dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + 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<Box<dyn std::any::Any + Send>>,
mut dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + 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::<oneshot::Receiver<Arc<#param_type>>>()
.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<Box<dyn std::any::Any + Send>>,
dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + 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()
.ok_or_else(|| format!("Missing receiver at index {}", #indices))?
.downcast::<oneshot::Receiver<Arc<#param_types>>>()
// Extract each parameter
let arc_results = (#(
iter.next()
.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>()))?;
)*
// 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)
+2 -2
View File
@@ -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(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
```
**dagrs** (Complex: traits + channels + IDs + manual wiring):
@@ -227,7 +227,7 @@ impl Task<String, Bytes> 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)
+1 -1
View File
@@ -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(|fut| tokio::spawn(fut).map(Result::unwrap))
```
**dagx prevents this at compile time:**
+23 -26
View File
@@ -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::<FuturesUnordered<_>>(); // 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.
+4 -5
View File
@@ -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
//
+4 -5
View File
@@ -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:");
+4 -5
View File
@@ -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());
+2 -4
View File
@@ -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();
+22 -35
View File
@@ -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());
}
+2 -4
View File
@@ -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)?);
+2 -4
View File
@@ -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)?);
+4 -5
View File
@@ -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());
+4 -5
View File
@@ -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());
+7 -10
View File
@@ -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() {
+13 -20
View File
@@ -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);
+16 -25
View File
@@ -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
+4 -5
View File
@@ -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
+16 -25
View File
@@ -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());
}
+15 -24
View File
@@ -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
{
+10 -15
View File
@@ -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");
+4 -2
View File
@@ -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<i32>
///
/// dag.run(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).await.unwrap();
/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// # };
/// ```
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
+1 -1
View File
@@ -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(|fut| tokio::spawn(fut).map(Result::unwrap)).",
task_id
)
}
+1 -1
View File
@@ -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]
+57 -89
View File
@@ -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
//!
@@ -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,9 +35,7 @@
use std::any::Any;
use std::sync::Arc;
use futures::channel::oneshot;
/// 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.
@@ -47,35 +45,34 @@ 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<Box<dyn Any + Send>>,
fn extract_from_deps(
deps: Vec<Arc<dyn Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self, String>> + Send;
}
// Unit type - no dependencies
impl ExtractInput for () {
async fn extract_from_channels(_receivers: Vec<Box<dyn Any + Send>>) -> Result<Self, String> {
async fn extract_from_deps(_deps: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
Ok(())
}
}
// Implementations for common primitive and standard library types.
// These allow tasks to receive these types as single inputs without wrapping in tuples.
// Receives Arc<T> from channel, clones the inner value for task consumption.
// Receives Arc<T> 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),+) => {
$(
impl ExtractInput for $t {
async fn extract_from_channels(mut receivers: Vec<Box<dyn Any + Send>>) -> Result<Self, String> {
if receivers.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", receivers.len()));
async fn extract_from_deps(mut dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
if dependencies.len() != 1 {
return Err(format!("Expected 1 dependency, got {}", dependencies.len()));
}
let rx = *receivers.pop()
let arc_value = dependencies.pop()
.unwrap()
.downcast::<oneshot::Receiver<Arc<$t>>>()
.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<Box<dyn Any + Send>>,
async fn extract_from_deps(
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> {
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::<oneshot::Receiver<Arc<Result<T, E>>>>()
.downcast::<Result<T, E>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Result<{}, {}>>",
@@ -110,9 +107,6 @@ where
std::any::type_name::<E>()
)
})?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone())
}
}
@@ -122,25 +116,22 @@ impl<T> ExtractInput for Option<T>
where
T: 'static + Clone + Send + Sync,
{
async fn extract_from_channels(
mut receivers: Vec<Box<dyn Any + Send>>,
async fn extract_from_deps(
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> {
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::<oneshot::Receiver<Arc<Option<T>>>>()
.downcast::<Option<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Option<{}>>",
std::any::type_name::<T>()
)
})?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone())
}
}
@@ -150,25 +141,22 @@ impl<T> ExtractInput for Vec<T>
where
T: 'static + Clone + Send + Sync,
{
async fn extract_from_channels(
mut receivers: Vec<Box<dyn Any + Send>>,
async fn extract_from_deps(
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> {
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::<oneshot::Receiver<Arc<Vec<T>>>>()
.downcast::<Vec<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Vec<{}>>",
std::any::type_name::<T>()
)
})?;
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<Box<dyn Any + Send>>,
async fn extract_from_deps(
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> {
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::<oneshot::Receiver<Arc<std::collections::HashMap<K, V>>>>()
.downcast::<std::collections::HashMap<K, V>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<HashMap<{}, {}>>",
@@ -196,9 +184,6 @@ where
std::any::type_name::<V>()
)
})?;
let arc_value = rx
.await
.map_err(|_| "Channel closed before receiving value".to_string())?;
Ok((*arc_value).clone())
}
}
@@ -209,25 +194,22 @@ impl<T> ExtractInput for Arc<T>
where
T: 'static + Clone + Send + Sync,
{
async fn extract_from_channels(
mut receivers: Vec<Box<dyn Any + Send>>,
async fn extract_from_deps(
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> Result<Self, String> {
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::<oneshot::Receiver<Arc<Arc<T>>>>()
.downcast::<Arc<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Arc<{}>>",
std::any::type_name::<T>()
)
})?;
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))
}
@@ -235,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.
///
@@ -251,43 +230,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<Box<dyn Any + Send>>) -> Result<Self, String> {
// Verify we have the correct number of receivers
async fn extract_from_deps(dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> {
// 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
// Dereference Box after downcasting to get the receiver itself
// dependencies now contain Arc-wrapped values for efficient fanout
// Dereference Box after downcasting to get the parameter 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::<oneshot::Receiver<Arc<$T>>>()
.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 parameter 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()
,)+))
}
}
+39 -127
View File
@@ -2,10 +2,9 @@
//!
//! 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;
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<T,E> type mismatch error message
// Create a channel that sends the WRONG type (String instead of Result<i32, String>)
let (tx, rx) = oneshot::channel::<Arc<String>>();
// 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<i32, String>)
// Try to extract as Result<i32, String> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new("hello") as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::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<T> type mismatch error message
// Create a channel that sends the WRONG type (i32 instead of Option<i32>)
let (tx, rx) = oneshot::channel::<Arc<i32>>();
// 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<i32>)
// Try to extract as Option<i32> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(42) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::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<T> type mismatch error message
// Create a channel that sends the WRONG type (String instead of Vec<u8>)
let (tx, rx) = oneshot::channel::<Arc<String>>();
// 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<u8>)
// Try to extract as Vec<u8> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<u8>::extract_from_channels(deps).await;
let deps = vec![Arc::new("not a vec".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Vec::<u8>::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<K,V> type mismatch error message
// Create a channel that sends the WRONG type (Vec<i32> instead of HashMap)
let (tx, rx) = oneshot::channel::<Arc<Vec<i32>>>();
// 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<i32> instead of HashMap)
// Try to extract as HashMap<String, i32> - this will fail with type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, i32>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
@@ -142,18 +113,12 @@ async fn test_hashmap_type_mismatch_coverage() {
async fn test_arc_type_mismatch_coverage() {
// Tests line 211 in extract.rs - Arc<T> type mismatch error message
// Arc is special: it expects Arc<Arc<T>> from channel
// Create a channel that sends Arc<String> instead of Arc<Arc<String>>
let (tx, rx) = oneshot::channel::<Arc<String>>();
// Send an Arc<String> value (missing one level of Arc wrapping)
tokio::spawn(async move {
let _ = tx.send(Arc::new("not double arc".to_string()));
});
// Create a dependency that provides Arc<String> instead of Arc<Arc<String>>
let deps = vec![Arc::new("not double arc".to_string()) as Arc<dyn Any + Send + Sync>];
// Try to extract as Arc<String> - this will fail with type mismatch
// because it expects Arc<Arc<String>> in the channel
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<String>::extract_from_channels(deps).await;
let result = Arc::<String>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
@@ -177,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::<Arc<Result<String, String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Ok("test".to_string())));
});
// Try to extract as Result<i32, String> - wrong Ok type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Ok::<_, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -199,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::<Arc<Result<i32, i32>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Err(42)));
});
// Try to extract as Result<i32, String> - wrong Err type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Err::<i32, i32>(42)) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -221,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::<Arc<Option<String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Some("test".to_string())));
});
// Try to extract as Option<i32> - wrong inner type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Some("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -243,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::<Arc<Vec<String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(vec!["a".to_string(), "b".to_string()]));
});
// Try to extract as Vec<i32> - wrong element type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(vec!["a".to_string(), "b".to_string()]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -265,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::<Arc<HashMap<i32, String>>>();
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<String, String> - wrong key type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -289,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::<Arc<HashMap<String, i32>>>();
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<String, String> - wrong value type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -314,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<i32> but expect Arc<String>
let (tx, rx) = oneshot::channel::<Arc<Arc<i32>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Arc::new(42)));
});
// Try to extract as Arc<String> - wrong inner type
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Arc::new(Arc::new(42))) as Arc<dyn Any + Send + Sync>];
let result = Arc::<String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -336,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::<Arc<String>>();
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<dyn Any + Send>];
let result = i32::extract_from_channels(deps).await;
let deps = vec![Arc::new("not an i32".to_string()) as Arc<dyn Any + Send + Sync>];
let result = i32::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
@@ -358,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<String, String>, expect Result<Option<i32>, String>
let (tx, rx) = oneshot::channel::<Arc<Result<String, String>>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new(Ok("test".to_string())));
});
// Try to extract as Result<Option<i32>, String> - nested type mismatch
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<Option<i32>, String>::extract_from_channels(deps).await;
let deps =
vec![Arc::new(Ok::<String, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<Option<i32>, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
+61 -174
View File
@@ -4,7 +4,6 @@
use crate::extract::ExtractInput;
use crate::task::Task;
use futures::channel::oneshot;
use std::any::Any;
use std::sync::Arc;
@@ -39,11 +38,11 @@ impl TestTaskWithInput {
async fn test_extract_result_type_single_dependency_error() {
// Test Result<T,E> with wrong dependency count
let deps = vec![
Box::new(oneshot::channel::<Result<i32, String>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Result<i32, String>>().1) as Box<dyn Any + Send>,
Arc::new(Ok::<i32, String>(5)) as Arc<dyn Any + Send + Sync>,
Arc::new(Err::<i32, String>("test".to_string())) as Arc<dyn Any + Send + Sync>,
]; // Two dependencies when expecting 1
let result = Result::<i32, String>::extract_from_channels(deps).await;
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
}
@@ -51,15 +50,9 @@ async fn test_extract_result_type_single_dependency_error() {
#[tokio::test]
async fn test_extract_result_type_success() {
// Test Result<T,E> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Result<i32, String>>>();
// Send value in background
tokio::spawn(async move {
let _ = tx.send(Arc::new(Ok(42)));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Ok::<i32, String>(42)) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), Ok(42));
}
@@ -68,12 +61,12 @@ async fn test_extract_result_type_success() {
async fn test_extract_option_type_single_dependency_error() {
// Test Option<T> with wrong dependency count
let deps = vec![
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Option<String>>().1) as Box<dyn Any + Send>,
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
]; // Three dependencies when expecting 1
let result = Option::<String>::extract_from_channels(deps).await;
let result = Option::<String>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 3"));
}
@@ -81,15 +74,8 @@ async fn test_extract_option_type_single_dependency_error() {
#[tokio::test]
async fn test_extract_option_type_success() {
// Test Option<T> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Option<i32>>>();
// Send value in background
tokio::spawn(async move {
let _ = tx.send(Arc::new(Some(99)));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(Some(99)) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(99));
}
@@ -99,7 +85,7 @@ async fn test_extract_vec_type_single_dependency_error() {
// Test Vec<T> with wrong dependency count
let deps = vec![]; // Zero dependencies when expecting 1
let result = Vec::<u8>::extract_from_channels(deps).await;
let result = Vec::<u8>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
}
@@ -107,15 +93,8 @@ async fn test_extract_vec_type_single_dependency_error() {
#[tokio::test]
async fn test_extract_vec_type_success() {
// Test Vec<T> successful extraction
let (tx, rx) = oneshot::channel::<Arc<Vec<i32>>>();
// 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<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![1, 2, 3]);
}
@@ -124,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::<i32>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
]; // 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::<String>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<String>().1) as Box<dyn Any + Send>,
Arc::new(String::default()) as Arc<dyn Any + Send + Sync>,
Arc::new(String::default()) as Arc<dyn Any + Send + Sync>,
];
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::<f64>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<f64>().1) as Box<dyn Any + Send>,
Arc::new(0.0) as Arc<dyn Any + Send + Sync>,
Arc::new(0.0) as Arc<dyn Any + Send + Sync>,
];
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"));
}
@@ -155,67 +134,38 @@ 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::<i32>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<i32>().1) as Box<dyn Any + Send>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
];
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::<Arc<i32>>();
let (tx2, rx2) = oneshot::channel::<Arc<i32>>();
// 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<dyn Any + Send>,
Box::new(rx2) as Box<dyn Any + Send>,
Arc::new(42) as Arc<dyn Any + Send + Sync>,
Arc::new(100) as Arc<dyn Any + Send + Sync>,
];
// 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::<Arc<i32>>();
// Explicitly drop tx to close the channel
drop(tx);
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
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
let (tx, rx) = oneshot::channel::<String>();
// 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<dyn Any + Send>];
let result = i32::extract_from_channels(deps).await;
let deps = vec![Arc::new("hello".to_string()) as Arc<dyn Any + Send + Sync>];
let result = i32::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
@@ -225,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::<Arc<$t>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new($val));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = <$t>::extract_from_channels(deps).await;
let deps = vec![Arc::new($val) as Arc<dyn Any + Send + Sync>];
let result = <$t>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), $val);
};
@@ -248,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::<Arc<i32>>();
let (tx2, rx2) = oneshot::channel::<Arc<i32>>();
let (tx3, rx3) = oneshot::channel::<Arc<i32>>();
let (tx4, rx4) = oneshot::channel::<Arc<i32>>();
let (tx5, rx5) = oneshot::channel::<Arc<i32>>();
// 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<dyn Any + Send>,
Box::new(rx2) as Box<dyn Any + Send>,
Box::new(rx3) as Box<dyn Any + Send>,
Box::new(rx4) as Box<dyn Any + Send>,
Box::new(rx5) as Box<dyn Any + Send>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(1) as Arc<dyn Any + Send + Sync>,
Arc::new(2) as Arc<dyn Any + Send + Sync>,
Arc::new(3) as Arc<dyn Any + Send + Sync>,
Arc::new(4) as Arc<dyn Any + Send + Sync>,
];
// 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));
}
@@ -280,17 +209,11 @@ async fn test_extract_larger_tuples() {
#[tokio::test]
async fn test_extract_arc_type() {
// Test Arc<T> extraction
let (tx, rx) = oneshot::channel::<Arc<Arc<String>>>();
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<dyn Any + Send>];
let result = Arc::<String>::extract_from_channels(deps).await;
let deps = vec![Arc::new(test_arc) as Arc<dyn Any + Send + Sync>];
let result = Arc::<String>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(*result.unwrap(), *test_arc_clone);
}
@@ -301,74 +224,43 @@ use std::collections::HashMap;
#[tokio::test]
async fn test_hashmap_extract_wrong_dependency_count() {
let deps = vec![
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>,
Arc::new(HashMap::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
Arc::new(HashMap::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
let result = HashMap::<String, i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
}
#[tokio::test]
async fn test_hashmap_extract_type_mismatch() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong type".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new("wrong type".to_string()) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_hashmap_extract_channel_closed() {
let (tx, rx) = oneshot::channel::<Arc<HashMap<String, i32>>>();
drop(tx);
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::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 (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::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::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::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::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
@@ -376,20 +268,15 @@ async fn test_vec_extract_downcast_failure() {
#[tokio::test]
async fn test_arc_extract_wrong_dependency_count() {
let deps = vec![];
let result = Arc::<i32>::extract_from_channels(deps).await;
let result = Arc::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
}
#[tokio::test]
async fn test_arc_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<i32>::extract_from_channels(deps).await;
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Arc::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
@@ -397,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::<Arc<i32>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
];
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()
+18 -11
View File
@@ -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(|fut| { tokio::spawn(fut); }).await.unwrap();
//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! assert_eq!(dag.get(sum).unwrap(), 5);
//! # };
//! ```
@@ -64,7 +65,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
@@ -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(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).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(|fut| { tokio::spawn(fut); }).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<String>, 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(|fut| { tokio::spawn(fut); }).await.unwrap();
//! dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
//! # };
//! ```
//!
+10 -54
View File
@@ -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<Box<dyn Any + Send>>;
/// Type-erased channel pair (senders, receivers)
type ChannelPair = (ErasedChannels, ErasedChannels);
/// Type-erased async execution result (Arc-wrapped for efficient fanout)
type ExecuteFuture =
Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DagError>> + Send>>;
@@ -33,26 +25,18 @@ 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 {
/// 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.
/// 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.
fn execute_with_channels(
fn execute_with_deps(
self: Box<Self>,
receivers: ErasedChannels,
senders: ErasedChannels,
dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> ExecuteFuture;
}
@@ -66,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<T: Task> {
pub(crate) id: NodeId,
pub(crate) task: T,
@@ -86,33 +67,19 @@ 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::<Arc<T::Output>>();
senders.push(Box::new(tx) as Box<dyn Any + Send>);
receivers.push(Box::new(rx) as Box<dyn Any + Send>);
}
(senders, receivers)
}
fn execute_with_channels(
fn execute_with_deps(
self: Box<Self>,
receivers: ErasedChannels,
senders: ErasedChannels,
dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> 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
// 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 });
}
};
@@ -124,17 +91,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<T> knows T::Output, so downcast is safe
for sender_any in senders {
if let Ok(sender) = sender_any.downcast::<oneshot::Sender<Arc<T::Output>>>() {
// 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<dyn Any + Send + Sync>)
})
+20 -92
View File
@@ -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 {
@@ -67,49 +46,38 @@ 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::<std::sync::Arc<i32>>();
// Execute dependency node with sender
let dep_senders = vec![Box::new(tx) as Box<dyn std::any::Any + Send>];
let dep_receivers = vec![];
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 receivers = vec![Box::new(rx) as Box<dyn std::any::Any + Send>];
let senders = vec![];
let dependencies = vec![dep_result as Arc<dyn std::any::Any + Send + Sync>];
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());
}
#[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 receivers = vec![];
let senders = vec![];
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)
@@ -119,58 +87,18 @@ 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![];
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 rx = receivers
.pop()
.unwrap()
.downcast::<futures::channel::oneshot::Receiver<std::sync::Arc<i32>>>()
.unwrap();
let received_arc = rx.await;
assert!(received_arc.is_ok());
assert_eq!(*received_arc.unwrap(), 42);
let arc_value = result.unwrap().downcast::<i32>().unwrap();
// Also verify output was returned (Arc-wrapped)
let output = result.unwrap();
let arc_value = output.downcast::<i32>().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::<futures::channel::oneshot::Receiver<std::sync::Arc<i32>>>()
.unwrap();
let received_arc = rx.await;
assert!(received_arc.is_ok());
assert_eq!(*received_arc.unwrap(), 100);
}
}
+108 -131
View File
@@ -5,16 +5,18 @@
//!
//! 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::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use futures::channel::mpsc;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt};
use futures::stream::FuturesUnordered;
use futures::{FutureExt, StreamExt, TryFutureExt};
use parking_lot::Mutex;
#[cfg(feature = "tracing")]
@@ -54,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 }
///
@@ -82,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(|fut| { tokio::spawn(fut); }).await.unwrap();
/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
///
/// assert_eq!(dag.get(sum).unwrap(), 5);
/// # };
@@ -95,7 +98,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<Vec<Option<Box<dyn ExecutableNode>>>>,
pub(crate) nodes: Mutex<Vec<Option<Box<dyn ExecutableNode + Sync>>>>,
pub(crate) outputs: Mutex<HashMap<NodeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>>,
pub(crate) edges: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> dependencies
pub(crate) dependents: Mutex<HashMap<NodeId, Vec<NodeId>>>, // node -> tasks that depend on it
@@ -147,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,
@@ -188,13 +192,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(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
/// assert_eq!(dag.get(&inc).unwrap(), 11);
/// # };
/// ```
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
where
Tk: Task + 'static,
Tk: Task + Sync + 'static,
Tk::Input: 'static + Clone,
Tk::Output: 'static + Clone,
{
@@ -230,10 +234,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 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) }`
/// - Single-threaded: `|fut| fut`
/// - For computationally light tasks, concurrency without parallelism can be significantly faster.
///
/// # Errors
///
@@ -249,6 +256,7 @@ impl DagRunner {
///
/// ```
/// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// // Tuple struct
/// struct Value(i32);
///
@@ -272,21 +280,15 @@ 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(|fut| tokio::spawn(fut).map(Result::unwrap)).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, spawner)))]
pub async fn run<S>(&self, spawner: S) -> DagResult<()>
pub async fn run<S, F>(&self, spawner: S) -> DagResult<()>
where
S: Fn(BoxFuture<'static, ()>),
S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>) -> F,
F: Future<Output = DagResult<(NodeId, Arc<dyn Any + Send + Sync>)>>,
{
#[cfg(feature = "tracing")]
info!("starting DAG execution");
@@ -320,47 +322,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<NodeId, Vec<Box<dyn std::any::Any + Send>>> =
HashMap::new();
let mut producer_senders: HashMap<NodeId, Vec<Box<dyn std::any::Any + Send>>> =
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<std::sync::Arc<dyn std::any::Any + Send + Sync>, DagError>,
)>();
let mut outputs: HashMap<NodeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
let mut first_error = None;
for layer in layers {
#[cfg(feature = "tracing")]
@@ -375,7 +338,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.
@@ -402,8 +364,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 +371,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 +383,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| {
@@ -450,80 +410,96 @@ 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 mut futures: FuturesUnordered<_> = layer
.into_iter()
.filter_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?;
Ok((node_id, result))
};
Some(
AssertUnwindSafe(spawner(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::<String>()
{
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
}
})
.collect();
while let Some(out) = futures.next().await {
match out {
Ok(output) => {
outputs.insert(output.0, output.1);
}
Err(e) => {
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 {
@@ -554,6 +530,7 @@ impl DagRunner {
///
/// ```no_run
/// # use dagx::{task, DagRunner, Task};
/// # use futures::FutureExt;
/// struct Configuration {
/// setting: i32,
/// }
@@ -575,7 +552,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(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
///
/// assert_eq!(dag.get(task).unwrap(), 42);
/// # };
+19 -30
View File
@@ -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<u8>
let wrong_handle: TaskHandle<Vec<u8>> = 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<HashMap<String, i32>> = TaskHandle {
@@ -136,11 +131,9 @@ async fn test_get_type_mismatch_vec_different_types() {
// Task outputs Vec<i32>
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<String>
let wrong_handle: TaskHandle<Vec<String>> = TaskHandle {
@@ -170,11 +163,9 @@ async fn test_get_type_mismatch_option_types() {
// Task outputs Option<i32>
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<String>
let wrong_handle: TaskHandle<Option<String>> = 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<String> = TaskHandle {
+14 -28
View File
@@ -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<String> = 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
+6 -6
View File
@@ -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.
///
@@ -87,7 +87,7 @@ use std::future::Future;
/// # Output Wrapping
///
/// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out.
/// You define `Output = T`, but the framework wraps it in `Arc<T>` before sending to channels.
/// You define `Output = T`, but the framework wraps it in `Arc<T>` 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<Output = Self::Output> + 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
@@ -106,7 +106,7 @@ pub trait Task: Send {
#[doc(hidden)]
fn extract_and_run(
self,
receivers: Vec<Box<dyn std::any::Any + Send>>,
dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> impl Future<Output = Result<Self::Output, String>> + Send;
}
@@ -149,10 +149,10 @@ where
async fn extract_and_run(
self,
receivers: Vec<Box<dyn std::any::Any + Send>>,
dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> Result<Self::Output, String> {
// 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))?;
+2 -1
View File
@@ -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<Box<dyn std::any::Any + Send>>,
_dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, String>> + Send>> {
Box::pin(async move {
// This is a test helper - just return an error
+4 -3
View File
@@ -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(|fut| { tokio::spawn(fut); }).await.unwrap();
/// dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await.unwrap();
///
/// assert_eq!(dag.get(node).unwrap(), 42);
/// # };
+16 -25
View File
@@ -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<i32> = 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<i32> = second_builder.into();
let third_handle: TaskHandle<bool> = 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<i32> = 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<Vec<i32>> = 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);
+54 -91
View File
@@ -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();
+10 -36
View File
@@ -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(())
+77 -20
View File
@@ -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,78 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&current_layer[0], &current_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
let result = dag.get(root)?;
assert!(result > 0, "Expected positive result, got {}", result);
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<dagx::TaskHandle<i32>> = (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((&current_layer[0], &current_layer[1]));
dag.run(|fut| fut).await?;
// We created 1024 ones and 1024 zeros at the bottom
// The sum should propagate up to 1024
@@ -139,10 +205,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 +258,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 +299,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(())
+11 -40
View File
@@ -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)?);
+12 -44
View File
@@ -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::<i32, Never>::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(())
+13 -20
View File
@@ -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();
+4 -5
View File
@@ -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)
+5 -16
View File
@@ -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(())
+6 -20
View File
@@ -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);
+8 -28
View File
@@ -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(())
+18 -35
View File
@@ -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::<i32, String>::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::<i32> }));
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();
+13 -40
View File
@@ -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);
+9 -35
View File
@@ -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());
+41 -66
View File
@@ -1,27 +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(|fut| {
tokio::spawn(fut);
})
.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(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
.unwrap();
assert_eq!(dag.get(task).unwrap(), 42);
}
@@ -43,11 +40,9 @@ async fn test_deep_chain() {
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(d);
dag.run(|fut| {
tokio::spawn(fut);
})
.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
@@ -62,11 +57,9 @@ async fn test_wide_parallel() {
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.collect();
dag.run(|fut| {
tokio::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);
@@ -88,11 +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(|fut| {
tokio::spawn(fut);
})
.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
@@ -118,11 +109,9 @@ async fn test_different_output_types() {
}
}));
dag.run(|fut| {
tokio::spawn(fut);
})
.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]);
@@ -153,11 +142,9 @@ async fn test_multiple_sinks() {
.add_task(task_fn(|x: i32| async move { x * 3 }))
.depends_on(branch2);
dag.run(|fut| {
tokio::spawn(fut);
})
.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
@@ -177,11 +164,9 @@ 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);
})
.await
.unwrap();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
.unwrap();
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
}
@@ -204,11 +189,9 @@ 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);
})
.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);
@@ -230,18 +213,16 @@ async fn test_producer_consumer_channels() {
))
.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(), 15);
}
#[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,11 +238,9 @@ 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);
})
.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);
@@ -287,11 +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(|fut| {
tokio::spawn(fut);
})
.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
}
@@ -337,11 +314,9 @@ async fn test_layer_execution_order() {
}))
.depends_on(t2);
dag.run(|fut| {
tokio::spawn(fut);
})
.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]);
+13 -20
View File
@@ -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");
+34 -27
View File
@@ -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(&parallel_tasks[0]);
let seq1 = dag
.add_task(task_fn(move |x: i32| async move {
sleep(sleep_duration).await;
x + 1
}))
.depends_on(&parallel_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(())
}
}
+13 -25
View File
@@ -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::<i32, String>("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);
}
+32 -47
View File
@@ -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();
}
+5 -16
View File
@@ -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");
+2 -4
View File
@@ -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
+15 -11
View File
@@ -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;
@@ -16,16 +17,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(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify all tasks were spawned
for (i, task) in tasks.iter().enumerate() {
@@ -71,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() {
+7 -24
View File
@@ -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((&parallel1, &parallel2));
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
+3 -8
View File
@@ -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);
+5 -16
View File
@@ -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);
+6 -20
View File
@@ -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);
+5 -16
View File
@@ -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 {
+7 -24
View File
@@ -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"));
+4 -5
View File
@@ -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);
+7 -10
View File
@@ -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);
+5 -5
View File
@@ -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| 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| { async_std::task::spawn(fut); }).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| { async_std::task::spawn(fut); }).await.unwrap();
dag.run(|fut| async_std::task::spawn(fut)).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60);
}
}
+2 -4
View File
@@ -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(())
+3
View File
@@ -2,3 +2,6 @@
pub mod compatibility;
pub mod quirky;
mod async_std;
mod smol;
+52 -100
View File
@@ -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::<i32, String>(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);
});
+5 -5
View File
@@ -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,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).detach(); }).await.unwrap();
dag.run(|fut| smol::spawn(fut)).await.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5);
});
@@ -43,7 +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).detach(); }).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);
@@ -63,8 +63,8 @@ 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)).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60);
});
}
}
+10 -15
View File
@@ -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);
}
+10 -15
View File
@@ -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
}
+25 -40
View File
@@ -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!");
}
+8 -28
View File
@@ -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();
+5 -16
View File
@@ -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
+13 -20
View File
@@ -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);
@@ -44,11 +45,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 +68,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 +94,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 +128,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
+7 -10
View File
@@ -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);
}
+37 -60
View File
@@ -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<i32, String>
let result_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(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);
}
+10 -15
View File
@@ -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);