diff --git a/DOCUMENTATION_UPDATE_SUMMARY.md b/DOCUMENTATION_UPDATE_SUMMARY.md deleted file mode 100644 index 5d89ef4..0000000 --- a/DOCUMENTATION_UPDATE_SUMMARY.md +++ /dev/null @@ -1,109 +0,0 @@ -# Documentation Update Summary - Custom Type Support - -## Context - -Fixed a significant issue where dagx only worked with types that had `ExtractInput` trait implementations (primitives, String, Vec, HashMap, etc.). Custom user types didn't work. - -**The Fix**: The `#[task]` macro now generates inline `extract_and_run()` methods with type-specific extraction logic. This means **ANY type** implementing `Clone + Send + Sync + 'static` works automatically - no trait implementations required! - -## Files Updated - -### 1. **README.md** -- **Features section**: Added "Works with ANY type" as a key feature (line 117) -- **Core Concepts**: Added new "Custom Types" section with example (lines 190-218) -- **Examples**: Added `custom_types.rs` to advanced examples list (line 232) - -### 2. **CHANGELOG.md** -- **[Unreleased] section**: Added comprehensive documentation of this improvement (lines 8-33) - - Explained the change from ExtractInput to macro-generated extraction - - Highlighted that custom types now work seamlessly - - Noted new example file - -### 3. **src/lib.rs** (Main library documentation) -- **Features**: Added custom type support as third bullet point (lines 13-15) -- **Examples section**: Added new "Custom Types" example showing usage (lines 439-484) - -### 4. **src/task.rs** (Task trait documentation) -- **Trait docs**: Updated to mention macro generates extraction logic (lines 10-15) -- Added note that custom types work automatically - -### 5. **src/extract.rs** (ExtractInput trait documentation) -- **Module docs**: Updated to clarify this is now legacy/internal only (lines 1-33) -- Explained modern approach via `#[task]` macro vs legacy ExtractInput trait -- Noted ExtractInput only used by internal `task_fn` helper - -### 6. **dagx-macros/src/lib.rs** (Macro documentation) -- **Macro docs**: Added emphasis on type-specific extraction generation (lines 15-22) -- Highlighted that custom types work without trait implementations - -### 7. **examples/custom_types.rs** (NEW FILE) -- **Created comprehensive example** demonstrating custom type support -- Shows nested custom types (User, Project, TeamMember, Team) -- Demonstrates types flowing through multiple task layers -- Full working example with detailed documentation comments -- Expected output documented in header - -## Key Messages in Documentation - -### What Changed -1. **Before**: Only types with ExtractInput implementations worked (primitives + standard types) -2. **After**: ANY type with `Clone + Send + Sync + 'static` works automatically - -### Why This Matters -- No trait boilerplate for custom types -- Nested structs, collections of custom types all work seamlessly -- Simpler mental model - just derive Clone and go! -- More ergonomic API - -### How It Works -- `#[task]` macro generates custom `extract_and_run()` implementation per task -- Type-specific extraction logic inlined at compile time -- No runtime overhead compared to ExtractInput approach -- ExtractInput now only used internally by test helper - -## Documentation Gaps Remaining - -None found. The following were checked and updated: -- ✅ README.md (main documentation) -- ✅ CHANGELOG.md (version history) -- ✅ src/lib.rs (API documentation) -- ✅ src/task.rs (Task trait docs) -- ✅ src/extract.rs (ExtractInput trait docs) -- ✅ dagx-macros/src/lib.rs (macro documentation) -- ✅ examples/ (new example added) -- ✅ CONTRIBUTING.md (no changes needed - general guidelines) -- ✅ docs/*.md (no changes needed - architectural docs) - -## Example Test Status - -The new `custom_types.rs` example: -- Created at `/home/swaits/Code/dagx/examples/custom_types.rs` -- Demonstrates User, Project, TeamMember, Team custom types -- Shows nested types and collections -- Needs to be compiled and tested: `cargo run --example custom_types` - -## Recommendations - -### Before Release -1. **Compile and test** the new example: `cargo run --example custom_types` -2. **Update lib.rs last audited date** (currently shows 2025-10-06 for v0.1.0) -3. **Review CHANGELOG** to ensure version number and date are correct when releasing -4. Consider adding a **migration guide** if there are any breaking changes - -### Future Documentation Enhancements -1. Consider adding a "Type Requirements" section to README explaining `Clone + Send + Sync + 'static` -2. Could add more examples showing: - - Enums as task outputs - - Generic custom types - - Types with lifetimes (showing why 'static is needed) - -## Summary - -All documentation has been updated to accurately reflect the new custom type support. The key improvement - that ANY type works automatically with just `Clone + Send + Sync` - is now prominently featured in: -- Main features list -- Core concepts with examples -- API documentation -- Macro documentation -- A comprehensive new example - -The documentation emphasizes this is a **major ergonomic improvement** that makes dagx significantly easier to use with custom types. diff --git a/README.md b/README.md index 17300b5..16875ce 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ See [design philosophy](docs/DESIGN_PHILOSOPHY.md) for details. - **Compile-time cycle prevention**: Type system makes cycles impossible — no runtime checks - **Compile-time type safety**: Dependencies validated at compile time, no runtime type errors -- **Works with ANY type**: Custom types work automatically — just `Clone + Send + Sync`, no trait implementations needed +- **Works with ANY type**: Custom types work automatically — just `Send + Sync + 'static`, no trait implementations needed - **Runtime-agnostic**: Works with Tokio, async-std, smol, or any async runtime - **True parallelism**: Tasks spawn to multiple threads for genuine parallel execution - **Type-state pattern**: API prevents incorrect wiring through compile-time errors @@ -189,10 +189,9 @@ let task = dag.add_task(my_task).depends_on((&upstream1, &upstream2)); ### Custom Types -**dagx works with ANY type automatically!** As long as your type implements `Clone + Send + Sync + 'static`, it works seamlessly: +**dagx works with ANY type automatically!** As long as your type implements `Send + Sync + 'static`, it works seamlessly: ```rust -#[derive(Clone)] // Just derive Clone! struct User { name: String, age: u32, diff --git a/benches/basic/execution.rs b/benches/basic/execution.rs index 9296422..85aa2c0 100644 --- a/benches/basic/execution.rs +++ b/benches/basic/execution.rs @@ -12,7 +12,7 @@ pub fn bench_dag_execution(c: &mut Criterion) { rt.block_on(async { let dag = DagRunner::new(); for i in 0..10 { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -26,7 +26,7 @@ pub fn bench_dag_execution(c: &mut Criterion) { rt.block_on(async { let dag = DagRunner::new(); for i in 0..100 { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -51,61 +51,61 @@ pub fn bench_dag_execution(c: &mut Criterion) { let t0 = dag.add_task(Value(0)); let t1 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t0); let t2 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t1); let t3 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t2); let t4 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t3); let t5 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t4); let t6 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t5); let t7 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t6); let t8 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t7); let t9 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t8); let t10 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t9); let t11 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t10); let t12 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t11); let t13 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t12); let t14 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t13); let t15 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t14); let t16 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t15); let t17 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t16); let t18 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t17); let _t19 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t18); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/benches/basic/scaling.rs b/benches/basic/scaling.rs index 7fd1501..32199aa 100644 --- a/benches/basic/scaling.rs +++ b/benches/basic/scaling.rs @@ -17,7 +17,7 @@ pub fn bench_dag_scaling(c: &mut Criterion) { rt.block_on(async { let dag = DagRunner::new(); for i in 0..size { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await diff --git a/benches/comparisons/deep_chain.rs b/benches/comparisons/deep_chain.rs index 0cf5b69..e58eb32 100644 --- a/benches/comparisons/deep_chain.rs +++ b/benches/comparisons/deep_chain.rs @@ -18,14 +18,14 @@ pub fn bench_deep_chain(c: &mut Criterion) { let dag = DagRunner::new(); - let first: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 0 })).into(); + let first: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)).into(); let mut prev = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(first); for i in 2..100 { prev = dag - .add_task(task_fn(move |x: i32| async move { x + i })) + .add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(prev); } diff --git a/benches/comparisons/diamond_pattern.rs b/benches/comparisons/diamond_pattern.rs index a0a8ebb..8406457 100644 --- a/benches/comparisons/diamond_pattern.rs +++ b/benches/comparisons/diamond_pattern.rs @@ -18,15 +18,15 @@ pub fn bench_diamond_pattern(c: &mut Criterion) { let dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); + let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); let b = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(a); let d = dag - .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) + .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((&b, &c)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/benches/comparisons/etl_pipeline.rs b/benches/comparisons/etl_pipeline.rs index 50a7be6..bf7e506 100644 --- a/benches/comparisons/etl_pipeline.rs +++ b/benches/comparisons/etl_pipeline.rs @@ -18,34 +18,34 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { let dag = DagRunner::new(); // Extract from 3 sources - let source1 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })); - let source2 = dag.add_task(task_fn(|_: ()| async { vec![6, 7, 8, 9, 10] })); - let source3 = dag.add_task(task_fn(|_: ()| async { vec![11, 12, 13, 14, 15] })); + let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])); + let source2 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![6, 7, 8, 9, 10])); + let source3 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![11, 12, 13, 14, 15])); // Transform each source let transform1 = dag - .add_task(task_fn(|data: Vec| async move { - data.into_iter().map(|x| x * 2).collect::>() + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter().map(|x| x * 2).collect::>() })) .depends_on(source1); let transform2 = dag - .add_task(task_fn(|data: Vec| async move { - data.into_iter().map(|x| x * 2).collect::>() + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter().map(|x| x * 2).collect::>() })) .depends_on(source2); let transform3 = dag - .add_task(task_fn(|data: Vec| async move { - data.into_iter().map(|x| x * 2).collect::>() + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter().map(|x| x * 2).collect::>() })) .depends_on(source3); // Merge all transformed data let merge = dag - .add_task(task_fn( - |(d1, d2, d3): (Vec, Vec, Vec)| async move { - let mut result = d1; + .add_task(task_fn::<(Vec<_>, Vec<_>, Vec<_>), _, _>( + |(d1, d2, d3): (&Vec, &Vec, &Vec)| { + let mut result = d1.clone(); result.extend(d2); result.extend(d3); result @@ -55,15 +55,18 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { // Validate let validate = dag - .add_task(task_fn(|data: Vec| async move { - data.into_iter().filter(|&x| x > 0).collect::>() + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter() + .copied() + .filter(|&x| x > 0) + .collect::>() })) .depends_on(merge); // Load (compute sum) let load = dag - .add_task(task_fn(|data: Vec| async move { - data.into_iter().sum::() + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter().sum::() })) .depends_on(validate); diff --git a/benches/comparisons/large_scale.rs b/benches/comparisons/large_scale.rs index dd76cc0..fbb9541 100644 --- a/benches/comparisons/large_scale.rs +++ b/benches/comparisons/large_scale.rs @@ -19,7 +19,7 @@ pub fn bench_large_scale(c: &mut Criterion) { let dag = DagRunner::new(); for i in 0..10_000 { - dag.add_task(task_fn(move |_: ()| async move { i * 2 })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)); } dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/benches/comparisons/linear_pipeline.rs b/benches/comparisons/linear_pipeline.rs index 3b1402c..bfbb7cc 100644 --- a/benches/comparisons/linear_pipeline.rs +++ b/benches/comparisons/linear_pipeline.rs @@ -17,18 +17,18 @@ pub fn bench_linear_pipeline(c: &mut Criterion) { let dag = DagRunner::new(); - let a = dag.add_task(task_fn(|_: ()| async { 1 })); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let b = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(b); let d = dag - .add_task(task_fn(|x: i32| async move { x - 1 })) + .add_task(task_fn::(|&x: &i32| x - 1)) .depends_on(c); let e = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(d); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/benches/comparisons/wide_fanout.rs b/benches/comparisons/wide_fanout.rs index 25b0079..6aa00fc 100644 --- a/benches/comparisons/wide_fanout.rs +++ b/benches/comparisons/wide_fanout.rs @@ -17,11 +17,11 @@ pub fn bench_wide_fanout(c: &mut Criterion) { use dagx::{task_fn, DagRunner}; let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Create 100 tasks that all depend on source for i in 0..100 { - dag.add_task(task_fn(move |x: i32| async move { x + i })) + dag.add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(source); } diff --git a/benches/patterns/breakdown.rs b/benches/patterns/breakdown.rs index d887449..c4f5483 100644 --- a/benches/patterns/breakdown.rs +++ b/benches/patterns/breakdown.rs @@ -14,7 +14,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { b.iter(|| { let dag = DagRunner::new(); for i in 0..10_000 { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } }); }); @@ -25,7 +25,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { rt.block_on(async { let dag = DagRunner::new(); for i in 0..10_000 { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -39,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { // Pre-build the DAG outside the measurement let dag = DagRunner::new(); for i in 0..10_000 { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } b.iter(|| { diff --git a/benches/patterns/diamond.rs b/benches/patterns/diamond.rs index cf012b9..bb522a9 100644 --- a/benches/patterns/diamond.rs +++ b/benches/patterns/diamond.rs @@ -14,16 +14,16 @@ pub fn bench_diamond(c: &mut Criterion) { for i in 0..50 { let source: TaskHandle<_> = - dag.add_task(task_fn(move |_: ()| async move { i })).into(); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into(); let left = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(source); let right = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(source); - dag.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) + dag.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) .depends_on((&left, &right)); } diff --git a/benches/patterns/fanout.rs b/benches/patterns/fanout.rs index 71ee6b9..14c61d8 100644 --- a/benches/patterns/fanout.rs +++ b/benches/patterns/fanout.rs @@ -12,10 +12,10 @@ pub fn bench_fanout(c: &mut Criterion) { b.iter(|| { rt.block_on(async { let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); for i in 0..100 { - dag.add_task(task_fn(move |x: i32| async move { x + i })) + dag.add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(source); } @@ -32,14 +32,14 @@ pub fn bench_fanout(c: &mut Criterion) { rt.block_on(async { let dag = DagRunner::new(); let source: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { // Create 1000 strings - framework wraps in Arc automatically (0..1000).map(|i| format!("Item {}", i)).collect::>() })) .into(); for i in 0..100 { - dag.add_task(task_fn(move |data: Vec| async move { + dag.add_task(task_fn::, _, _>(move |data: &Vec| { // Each consumer gets data extracted from Arc data.iter().filter(|s| s.contains(&i.to_string())).count() })) @@ -63,11 +63,12 @@ pub fn bench_fanout(c: &mut Criterion) { // Source produces a simple Copy type (usize) // Framework wraps in Arc internally - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42usize })).into(); + let source: TaskHandle<_> = + dag.add_task(task_fn::<(), _, _>(|_: ()| 42usize)).into(); // 100 dependents - Arc overhead visible on Copy types for i in 0..100 { - dag.add_task(task_fn(move |x: usize| async move { x + i })) + dag.add_task(task_fn::(move |x: &usize| x + i)) .depends_on(source); } diff --git a/benches/realistic/config_broadcast.rs b/benches/realistic/config_broadcast.rs index 0be4802..b0a5658 100644 --- a/benches/realistic/config_broadcast.rs +++ b/benches/realistic/config_broadcast.rs @@ -21,7 +21,7 @@ pub fn bench_config_broadcast(c: &mut Criterion) { // Configuration source - framework wraps HashMap in Arc automatically let config: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { HashMap::from([ ("max_connections".to_string(), "100".to_string()), ("timeout_ms".to_string(), "5000".to_string()), @@ -37,20 +37,22 @@ pub fn bench_config_broadcast(c: &mut Criterion) { // Workers receive and process based on shared config for worker_id in 0..workers { - dag.add_task(task_fn(move |cfg: HashMap| async move { - // Simulate work based on configuration - let batch_size: usize = cfg - .get("batch_size") - .and_then(|s| s.parse().ok()) - .unwrap_or(10); - let timeout: usize = cfg - .get("timeout_ms") - .and_then(|s| s.parse().ok()) - .unwrap_or(1000); + dag.add_task(task_fn::, _, _>( + move |cfg: &HashMap| { + // Simulate work based on configuration + let batch_size: usize = cfg + .get("batch_size") + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + let timeout: usize = cfg + .get("timeout_ms") + .and_then(|s| s.parse().ok()) + .unwrap_or(1000); - // Simulate some computation - black_box(worker_id * batch_size + timeout / 100) - })) + // Simulate some computation + black_box(worker_id * batch_size + timeout / 100) + }, + )) .depends_on(config); } diff --git a/benches/realistic/etl.rs b/benches/realistic/etl.rs index 6810787..9f4f853 100644 --- a/benches/realistic/etl.rs +++ b/benches/realistic/etl.rs @@ -21,24 +21,26 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { let dag = DagRunner::new(); // Extract: Load JSON-like data (framework wraps in Arc automatically) - let extract: TaskHandle<_> = dag.add_task(task_fn(move |_: ()| async move { - (0..count) - .map(|i| { - format!( - "{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}", - i, - i, - i * 10 - ) - }) - .collect::>() - })).into(); + let extract: TaskHandle<_> = dag + .add_task(task_fn::<(), _, _>(move |_: ()| { + (0..count) + .map(|i| { + format!( + "{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}", + i, + i, + i * 10 + ) + }) + .collect::>() + })) + .into(); // Transform: Three parallel transformations // T1: Parse and validate let validate = dag - .add_task(task_fn(move |data: Vec| async move { + .add_task(task_fn::, _, _>(move |data: &Vec| { data.iter() .filter(|s| { s.contains("\"id\"") @@ -51,7 +53,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { // T2: Extract scores let scores = dag - .add_task(task_fn(move |data: Vec| async move { + .add_task(task_fn::, _, _>(move |data: &Vec| { data.iter() .filter_map(|s| { s.split("\"score\":").nth(1).and_then(|part| { @@ -64,7 +66,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { // T3: Extract names let names = dag - .add_task(task_fn(move |data: Vec| async move { + .add_task(task_fn::, _, _>(move |data: &Vec| { data.iter() .filter_map(|s| { s.split("\"name\":\"") @@ -77,8 +79,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { .depends_on(extract); // Load: Aggregate all transformations - dag.add_task(task_fn( - move |(valid, scores, names): (usize, Vec, Vec)| async move { + dag.add_task(task_fn::<(usize, Vec<_>, Vec<_>), _, _>( + move |(valid, scores, names): (&usize, &Vec, &Vec)| { let avg_score = scores.iter().sum::() / scores.len().max(1) as i32; format!( @@ -91,9 +93,9 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { )) .depends_on((&validate, &scores, &names)); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); }) }); }, diff --git a/benches/realistic/memory_efficiency.rs b/benches/realistic/memory_efficiency.rs index 8670ab6..fa3f435 100644 --- a/benches/realistic/memory_efficiency.rs +++ b/benches/realistic/memory_efficiency.rs @@ -20,7 +20,7 @@ pub fn bench_memory_efficiency(c: &mut Criterion) { let dag = DagRunner::new(); let source: TaskHandle<_> = dag - .add_task(task_fn(move |_: ()| async move { + .add_task(task_fn::<(), _, _>(move |_: ()| { // Create MB of data - framework wraps in Arc automatically vec![0u8; mb * 1024 * 1024] })) @@ -28,7 +28,7 @@ pub fn bench_memory_efficiency(c: &mut Criterion) { // 20 consumers share the data via automatic Arc wrapping for i in 0..20 { - dag.add_task(task_fn(move |data: Vec| async move { + dag.add_task(task_fn::, _, _>(move |data: &Vec| { // Process a small portion (data extracted from Arc) let sample_sum: u32 = data .iter() diff --git a/benches/realistic/mixed.rs b/benches/realistic/mixed.rs index 445eb41..02bf033 100644 --- a/benches/realistic/mixed.rs +++ b/benches/realistic/mixed.rs @@ -14,7 +14,7 @@ pub fn bench_mixed_patterns(c: &mut Criterion) { // Stage 1: Load data (framework wraps in Arc automatically) let data: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { (0..1000) .map(|i| format!("Record-{:04}", i)) .collect::>() @@ -23,31 +23,33 @@ pub fn bench_mixed_patterns(c: &mut Criterion) { // Stage 2: Fan-out for parallel analysis let analysis1 = dag - .add_task(task_fn(|d: Vec| async move { + .add_task(task_fn::, _, _>(|d: &Vec| { d.iter().filter(|s| s.contains("00")).count() })) .depends_on(data); let analysis2 = dag - .add_task(task_fn(|d: Vec| async move { + .add_task(task_fn::, _, _>(|d: &Vec| { d.iter().map(|s| s.len()).sum::() })) .depends_on(data); let analysis3 = dag - .add_task(task_fn(|d: Vec| async move { d.len() })) + .add_task(task_fn::, _, _>(|d: &Vec| d.len())) .depends_on(data); // Stage 3: Fan-in to aggregate let summary = dag - .add_task(task_fn(|(a1, a2, a3): (usize, usize, usize)| async move { - format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3) - })) + .add_task(task_fn::<(usize, usize, usize), _, _>( + |(a1, a2, a3): (&usize, &usize, &usize)| { + format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3) + }, + )) .depends_on((&analysis1, &analysis2, &analysis3)); // Stage 4: Fan-out final report to multiple destinations for i in 0..3 { - dag.add_task(task_fn(move |report: String| async move { + dag.add_task(task_fn::(move |report: &String| { format!("Destination {}: {}", i, report) })) .depends_on(summary); diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index 23f09fb..891e03b 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -4,22 +4,22 @@ //! the `Task` trait by deriving Input and Output types from the `run()` method signature. use proc_macro::TokenStream; +use proc_macro2::Span; use quote::quote; -use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType, Type}; +use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, ReturnType, Type}; /// Attribute macro to automatically implement the `Task` trait. /// /// Apply this to an `impl` block containing a `run()` method (sync or async). The macro: /// - Derives `Input` and `Output` types from the `run()` signature /// - Automatically implements the `Task` trait -/// - **Generates type-specific extraction logic** - works with ANY type (Clone + Send + Sync)! +/// - **Generates type-specific extraction logic** - works with ANY type (Send + Sync + 'static)! /// - Supports both sync and async run methods /// - Supports stateless (no self) and stateful (&self, &mut self) tasks /// - Handles various input patterns (no inputs, single input, multiple inputs) /// /// **Key Feature**: Custom types work automatically without implementing any traits! -/// The macro generates inline extraction logic in `extract_and_run()` specific to your -/// task's parameter types. Just derive `Clone` on your types and they'll work seamlessly. +/// The macro generates inline extraction logic specific to your task's parameter types. /// /// # Task Patterns /// @@ -135,8 +135,6 @@ use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType /// - The impl block must contain exactly one `async fn run()` method /// - The `run()` method can be stateless (no self parameter) or stateful (`&mut self`) /// - All input parameters must be references (e.g., `&i32`, not `i32`) -/// - The macro requires `Task` to be in scope: `use dagx::Task;` -/// - For stateless tasks, the struct must implement `Default` (e.g., unit structs) /// /// # Generated Code /// @@ -232,9 +230,6 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Check if the method is async or sync - let is_async = run_method.sig.asyncness.is_some(); - // Extract parameters (excluding self) let params_result: Result, _> = run_method .sig @@ -280,32 +275,39 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { // Extract return type let output_type = match &run_method.sig.output { ReturnType::Default => syn::parse_quote!(()), - ReturnType::Type(_, ty) => ty.clone(), + ReturnType::Type(_, ty) => { + if let Type::Tuple(_tuple) = &**ty { + return syn::Error::new_spanned( + ty, + "Returning bare tuples from task functions is currently not supported.\n\n\ + Please wrap this in a newtype or struct.", + ) + .into_compile_error() + .into(); + } + ty.clone() + } }; // Build Input type based on parameter count - let input_type = match params.len() { - 0 => quote! { () }, - 1 => { - let (_name, ty) = ¶ms[0]; - quote! { #ty } - } - _ => { - let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect(); - quote! { ( #(#types),* ) } - } - }; + let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect(); + let input_type = quote! { ( #(#types,)* ) }; - // Generate parameter destructuring for the wrapper run() method - let (param_destructure, param_refs) = if params.is_empty() { - (quote! { _ }, quote! {}) - } else if params.len() == 1 { - let (name, _) = ¶ms[0]; - (quote! { #name }, quote! { &#name }) - } else { - let names: Vec<_> = params.iter().map(|(name, _)| name).collect(); - let refs: Vec<_> = params.iter().map(|(name, _)| quote! { &#name }).collect(); - (quote! { ( #(#names),* ) }, quote! { #(#refs),* }) + // Generate parameter references for the wrapper run() method + let (param_destructure, param_refs) = { + let (destructure, refs): (Vec<_>, Vec<_>) = params + .iter() + .map(|(ident, _ty)| { + let ident = Ident::new(&format!("__dagx_extract_{}", ident), Span::mixed_site()); + ( + quote! { + let (#ident, input) = input.next(); + }, + ident, + ) + }) + .unzip(); + (quote! { #(#destructure)* }, quote! { #(#refs),* }) }; // Clone the run method and rename it to run_impl @@ -319,179 +321,30 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { .iter() .any(|arg| matches!(arg, FnArg::Receiver(_))); - // Generate extract_and_run implementation based on parameter count - let extract_and_run_impl = match params.len() { - 0 => { - // Zero parameters - no extraction needed - quote! { - fn extract_and_run( - self, - _dependencies: Vec>, - ) -> impl std::future::Future> + Send { - async move { - let input = (); - Ok(self.run(input).await) - } - } - } - } - 1 => { - // Single parameter - let param_type = ¶ms[0].1; - quote! { - fn extract_and_run( - self, - mut dependencies: Vec>, - ) -> impl std::future::Future> + Send { - async move { - use std::sync::Arc; + let is_async = run_method.sig.asyncness.is_some(); - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - - let arc_value = dependencies.pop() - .unwrap() - .downcast::<#param_type>() - .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<#param_type>()))?; - - let input = (*arc_value).clone(); - Ok(self.run(input).await) - } - } - } - } - _ => { - // Multiple parameters - let param_count = params.len(); - let param_types: Vec<_> = params.iter().map(|(_, ty)| ty).collect(); - let indices: Vec<_> = (0..param_count).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, - dependencies: Vec>, - ) -> impl std::future::Future> + Send { - async move { - use std::sync::Arc; - - let expected_count = #param_count; - if dependencies.len() != expected_count { - return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len())); - } - - let mut iter = dependencies.into_iter(); - - // 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>()))?, - )*); - - // Clone inner values and build tuple - let input = (#( - (*arc_results.#syn_indices).clone() - ),*); - - Ok(self.run(input).await) - } - } - } - } + let run_call = match (has_self_receiver, is_async) { + (true, true) => quote! { self.run_impl(#param_refs).await }, + (true, false) => quote! { self.run_impl(#param_refs) }, + (false, true) => quote! { Self::run_impl(#param_refs).await }, + (false, false) => quote! { Self::run_impl(#param_refs) }, }; // Generate the Task trait implementation based on whether we have self and async/sync - let expanded = if has_self_receiver { - // Stateful task - consumes self but delegates to a method that borrows - if is_async { - // Async with self - quote! { - impl ::dagx::Task for #struct_name { - type Input = #input_type; - type Output = #output_type; + quote! { + impl ::dagx::Task for #struct_name { + type Input = #input_type; + type Output = #output_type; - async fn run(mut self, input: Self::Input) -> Self::Output { - let #param_destructure = input; - self.run_impl(#param_refs).await - } - - #extract_and_run_impl - } - - impl #struct_name { - #run_impl_method - } - } - } else { - // Sync with self - wrap in async block - quote! { - impl ::dagx::Task for #struct_name { - type Input = #input_type; - type Output = #output_type; - - async fn run(mut self, input: Self::Input) -> Self::Output { - let #param_destructure = input; - self.run_impl(#param_refs) - } - - #extract_and_run_impl - } - - impl #struct_name { - #run_impl_method - } + async fn run(mut self, mut input: ::dagx::TaskInput<'_, Self::Input>) -> Self::Output { + #param_destructure + #run_call } } - } else { - // Stateless task - if is_async { - // Async stateless - quote! { - impl ::dagx::Task for #struct_name { - type Input = #input_type; - type Output = #output_type; - async fn run(self, input: Self::Input) -> Self::Output { - let #param_destructure = input; - Self::run_impl(#param_refs).await - } - - #extract_and_run_impl - } - - impl #struct_name { - #[inline] - #run_impl_method - } - } - } else { - // Sync stateless - quote! { - impl ::dagx::Task for #struct_name { - type Input = #input_type; - type Output = #output_type; - - async fn run(self, input: Self::Input) -> Self::Output { - let #param_destructure = input; - Self::run_impl(#param_refs) - } - - #extract_and_run_impl - } - - impl #struct_name { - #[inline] - #run_impl_method - } - } + impl #struct_name { + #run_impl_method } - }; - - TokenStream::from(expanded) + } + .into() } diff --git a/docs/COMPARISONS.md b/docs/COMPARISONS.md index dd7ed7a..eec374d 100644 --- a/docs/COMPARISONS.md +++ b/docs/COMPARISONS.md @@ -227,7 +227,7 @@ impl Task for MyTask { let mut dag = Dag::default(); dag.add_vertex("one", || async move { /* task */ }); dag.add_edge("one", "two"); -dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; +dag.run().await?; ``` ### tasksitter (Dynamic Workflows) diff --git a/examples/01_basic.rs b/examples/01_basic.rs index 6065b42..3b6d5bb 100644 --- a/examples/01_basic.rs +++ b/examples/01_basic.rs @@ -113,11 +113,10 @@ async fn main() { // Step 7: Retrieve results // // Use .get() with a TaskHandle to retrieve the output. - // Results are cloned, so you can retrieve them multiple times. + // Results are not cloned, so you can only retrieve them once. println!("\nResult: {}", dag.get(sum).unwrap()); // Note: You can also retrieve intermediate results assert_eq!(dag.get(x).unwrap(), 2); assert_eq!(dag.get(y).unwrap(), 3); - assert_eq!(dag.get(sum).unwrap(), 5); } diff --git a/examples/04_parallel_computation.rs b/examples/04_parallel_computation.rs index abbca1f..2198956 100644 --- a/examples/04_parallel_computation.rs +++ b/examples/04_parallel_computation.rs @@ -52,7 +52,7 @@ //! This proves dagx runs tasks on multiple CPU cores simultaneously. //! ``` -use dagx::{task, DagResult, DagRunner, Task}; +use dagx::{task, DagResult, DagRunner, Task, TaskInput}; use futures::FutureExt; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -152,10 +152,10 @@ async fn main() -> DagResult<()> { let task3 = ComputeSum::new(501, 751, WORK_DELAY); let task4 = ComputeSum::new(751, 1001, WORK_DELAY); - let r1 = task1.run(()).await; - let r2 = task2.run(()).await; - let r3 = task3.run(()).await; - let r4 = task4.run(()).await; + let r1 = task1.run(TaskInput::empty()).await; + let r2 = task2.run(TaskInput::empty()).await; + let r3 = task3.run(TaskInput::empty()).await; + let r4 = task4.run(TaskInput::empty()).await; let result = r1 + r2 + r3 + r4; let elapsed = start.elapsed(); diff --git a/examples/custom_types.rs b/examples/custom_types.rs index 60f8198..c4ebb11 100644 --- a/examples/custom_types.rs +++ b/examples/custom_types.rs @@ -1,20 +1,18 @@ //! # Custom Types: Using Your Own Structs //! //! This example demonstrates that **ANY custom type** works in dagx as long as it implements -//! `Clone + Send + Sync + 'static`. You don't need to implement any special traits! +//! ` Send + Sync + 'static`. You don't need to implement any special traits! //! //! ## What You'll Learn //! - How to use custom structs as task outputs //! - How custom types flow through the DAG automatically //! - How one task's output can be consumed by multiple downstream tasks -//! - No trait implementations required (just derive Clone!) //! //! ## Key Insight //! //! **dagx works with ANY type automatically!** The `#[task]` macro generates type-specific //! extraction logic, so you don't need to implement any special traits. Just make sure your //! type implements: -//! - `Clone` (so it can be extracted from Arc) //! - `Send + Sync` (required for async tasks) //! - `'static` (no lifetime constraints) //! @@ -39,8 +37,7 @@ use dagx::{task, DagRunner, TaskHandle}; use futures::FutureExt; -// Custom type - just derive Clone! -#[derive(Clone, Debug)] +#[derive(Debug)] struct User { name: String, age: u32, diff --git a/examples/debug_tracing.rs b/examples/debug_tracing.rs index 9525f5c..233ecfa 100644 --- a/examples/debug_tracing.rs +++ b/examples/debug_tracing.rs @@ -446,7 +446,7 @@ async fn main() { .unwrap(); // Check results - match dag.get(payment).unwrap() { + match dag.get(payment).unwrap().as_ref() { Ok(_) => println!("\nPayment validation succeeded"), Err(trace) => { println!("\n[{}] Payment validation failed:", correlation_id); diff --git a/examples/error_handling.rs b/examples/error_handling.rs index c6aa7ab..22cd157 100644 --- a/examples/error_handling.rs +++ b/examples/error_handling.rs @@ -227,7 +227,7 @@ async fn main() { .await .unwrap(); - match dag.get(logged).unwrap() { + match dag.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } @@ -253,7 +253,7 @@ async fn main() { .await .unwrap(); - match dag.get(logged).unwrap() { + match dag.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } @@ -279,7 +279,7 @@ async fn main() { .await .unwrap(); - match dag.get(logged).unwrap() { + match dag.get(logged).unwrap().as_ref() { Ok(result) => println!("Final result: {}\n", result), Err(e) => eprintln!("Final error: {}\n", e), } diff --git a/examples/parallelism_proof.rs b/examples/parallelism_proof.rs deleted file mode 100644 index 97e7964..0000000 --- a/examples/parallelism_proof.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! # Proof of True Parallelism -//! -//! This example demonstrates that dagx provides genuine parallel execution -//! by running 10,000 tasks that each sleep for 1 second, completing in ~1 second -//! total instead of 10,000 seconds. -//! -//! ## What You'll Learn -//! - How to verify true parallel execution -//! - The difference between concurrency and parallelism -//! - How dagx scales to thousands of parallel tasks -//! - Performance characteristics of parallel execution -//! -//! ## The Proof -//! -//! If tasks ran sequentially: -//! - 10,000 tasks × 1 second each = 10,000 seconds (2.78 hours) -//! -//! With true parallelism: -//! - All 10,000 tasks run simultaneously = ~1 second total -//! -//! This dramatic difference (10,000x speedup) proves that tasks execute in -//! parallel across multiple threads, not sequentially. -//! -//! ## Key Concepts -//! -//! **Concurrency** - Multiple tasks making progress, but not necessarily at the same time -//! **Parallelism** - Multiple tasks executing simultaneously on different CPU cores -//! -//! This example proves dagx provides true parallelism. -//! -//! ## Running This Example -//! ```bash -//! cargo run --example parallelism_proof --release -//! ``` -//! -//! ## Expected Output -//! ```text -//! === Parallelism Proof === -//! -//! Launching 10,000 tasks that each sleep for 1 second... -//! -//! Result: 10,000 tasks × 1s sleep completed in ~1.2s -//! -//! Proof: If tasks ran sequentially, this would take 10,000 seconds (2.78 hours). -//! Actual time: 1.2s -//! Speedup: 8333x -//! This proves TRUE PARALLEL EXECUTION! -//! ``` - -use dagx::{task, DagRunner}; -use futures::FutureExt; -use std::time::{Duration, Instant}; -use tokio::time::sleep; - -// Task that sleeps for a duration and returns its ID -struct SleepTask { - id: u32, - duration: Duration, -} - -#[task] -impl SleepTask { - async fn run(&self) -> u32 { - sleep(self.duration).await; - self.id - } -} - -#[tokio::main] -async fn main() { - println!("=== Parallelism Proof ===\n"); - println!("Launching 10,000 tasks that each sleep for 1 second...\n"); - - let dag = DagRunner::new(); - let sleep_duration = Duration::from_secs(1); - let num_tasks = 10_000u32; - - // Create 10,000 independent tasks that each sleep for 1 second - let tasks: Vec<_> = (0..num_tasks) - .map(|i| { - dag.add_task(SleepTask { - id: i, - duration: sleep_duration, - }) - }) - .collect(); - - // Measure execution time - let start = Instant::now(); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - let elapsed = start.elapsed(); - - // Verify all tasks completed correctly - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i as u32); - } - - // Calculate what sequential execution would take - let sequential_time = sleep_duration * num_tasks; - let speedup = sequential_time.as_secs_f64() / elapsed.as_secs_f64(); - - println!( - "Result: {} tasks × {}s sleep completed in {:.1}s\n", - num_tasks, - sleep_duration.as_secs(), - elapsed.as_secs_f64() - ); - - println!( - "Proof: If tasks ran sequentially, this would take {} seconds ({:.2} hours).", - sequential_time.as_secs(), - sequential_time.as_secs_f64() / 3600.0 - ); - println!(" Actual time: {:.1}s", elapsed.as_secs_f64()); - println!(" Speedup: {:.0}x", speedup); - - // The proof: execution time should be close to 1 second, not 10,000 seconds - if elapsed < Duration::from_secs(3) { - println!(" This proves TRUE PARALLEL EXECUTION!"); - } else { - panic!( - "FAILED: Tasks appear to be running sequentially! Took {:.1}s instead of ~1s", - elapsed.as_secs_f64() - ); - } - - println!("\n=== Parallelism Proof Complete ==="); -} diff --git a/src/builder.rs b/src/builder.rs index ab339ec..1f965a3 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,15 +11,6 @@ use crate::types::{NodeId, TaskHandle}; #[cfg(feature = "tracing")] use tracing::debug; -/// Marker trait to identify the unit type at compile time. -/// -/// This is used to enforce that tasks with non-unit inputs MUST call `.depends_on()`, -/// while tasks with unit inputs can be used directly. -pub trait IsUnitType {} - -/// Only the unit type implements IsUnitType -impl IsUnitType for () {} - /// Node builder that tracks dependency completion via type state. /// /// A `TaskBuilder` is returned from [`DagRunner::add_task`] and uses the type-state diff --git a/src/builder/tests.rs b/src/builder/tests.rs index c9aca0d..087250b 100644 --- a/src/builder/tests.rs +++ b/src/builder/tests.rs @@ -1,6 +1,5 @@ //! Unit tests for builder module -use crate::builder::IsUnitType; use crate::runner::DagRunner; use crate::types::TaskHandle; @@ -52,24 +51,12 @@ fn test_task_builder_from_conversion() { let builder = dag.add_task(TestTask { value: 42 }); // Convert builder to TaskHandle - let handle: TaskHandle = builder.into(); + let handle: TaskHandle<_> = builder.into(); // Check that the ID is preserved assert_eq!(handle.id.0, 0); // First task should have ID 0 } -#[test] -fn test_is_unit_type_trait() { - // Test that IsUnitType is correctly implemented for () - fn assert_is_unit_type() {} - - // This should compile - () implements IsUnitType - assert_is_unit_type::<()>(); - - // We can't test the negative case (non-unit types don't implement it) - // because that would be a compile error -} - #[test] fn test_task_builder_depends_on_returns_handle() { init_tracing(); @@ -79,7 +66,7 @@ fn test_task_builder_depends_on_returns_handle() { let dependent = dag.add_task(TestTaskWithInput).depends_on(source); // depends_on should return a TaskHandle - let _handle: TaskHandle = dependent; + let _handle: TaskHandle<_> = dependent; } #[test] @@ -92,9 +79,9 @@ fn test_task_builder_chain() { let t3 = dag.add_task(TestTaskWithInput).depends_on(t2); // All should return TaskHandles - let _: TaskHandle = t1; - let _: TaskHandle = t2; - let _: TaskHandle = t3; + let _: TaskHandle<_> = t1; + let _: TaskHandle<_> = t2; + let _: TaskHandle<_> = t3; } #[test] @@ -118,7 +105,7 @@ fn test_multiple_dependencies() { let sum = dag.add_task(AddTask).depends_on((a, b)); // Should return a TaskHandle - let _: TaskHandle = sum; + let _: TaskHandle<_> = sum; } #[test] diff --git a/src/deps.rs b/src/deps.rs index 5d70b36..a92c796 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -50,7 +50,7 @@ impl DepsTuple<()> for () { } // Implementation for single dependency (all forms) -impl DepsTuple for T +impl DepsTuple<(T::Output,)> for T where T: IntoNodeRef, { diff --git a/src/deps/tests.rs b/src/deps/tests.rs index a22e328..8d02881 100644 --- a/src/deps/tests.rs +++ b/src/deps/tests.rs @@ -207,7 +207,7 @@ fn test_deps_handle_ref_explicit() { // Create a reference and explicitly call to_node_ids let handle_ref = &handle; - let node_ids = DepsTuple::::to_node_ids(handle_ref); + let node_ids = DepsTuple::<(i32,)>::to_node_ids(handle_ref); assert_eq!(node_ids.len(), 1); assert_eq!(node_ids[0], NodeId(77)); diff --git a/src/extract.rs b/src/extract.rs index a338e12..f11c262 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -5,8 +5,8 @@ //! **NOTE**: This module is now primarily for internal use. The `ExtractInput` trait is only //! used by the internal `task_fn` test helper function. //! -//! **For regular tasks**: The `#[task]` macro generates inline `extract_and_run()` methods with -//! type-specific extraction logic. This means **ANY type** implementing `Clone + Send + Sync` +//! **For regular tasks**: The `#[task]` macro generates inline `run()` methods with +//! type-specific extraction logic. This means **ANY type** implementing `Send + Sync` //! works automatically without needing `ExtractInput` implementations! //! //! # How it works @@ -15,8 +15,8 @@ //! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones). //! //! **Modern approach (via `#[task]` macro)**: -//! - The macro generates custom extraction logic inline in `extract_and_run()` -//! - Works with ANY type (just needs `Clone + Send + Sync`) +//! - The macro generates custom extraction logic inline in `run()` +//! - Works with ANY type (just needs `Send + Sync + 'static`) //! - No trait implementations needed! //! //! **Legacy approach (via `ExtractInput` trait)**: @@ -32,27 +32,30 @@ //! //! This is standard practice in Rust for working with tuples of different arities. -use std::any::Any; -use std::sync::Arc; +use std::{collections::HashMap, hash::Hash, sync::Arc}; -/// Helper trait for async input extraction from list of dependencies. +use crate::TaskInput; + +/// Helper trait for 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. -/// -/// NOTE: This trait is now primarily used by the internal `task_fn` test helper. -/// The `#[task]` macro generates inline extraction logic via `extract_and_run()`, -/// which allows ANY type to work without requiring ExtractInput implementations. #[allow(dead_code)] -pub(crate) trait ExtractInput: Sized + Clone { - fn extract_from_deps( - deps: Vec>, - ) -> impl std::future::Future> + Send; +pub trait ExtractInput: Sized + Clone { + type Input: Send; + type Retv<'input>; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, Self::Input>, + ) -> Result, String>; } // Unit type - no dependencies impl ExtractInput for () { - async fn extract_from_deps(_deps: Vec>) -> Result { + type Input = (); + type Retv<'input> = (); + + fn extract_from_task_input(_input: TaskInput<'_, Self>) -> Result { Ok(()) } } @@ -65,15 +68,10 @@ macro_rules! impl_extract_single { ($($t:ty),+) => { $( impl ExtractInput for $t { - async fn extract_from_deps(mut dependencies: Vec>) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_value = dependencies.pop() - .unwrap() - .downcast::<$t>() - .map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<$t>()))?; - Ok((*arc_value).clone()) + type Input = ($t,); + type Retv<'input> = &'input $t; + fn extract_from_task_input<'input>(input: TaskInput<'input, Self::Input>) -> Result, String> { + Ok(input.next().0) } } )+ @@ -90,24 +88,13 @@ where T: 'static + Clone + Send + Sync, E: 'static + Clone + Send + Sync, { - async fn extract_from_deps( - mut dependencies: Vec>, - ) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_value = dependencies - .pop() - .unwrap() - .downcast::>() - .map_err(|_| { - format!( - "Type mismatch: expected Arc>", - std::any::type_name::(), - std::any::type_name::() - ) - })?; - Ok((*arc_value).clone()) + type Input = (Self,); + type Retv<'input> = &'input Result; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, (Self,)>, + ) -> Result, String> { + Ok(input.next().0) } } @@ -116,23 +103,13 @@ impl ExtractInput for Option where T: 'static + Clone + Send + Sync, { - async fn extract_from_deps( - mut dependencies: Vec>, - ) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_value = dependencies - .pop() - .unwrap() - .downcast::>() - .map_err(|_| { - format!( - "Type mismatch: expected Arc>", - std::any::type_name::() - ) - })?; - Ok((*arc_value).clone()) + type Input = (Self,); + type Retv<'input> = &'input Option; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, (Self,)>, + ) -> Result, String> { + Ok(input.next().0) } } @@ -141,77 +118,43 @@ impl ExtractInput for Vec where T: 'static + Clone + Send + Sync, { - async fn extract_from_deps( - mut dependencies: Vec>, - ) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_value = dependencies - .pop() - .unwrap() - .downcast::>() - .map_err(|_| { - format!( - "Type mismatch: expected Arc>", - std::any::type_name::() - ) - })?; - Ok((*arc_value).clone()) + type Input = (Self,); + type Retv<'input> = &'input Vec; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, (Self,)>, + ) -> Result, String> { + Ok(input.next().0) } } // Implement ExtractInput for HashMap types -impl ExtractInput for std::collections::HashMap +impl ExtractInput for HashMap where - K: 'static + Clone + Send + Sync + Eq + std::hash::Hash, + K: 'static + Clone + Send + Sync + Eq + Hash, V: 'static + Clone + Send + Sync, { - async fn extract_from_deps( - mut dependencies: Vec>, - ) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_value = dependencies - .pop() - .unwrap() - .downcast::>() - .map_err(|_| { - format!( - "Type mismatch: expected Arc>", - std::any::type_name::(), - std::any::type_name::() - ) - })?; - Ok((*arc_value).clone()) + type Input = (Self,); + type Retv<'input> = &'input HashMap; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, (Self,)>, + ) -> Result, String> { + Ok(input.next().0) } } -// Implement ExtractInput for Arc types - SPECIAL CASE: just clone the Arc, don't unwrap! -// This allows zero-copy data sharing for users who wrap their data in Arc. impl ExtractInput for Arc where T: 'static + Clone + Send + Sync, { - async fn extract_from_deps( - mut dependencies: Vec>, - ) -> Result { - if dependencies.len() != 1 { - return Err(format!("Expected 1 dependency, got {}", dependencies.len())); - } - let arc_arc_value = dependencies - .pop() - .unwrap() - .downcast::>() - .map_err(|_| { - format!( - "Type mismatch: expected Arc>", - std::any::type_name::() - ) - })?; - // Return the inner Arc (clone is cheap - just refcount increment) - Ok(Arc::clone(&*arc_arc_value)) + type Input = (Self,); + type Retv<'input> = &'input Arc; + + fn extract_from_task_input<'input>( + input: TaskInput<'input, (Self,)>, + ) -> Result, String> { + Ok(input.next().0) } } @@ -228,35 +171,19 @@ where /// /// Therefore, no runtime validation is needed - if it compiles, the dependency count is correct. macro_rules! impl_extract_tuple { - ($($T:ident : $idx:tt),+) => { + ($($T:ident),+) => { impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) { - async fn extract_from_deps(dependencies: Vec>) -> Result { - // Verify we have the correct number of dependencies - let expected_count = [$($idx),+].len(); - if dependencies.len() != expected_count { - return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len())); - } + type Input = Self; + type Retv<'input> = ($(&'input $T,)+); - // Convert Vec to iterator so we can consume it - let mut iter = dependencies.into_iter(); + fn extract_from_task_input<'input>(input: TaskInput<'input, Self::Input>) -> Result, String> { + $( + let ($T, input): (&'input $T, _) = input.next(); + )+ - // 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 - let arc_results = ( - $( - 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>()))?, - )+ - ); + let _input: TaskInput<'input, ()> = input; - // Clone inner values from Arc (cheap for small types, necessary for ownership) - Ok(($( - (*arc_results.$idx).clone() - ,)+)) + Ok(($($T,)+)) } } }; @@ -265,16 +192,15 @@ macro_rules! impl_extract_tuple { // Generate ExtractInput implementations for tuples of size 2-8. // Each invocation creates an impl for a specific tuple size. // Supporting up to 8 elements covers the vast majority of use cases. -impl_extract_tuple!(A:0, B:1); -impl_extract_tuple!(A:0, B:1, C:2); -impl_extract_tuple!(A:0, B:1, C:2, D:3); -impl_extract_tuple!(A:0, B:1, C:2, D:3, E:4); -impl_extract_tuple!(A:0, B:1, C:2, D:3, E:4, F:5); -impl_extract_tuple!(A:0, B:1, C:2, D:3, E:4, F:5, G:6); -impl_extract_tuple!(A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7); +#[allow(non_snake_case)] +mod impls { + use super::*; -#[cfg(test)] -mod tests; - -#[cfg(test)] -mod coverage_tests; + impl_extract_tuple!(A, B); + impl_extract_tuple!(A, B, C); + impl_extract_tuple!(A, B, C, D); + impl_extract_tuple!(A, B, C, D, E); + impl_extract_tuple!(A, B, C, D, E, F); + impl_extract_tuple!(A, B, C, D, E, F, G); + impl_extract_tuple!(A, B, C, D, E, F, G, H); +} diff --git a/src/extract/coverage_tests.rs b/src/extract/coverage_tests.rs deleted file mode 100644 index 40768ba..0000000 --- a/src/extract/coverage_tests.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! Coverage tests for uncovered error paths in extract.rs -//! -//! 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_deps with mismatched types. - -use crate::extract::ExtractInput; -use std::any::Any; -use std::collections::HashMap; -use std::sync::Arc; - -#[tokio::test] -async fn test_result_type_mismatch_coverage() { - // Tests line 95 in extract.rs - Result type mismatch error message - // Create a dependency that provides the WRONG type (String instead of Result) - // Try to extract as Result - this will fail with type mismatch - let deps = vec![Arc::new("hello") as Arc]; - let result = Result::::extract_from_deps(deps).await; - - // Verify the error message contains the expected type information - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - - // The error should mention Result type mismatch (line 95) - assert!( - err_msg.contains("Result"), - "Expected type mismatch error mentioning Result, got: {}", - err_msg - ); - assert!( - err_msg.contains("Type mismatch"), - "Expected 'Type mismatch' in error, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_option_type_mismatch_coverage() { - // Tests line 124 in extract.rs - Option type mismatch error message - // Create a dependency that provides the WRONG type (i32 instead of Option) - // Try to extract as Option - this will fail with type mismatch - let deps = vec![Arc::new(42) as Arc]; - let result = Option::::extract_from_deps(deps).await; - - // Verify the error message contains the expected type information - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - - // The error should mention Option type mismatch (line 124) - assert!( - err_msg.contains("Option"), - "Expected type mismatch error mentioning Option, got: {}", - err_msg - ); - assert!( - err_msg.contains("Type mismatch"), - "Expected 'Type mismatch' in error, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_vec_type_mismatch_coverage() { - // Tests line 152 in extract.rs - Vec type mismatch error message - // Create a dependency that provides the WRONG type (String instead of Vec) - // Try to extract as Vec - this will fail with type mismatch - let deps = vec![Arc::new("not a vec".to_string()) as Arc]; - let result = Vec::::extract_from_deps(deps).await; - - // Verify the error message contains the expected type information - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - - // The error should mention Vec type mismatch (line 152) - assert!( - err_msg.contains("Vec"), - "Expected type mismatch error mentioning Vec, got: {}", - err_msg - ); - assert!( - err_msg.contains("Type mismatch"), - "Expected 'Type mismatch' in error, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_hashmap_type_mismatch_coverage() { - // Tests line 181 in extract.rs - HashMap type mismatch error message - // Create a dependency that provides the WRONG type (Vec instead of HashMap) - // Try to extract as HashMap - this will fail with type mismatch - let deps = vec![Arc::new(vec![1, 2, 3]) as Arc]; - let result = HashMap::::extract_from_deps(deps).await; - - // Verify the error message contains the expected type information - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - - // The error should mention HashMap type mismatch (line 181) - assert!( - err_msg.contains("HashMap"), - "Expected type mismatch error mentioning HashMap, got: {}", - err_msg - ); - assert!( - err_msg.contains("Type mismatch"), - "Expected 'Type mismatch' in error, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_arc_type_mismatch_coverage() { - // Tests line 211 in extract.rs - Arc type mismatch error message - // Arc is special: it expects Arc> from channel - // Create a dependency that provides Arc instead of Arc> - let deps = vec![Arc::new("not double arc".to_string()) as Arc]; - - // Try to extract as Arc - this will fail with type mismatch - // because it expects Arc> in the channel - let result = Arc::::extract_from_deps(deps).await; - - // Verify the error message contains the expected type information - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - - // The error should mention Arc type mismatch (line 211) - assert!( - err_msg.contains("Arc"), - "Expected type mismatch error mentioning Arc, got: {}", - err_msg - ); - assert!( - err_msg.contains("Type mismatch"), - "Expected 'Type mismatch' in error, got: {}", - err_msg - ); -} - -// Additional tests for better coverage of complex scenarios - -#[tokio::test] -async fn test_result_with_wrong_ok_type() { - // Test Result with wrong Ok type - // Try to extract as Result - wrong Ok type - let deps = vec![Arc::new(Ok::<_, String>("test".to_string())) as Arc]; - let result = Result::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Result") && err_msg.contains("Type mismatch"), - "Expected Result type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_result_with_wrong_err_type() { - // Test Result with wrong Err type - // Try to extract as Result - wrong Err type - let deps = vec![Arc::new(Err::(42)) as Arc]; - let result = Result::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Result") && err_msg.contains("Type mismatch"), - "Expected Result type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_option_with_wrong_inner_type() { - // Test Option with wrong inner type - // Try to extract as Option - wrong inner type - let deps = vec![Arc::new(Some("test".to_string())) as Arc]; - let result = Option::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Option") && err_msg.contains("Type mismatch"), - "Expected Option type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_vec_with_wrong_element_type() { - // Test Vec with wrong element type - // Try to extract as Vec - wrong element type - let deps = vec![Arc::new(vec!["a".to_string(), "b".to_string()]) as Arc]; - let result = Vec::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Vec") && err_msg.contains("Type mismatch"), - "Expected Vec type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_hashmap_with_wrong_key_type() { - // Test HashMap with wrong key type - let mut map = HashMap::new(); - map.insert(1, "one".to_string()); - // Try to extract as HashMap - wrong key type - let deps = vec![Arc::new(map) as Arc]; - let result = HashMap::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("HashMap") && err_msg.contains("Type mismatch"), - "Expected HashMap type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_hashmap_with_wrong_value_type() { - // Test HashMap with wrong value type - let mut map = HashMap::new(); - map.insert("key".to_string(), 42); - // Try to extract as HashMap - wrong value type - let deps = vec![Arc::new(map) as Arc]; - let result = HashMap::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("HashMap") && err_msg.contains("Type mismatch"), - "Expected HashMap type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_arc_with_wrong_inner_type() { - // Test Arc with completely wrong inner type - // Send Arc but expect Arc - // Try to extract as Arc - wrong inner type - let deps = vec![Arc::new(Arc::new(Arc::new(42))) as Arc]; - let result = Arc::::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Arc") && err_msg.contains("Type mismatch"), - "Expected Arc type mismatch, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_primitive_type_mismatch_for_completeness() { - // Test primitive type mismatch (already covered in existing tests but good for completeness) - // Try to extract as i32 - type mismatch - let deps = vec![Arc::new("not an i32".to_string()) as Arc]; - let result = i32::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Type mismatch"), - "Expected type mismatch error, got: {}", - err_msg - ); -} - -#[tokio::test] -async fn test_complex_nested_result_option() { - // Test complex nested type: send Result, expect Result, String> - // Try to extract as Result, String> - nested type mismatch - let deps = - vec![Arc::new(Ok::("test".to_string())) as Arc]; - let result = Result::, String>::extract_from_deps(deps).await; - - assert!(result.is_err()); - let err_msg = result.unwrap_err(); - assert!( - err_msg.contains("Result") && err_msg.contains("Type mismatch"), - "Expected Result type mismatch, got: {}", - err_msg - ); -} diff --git a/src/extract/tests.rs b/src/extract/tests.rs deleted file mode 100644 index f8eef37..0000000 --- a/src/extract/tests.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! Unit tests for extract module - -#![allow(dead_code)] // Test helpers generate unused code via macro - -use crate::extract::ExtractInput; -use std::any::Any; -use std::sync::Arc; - -// Test task for error scenarios -#[allow(dead_code)] -struct TestTask { - value: i32, -} - -#[crate::task] -#[allow(dead_code)] -impl TestTask { - #[cfg(not(tarpaulin_include))] - async fn run(&self) -> i32 { - self.value - } -} - -#[allow(dead_code)] -struct TestTaskWithInput; - -#[crate::task] -#[allow(dead_code)] -impl TestTaskWithInput { - #[cfg(not(tarpaulin_include))] - async fn run(input: &i32) -> i32 { - input * 2 - } -} - -#[tokio::test] -async fn test_extract_result_type_single_dependency_error() { - // Test Result with wrong dependency count - let deps = vec![ - Arc::new(Ok::(5)) as Arc, - Arc::new(Err::("test".to_string())) as Arc, - ]; // Two dependencies when expecting 1 - - let result = Result::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); -} - -#[tokio::test] -async fn test_extract_result_type_success() { - // Test Result successful extraction - - let deps = vec![Arc::new(Ok::(42)) as Arc]; - let result = Result::::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), Ok(42)); -} - -#[tokio::test] -async fn test_extract_option_type_single_dependency_error() { - // Test Option with wrong dependency count - let deps = vec![ - Arc::new(None::) as Arc, - Arc::new(None::) as Arc, - Arc::new(None::) as Arc, - ]; // Three dependencies when expecting 1 - - let result = Option::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Expected 1 dependency, got 3")); -} - -#[tokio::test] -async fn test_extract_option_type_success() { - // Test Option successful extraction - let deps = vec![Arc::new(Some(99)) as Arc]; - let result = Option::::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), Some(99)); -} - -#[tokio::test] -async fn test_extract_vec_type_single_dependency_error() { - // Test Vec with wrong dependency count - let deps = vec![]; // Zero dependencies when expecting 1 - - let result = Vec::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Expected 1 dependency, got 0")); -} - -#[tokio::test] -async fn test_extract_vec_type_success() { - // Test Vec successful extraction - let deps = vec![Arc::new(vec![1, 2, 3]) as Arc]; - let result = Vec::::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec![1, 2, 3]); -} - -#[tokio::test] -async fn test_extract_primitive_single_dependency_error() { - // Test primitive types with wrong dependency count - let deps = vec![ - Arc::new(0) as Arc, - Arc::new(0) as Arc, - ]; // Two dependencies when expecting 1 - - 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![ - Arc::new(String::default()) as Arc, - Arc::new(String::default()) as Arc, - ]; - 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![ - Arc::new(0.0) as Arc, - Arc::new(0.0) as Arc, - ]; - let result = f64::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Expected 1 dependency, got 2")); -} - -#[tokio::test] -async fn test_extract_unit_type() { - // Test that unit type always succeeds regardless of dependencies - let deps = vec![]; - let result = <()>::extract_from_deps(deps).await; - assert!(result.is_ok()); - - // Even with dependencies (ignored), unit type succeeds - let deps = vec![ - Arc::new(0) as Arc, - Arc::new(0) as Arc, - ]; - let result = <()>::extract_from_deps(deps).await; - assert!(result.is_ok()); -} - -#[tokio::test] -async fn test_extract_tuple_basic() { - // Test tuple extraction - let deps = vec![ - Arc::new(42) as Arc, - Arc::new(100) as Arc, - ]; - - // Extract a tuple (i32, i32) - 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_type_mismatch() { - // Test type mismatch during downcast - // Try to extract as wrong type - put wrong receiver type in deps - let deps = vec![Arc::new("hello".to_string()) as Arc]; - let result = i32::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -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 deps = vec![Arc::new($val) as Arc]; - let result = <$t>::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), $val); - }; - } - - test_type!(i8, 42i8); - test_type!(i16, 42i16); - test_type!(i32, 42i32); - test_type!(i64, 42i64); - test_type!(String, "test".to_string()); -} - -#[tokio::test] -async fn test_extract_larger_tuples() { - // Test extraction for larger tuple sizes - let deps = vec![ - Arc::new(0) as Arc, - Arc::new(1) as Arc, - Arc::new(2) as Arc, - Arc::new(3) as Arc, - Arc::new(4) as Arc, - ]; - - // Extract a 5-element tuple - let result = <(i32, i32, i32, i32, i32)>::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), (0, 1, 2, 3, 4)); -} - -#[tokio::test] -async fn test_extract_arc_type() { - // Test Arc extraction - let test_arc = Arc::new("test".to_string()); - let test_arc_clone = test_arc.clone(); - - let deps = vec![Arc::new(test_arc) as Arc]; - let result = Arc::::extract_from_deps(deps).await; - assert!(result.is_ok()); - assert_eq!(*result.unwrap(), *test_arc_clone); -} - -// Error path tests for ExtractInput implementations -use std::collections::HashMap; - -#[tokio::test] -async fn test_hashmap_extract_wrong_dependency_count() { - let deps = vec![ - Arc::new(HashMap::::default()) as Arc, - Arc::new(HashMap::::default()) as Arc, - ]; - - let result = HashMap::::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 deps = vec![Arc::new("wrong type".to_string()) as Arc]; - let result = HashMap::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -async fn test_result_extract_downcast_failure() { - let deps = vec![Arc::new("wrong".to_string()) as Arc]; - let result = Result::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -async fn test_option_extract_downcast_failure() { - let deps = vec![Arc::new("wrong".to_string()) as Arc]; - let result = Option::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -async fn test_vec_extract_downcast_failure() { - let deps = vec![Arc::new("wrong".to_string()) as Arc]; - let result = Vec::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -async fn test_arc_extract_wrong_dependency_count() { - let deps = vec![]; - let result = Arc::::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 deps = vec![Arc::new("wrong".to_string()) as Arc]; - let result = Arc::::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Type mismatch")); -} - -#[tokio::test] -async fn test_tuple_wrong_dependency_count() { - let deps = vec![ - Arc::new(0) as Arc, - Arc::new(0) as Arc, - Arc::new(0) as Arc, - ]; - - let result = <(i32, i32)>::extract_from_deps(deps).await; - assert!(result.is_err()); - assert!(result - .unwrap_err() - .contains("Expected 2 dependencies, got 3")); -} diff --git a/src/lib.rs b/src/lib.rs index 938ecdf..d1d9726 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! system. The public API is fully type-safe with no runtime type errors. Internal execution //! uses type erasure for heterogeneous task storage, but this is never exposed to users. //! - **Works with ANY type**: Custom types work automatically with the `#[task]` macro—just -//! implement `Clone + Send + Sync`, no trait implementations needed! The macro generates +//! implement `Send + Sync`, no trait implementations needed! The macro generates //! type-specific extraction logic for seamless integration. //! - **Runtime-agnostic**: Works with any async runtime (Tokio, async-std, smol, Embassy, etc.) //! - **Type-state pattern**: The API guides you with compile-time errors if you wire dependencies @@ -442,15 +442,13 @@ //! //! ## Custom Types //! -//! **dagx works with ANY type automatically!** As long as your type implements `Clone + Send + Sync + 'static`, +//! **dagx works with ANY type automatically!** As long as your type is `Send + Sync + 'static`, //! the `#[task]` macro generates the necessary extraction logic: //! //! ```no_run //! # use dagx::{task, DagRunner, Task}; //! # use futures::FutureExt; //! -//! // Just derive Clone - that's all you need! -//! #[derive(Clone)] //! struct User { //! name: String, //! age: u32, @@ -645,7 +643,7 @@ //! } //! //! // Downstream tasks receive &Vec as normal -//! // Behind the scenes: Arc> is cloned cheaply, then inner Vec extracted +//! // Behind the scenes: Arc> is cloned cheaply, then a reference to the inner Vec is extracted //! struct ProcessData; //! #[task] //! impl ProcessData { @@ -791,7 +789,7 @@ mod types; pub use builder::TaskBuilder; pub use error::{DagError, DagResult}; pub use runner::DagRunner; -pub use task::Task; +pub use task::{Task, TaskInput}; pub use types::{Pending, TaskHandle}; // Re-export the procedural macro diff --git a/src/node.rs b/src/node.rs index bd9da25..d566a8b 100644 --- a/src/node.rs +++ b/src/node.rs @@ -12,8 +12,7 @@ use std::pin::Pin; use std::sync::Arc; use crate::error::DagError; -use crate::task::Task; -use crate::types::NodeId; +use crate::task::{Task, TaskInput}; /// Type-erased async execution result (Arc-wrapped for efficient fanout) type ExecuteFuture = @@ -52,37 +51,29 @@ pub(crate) trait ExecutableNode: Send { /// /// The output is also returned from execute_with_deps for central storage. pub(crate) struct TypedNode { - pub(crate) id: NodeId, pub(crate) task: T, } impl TypedNode { - pub(crate) fn new(id: NodeId, task: T) -> Self { - Self { id, task } + pub(crate) fn new(task: T) -> Self { + Self { task } } } impl ExecutableNode for TypedNode where - T::Input: 'static + Clone, - T::Output: 'static + Clone, + T::Input: 'static, + T::Output: 'static, { fn execute_with_deps( self: Box, dependencies: Vec>, ) -> ExecuteFuture { Box::pin(async move { - // Use the task's extract_and_run method which has inline extraction logic + // Use the task's 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(dependencies).await { - Ok(output) => output, - Err(_msg) => { - // 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 }); - } - }; + let output = self.task.run(TaskInput::new(dependencies.iter())).await; // Arc Wrapping Strategy: // Wrap output in Arc ONCE for efficient fan-out sharing. diff --git a/src/node/tests.rs b/src/node/tests.rs index bc0b25f..9e946d5 100644 --- a/src/node/tests.rs +++ b/src/node/tests.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use crate::node::{ExecutableNode, TypedNode}; -use crate::types::NodeId; // Simple test task struct TestTask { @@ -26,29 +25,11 @@ impl TaskWithDependency { } } -#[tokio::test] -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 dependencies - let dependencies = vec![]; - - let result = node.execute_with_deps(dependencies).await; - assert!(result.is_err()); - - if let Err(crate::error::DagError::InvalidDependency { task_id }) = result { - assert_eq!(task_id, 0); - } else { - panic!("Expected InvalidDependency error, got: {:?}", result); - } -} - #[tokio::test] 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)); + let dependency_node = Box::new(TypedNode::new(TestTask { value: 21 })); + let dependent_node = Box::new(TypedNode::new(TaskWithDependency)); // Execute dependency node with sender let dep_dependencies = vec![]; @@ -71,7 +52,7 @@ async fn test_execute_with_deps_success() { #[tokio::test] 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 })); + let node = Box::new(TypedNode::new(TestTask { value: 42 })); // No senders means this is a sink node let dependencies = vec![]; @@ -88,7 +69,7 @@ async fn test_execute_with_deps_sink_node() { #[tokio::test] 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 }); + let node = TypedNode::new(TestTask { value: 42 }); // Execute with senders (non-sink node) let node = Box::new(node); diff --git a/src/runner.rs b/src/runner.rs index faa9da9..339b81e 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -199,8 +199,8 @@ impl DagRunner { pub fn add_task(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending> where Tk: Task + Sync + 'static, - Tk::Input: 'static + Clone, - Tk::Output: 'static + Clone, + Tk::Input: 'static, + Tk::Output: 'static, { let id = self.alloc_id(); @@ -211,7 +211,7 @@ impl DagRunner { "adding task to DAG" ); - let node = TypedNode::new(id, task); + let node = TypedNode::new(task); self.nodes.lock().push(Some(Box::new(node))); self.edges.lock().insert(id, Vec::new()); self.dependents.lock().insert(id, Vec::new()); @@ -519,6 +519,7 @@ impl DagRunner { /// # Behavior /// /// All task outputs are stored after execution and can be retrieved via get(). + /// Each task's output can only be taken once, after which it will return `DagError::ResultNotFound`. /// /// # Errors /// @@ -557,26 +558,26 @@ impl DagRunner { /// assert_eq!(dag.get(task).unwrap(), 42); /// # }; /// ``` - pub fn get(&self, handle: H) -> DagResult + pub fn get(&self, handle: H) -> DagResult where H: Into>, { let handle: TaskHandle = handle.into(); - let outputs = self.outputs.lock(); + let mut outputs = self.outputs.lock(); - let arc_output = outputs.get(&handle.id).ok_or(DagError::ResultNotFound { + let arc_output = outputs.remove(&handle.id).ok_or(DagError::ResultNotFound { task_id: handle.id.0, })?; - // Downcast Arc to Arc, then clone the inner value - // The Arc itself is stored for efficient sharing, but get() clones the data - Arc::clone(arc_output) + // Downcast Arc to Arc + let output = arc_output .downcast::() - .map(|arc| (*arc).clone()) .map_err(|_| DagError::TypeMismatch { expected: std::any::type_name::(), found: "unknown", - }) + })?; + + Ok(Arc::into_inner(output).unwrap()) } #[inline] @@ -673,6 +674,3 @@ impl DagRunner { #[cfg(test)] mod tests; - -#[cfg(test)] -mod coverage_tests; diff --git a/src/runner/coverage_tests.rs b/src/runner/coverage_tests.rs deleted file mode 100644 index c501fe8..0000000 --- a/src/runner/coverage_tests.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! Coverage tests for runner module - specifically targeting uncovered error paths -//! -//! These tests ensure we hit the type mismatch error path in get() (line 601) - -use crate::error::DagError; -use crate::runner::DagRunner; -use crate::types::TaskHandle; -use futures::FutureExt; -use std::collections::HashMap; - -#[tokio::test] -async fn test_get_type_mismatch_error() { - // Test the type mismatch error path in get() - line 601 in runner.rs - // This occurs when Arc downcast fails due to type mismatch - - use crate::task_fn; - - let dag = DagRunner::new(); - - // Task outputs i32 - let source = dag.add_task(task_fn(|_: ()| async { 42i32 })); - - 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 - let wrong_handle: TaskHandle = TaskHandle { - id: source.id, - _phantom: std::marker::PhantomData, - }; - - let result = dag.get(wrong_handle); - - assert!(result.is_err()); - let err = result.unwrap_err(); - match err { - DagError::TypeMismatch { expected, found } => { - assert_eq!(expected, std::any::type_name::()); - assert_eq!(found, "unknown"); // This covers line 601! - } - _ => panic!("Expected TypeMismatch error, got: {:?}", err), - } -} - -#[tokio::test] -async fn test_get_type_mismatch_i32_to_vec() { - // Different type mismatch: i32 to Vec - use crate::task_fn; - - let dag = DagRunner::new(); - - // Task outputs i32 - let int_task = dag.add_task(task_fn(|_: ()| async { 123i32 })); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Try to get as Vec - let wrong_handle: TaskHandle> = TaskHandle { - id: int_task.id, - _phantom: std::marker::PhantomData, - }; - - let result = dag.get(wrong_handle); - - assert!(result.is_err()); - match result.unwrap_err() { - DagError::TypeMismatch { expected, found } => { - assert_eq!(expected, std::any::type_name::>()); - assert_eq!(found, "unknown"); - } - _ => panic!("Expected TypeMismatch error"), - } -} - -#[tokio::test] -async fn test_get_type_mismatch_struct_to_hashmap() { - // Complex type mismatch: custom struct to HashMap - - #[derive(Clone)] - #[allow(dead_code)] - struct CustomOutput { - value: i32, - } - - struct CustomTask; - - #[crate::task] - impl CustomTask { - async fn run(&self) -> CustomOutput { - CustomOutput { value: 42 } - } - } - - let dag = DagRunner::new(); - let task_handle = dag.add_task(CustomTask); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Try to get as HashMap - let wrong_handle: TaskHandle> = TaskHandle { - id: task_handle.id, - _phantom: std::marker::PhantomData, - }; - - let result = dag.get(wrong_handle); - - assert!(result.is_err()); - match result.unwrap_err() { - DagError::TypeMismatch { expected, found } => { - assert!(expected.contains("HashMap")); - assert_eq!(found, "unknown"); - } - _ => panic!("Expected TypeMismatch error"), - } -} - -#[tokio::test] -async fn test_get_type_mismatch_vec_different_types() { - // Type mismatch between Vec and Vec - use crate::task_fn; - - let dag = DagRunner::new(); - - // Task outputs Vec - let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Try to get as Vec - let wrong_handle: TaskHandle> = TaskHandle { - id: vec_task.id, - _phantom: std::marker::PhantomData, - }; - - let result = dag.get(wrong_handle); - - assert!(result.is_err()); - match result.unwrap_err() { - DagError::TypeMismatch { expected, found } => { - assert_eq!(expected, std::any::type_name::>()); - assert_eq!(found, "unknown"); - } - _ => panic!("Expected TypeMismatch error"), - } -} - -#[tokio::test] -async fn test_get_type_mismatch_option_types() { - // Type mismatch with Option types - use crate::task_fn; - - let dag = DagRunner::new(); - - // Task outputs Option - let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) })); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Try to get as Option - let wrong_handle: TaskHandle> = TaskHandle { - id: opt_task.id, - _phantom: std::marker::PhantomData, - }; - - let result = dag.get(wrong_handle); - - assert!(result.is_err()); - match result.unwrap_err() { - DagError::TypeMismatch { expected, found } => { - assert_eq!(expected, std::any::type_name::>()); - assert_eq!(found, "unknown"); - } - _ => panic!("Expected TypeMismatch error"), - } -} - -#[tokio::test] -async fn test_multiple_type_mismatches_same_dag() { - // Test multiple type mismatches in the same DAG - use crate::task_fn; - - let dag = DagRunner::new(); - - // Create tasks with different output types - let int_task = dag.add_task(task_fn(|_: ()| async { 100i32 })); - 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).map(Result::unwrap)) - .await - .unwrap(); - - // Try to get int_task as String - let wrong_int_handle: TaskHandle = TaskHandle { - id: int_task.id, - _phantom: std::marker::PhantomData, - }; - assert!(matches!( - dag.get(wrong_int_handle).unwrap_err(), - DagError::TypeMismatch { found, .. } if found == "unknown" - )); - - // Try to get string_task as bool - let wrong_string_handle: TaskHandle = TaskHandle { - id: string_task.id, - _phantom: std::marker::PhantomData, - }; - assert!(matches!( - dag.get(wrong_string_handle).unwrap_err(), - DagError::TypeMismatch { found, .. } if found == "unknown" - )); - - // Try to get bool_task as i32 - let wrong_bool_handle: TaskHandle = TaskHandle { - id: bool_task.id, - _phantom: std::marker::PhantomData, - }; - assert!(matches!( - dag.get(wrong_bool_handle).unwrap_err(), - DagError::TypeMismatch { found, .. } if found == "unknown" - )); - - // Verify correct types still work - assert_eq!(dag.get(int_task).unwrap(), 100); - assert_eq!(dag.get(string_task).unwrap(), "hello"); - assert!(dag.get(bool_task).unwrap()); -} diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 4ffae00..ddefb43 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -185,27 +185,6 @@ fn test_add_task_increments_id() { assert_eq!(dag.nodes.lock().len(), 3); } -#[tokio::test] -async fn test_multiple_get_calls() { - init_tracing(); - // Test that get() can be called multiple times for the same handle - let dag = DagRunner::new(); - let handle: TaskHandle<_> = dag.add_task(TestTask { value: 100 }).into(); - - 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(); - let result2 = dag.get(handle).unwrap(); - let result3 = dag.get(handle).unwrap(); - - assert_eq!(result1, 100); - assert_eq!(result2, 100); - assert_eq!(result3, 100); -} - #[tokio::test] async fn test_invalid_node_id_in_get() { init_tracing(); diff --git a/src/task.rs b/src/task.rs index 32883b0..604c2aa 100644 --- a/src/task.rs +++ b/src/task.rs @@ -3,16 +3,18 @@ //! 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, sync::Arc}; +use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}; + +use crate::extract::ExtractInput; /// A unit of async work with typed inputs and outputs. /// /// Use the [`task`](crate::task) macro to implement this trait. The macro automatically /// derives `Input` and `Output` types from your `run()` method signature and generates -/// type-specific extraction logic in `extract_and_run()`. +/// type-specific extraction logic in `run()`. /// /// **Custom types work automatically!** As long as your output type implements -/// `Clone + Send + Sync + 'static`, the macro handles everything. No trait implementations needed! +/// `Send + Sync + 'static`, the macro handles everything. No trait implementations needed! /// /// # Task Patterns /// @@ -96,18 +98,82 @@ pub trait Task: Send { type Input: Send; type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing - fn run(self, input: Self::Input) -> impl Future + Send; + fn run(self, input: TaskInput) -> impl Future + Send; +} - /// 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 - /// type to work without requiring trait implementations. - #[doc(hidden)] - fn extract_and_run( - self, - dependencies: Vec>, - ) -> impl Future> + Send; +pub struct TaskInput<'inputs, Input: Send> { + inputs: Iter<'inputs, Arc>, + phantom: PhantomData, +} + +impl<'inputs, Input: Send> TaskInput<'inputs, Input> { + pub(crate) fn new(inputs: Iter<'inputs, Arc>) -> Self { + Self { + inputs, + phantom: PhantomData, + } + } +} + +macro_rules! impl_task_input { + ($First:ident $(, $T:ident)*) => { + impl<'inputs, $First: 'static + Send, $($T: 'static + Send),*> TaskInput<'inputs, ($First, $($T,)*)> { + #[must_use] + pub fn next(mut self) -> (&'inputs $First, TaskInput<'inputs, ($($T,)*)>) { + let value = self.inputs.next().and_then(|value| value.downcast_ref()).unwrap(); + let next_inputs = TaskInput { + inputs: self.inputs, + phantom: PhantomData, + }; + (value, next_inputs) + } + } + }; +} + +impl_task_input!(A); +impl_task_input!(A, B); +impl_task_input!(A, B, C); +impl_task_input!(A, B, C, D); +impl_task_input!(A, B, C, D, E); +impl_task_input!(A, B, C, D, E, F); +impl_task_input!(A, B, C, D, E, F, G); +impl_task_input!(A, B, C, D, E, F, G, H); + +impl TaskInput<'static, ()> { + pub fn empty() -> Self { + Self { + inputs: [].iter(), + phantom: PhantomData, + } + } +} + +/// **INTERNAL USE ONLY**: This struct is for testing purposes only and should +/// not be used in production code. Use the `#[task]` macro instead. +#[doc(hidden)] +pub struct TaskFn +where + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + O: Send, +{ + f: F, + _phantom: std::marker::PhantomData O>, +} + +impl Task for TaskFn +where + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + O: Send + Sync, +{ + type Input = I::Input; + type Output = O; + + async fn run(mut self, input: TaskInput<'_, I::Input>) -> O { + (self.f)(I::extract_from_task_input(input).unwrap()) + } } /// Convenience function to create a task from a closure. @@ -115,51 +181,13 @@ pub trait Task: Send { /// **INTERNAL USE ONLY**: This function is for testing purposes only and should /// not be used in production code. Use the `#[task]` macro instead. #[doc(hidden)] -pub fn task_fn(f: F) -> impl Task +pub fn task_fn(f: F) -> TaskFn where - F: FnMut(I) -> Fut + Send + 'static, - Fut: Future + Send + 'static, - I: Send + crate::extract::ExtractInput + 'static, + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + I::Input: Send + Sync + 'static, O: Send + Sync + 'static, { - struct TaskFn - where - F: FnMut(I) -> Fut + Send, - Fut: Future + Send, - I: Send, - O: Send, - { - f: F, - _phantom: std::marker::PhantomData O>, - } - - impl Task for TaskFn - where - F: FnMut(I) -> Fut + Send, - Fut: Future + Send, - I: Send + crate::extract::ExtractInput, - O: Send + Sync, - { - type Input = I; - type Output = O; - - async fn run(mut self, input: I) -> O { - (self.f)(input).await - } - - async fn extract_and_run( - self, - dependencies: Vec>, - ) -> Result { - // For task_fn, we use ExtractInput since we don't have macro-generated extraction - let input = I::extract_from_deps(dependencies) - .await - .map_err(|e| format!("Failed to extract input: {}", e))?; - - Ok(self.run(input).await) - } - } - TaskFn { f, _phantom: std::marker::PhantomData, diff --git a/src/task/tests.rs b/src/task/tests.rs index 86e351d..99dbc38 100644 --- a/src/task/tests.rs +++ b/src/task/tests.rs @@ -1,6 +1,7 @@ //! Unit tests for task module -use crate::task::{task_fn, Task}; +use crate::task::{task_fn, Task, TaskInput}; +use std::any::Any; use std::future::Future; use std::pin::Pin; use std::sync::Arc; @@ -11,50 +12,71 @@ async fn test_task_fn_creation_and_execution() { // Test line 111 in task.rs - Task implementation for TaskFn // Create a simple task function - let task = task_fn(|input: i32| async move { input * 2 }); + let task = task_fn::(|input: &i32| input * 2); // Test the Task trait implementation - let result = task.run(42).await; + let result = task + .run(TaskInput::new( + [Arc::new(42) as Arc].iter(), + )) + .await; assert_eq!(result, 84); } #[tokio::test] async fn test_task_fn_with_unit_input() { // Test task_fn with unit input - let task = task_fn(|_: ()| async { 100 }); + let task = task_fn::<(), _, _>(|_: ()| 100); - let result = task.run(()).await; + let result = task + .run(TaskInput::new( + [Arc::new(()) as Arc].iter(), + )) + .await; assert_eq!(result, 100); } #[tokio::test] async fn test_task_fn_with_tuple_input() { // Test task_fn with tuple input - let task = task_fn(|(a, b): (i32, i32)| async move { a + b }); + let task = task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b); - let result = task.run((10, 20)).await; + let result = task + .run(TaskInput::new( + [ + Arc::new(10) as Arc, + Arc::new(20) as Arc, + ] + .iter(), + )) + .await; assert_eq!(result, 30); } #[tokio::test] async fn test_task_fn_with_string_input() { // Test task_fn with String input - let task = task_fn(|input: String| { - let len = input.len(); - async move { len } - }); + let task = task_fn::(|input: &String| input.len()); let test_string = String::from("hello"); - let result = task.run(test_string).await; + let result = task + .run(TaskInput::new( + [Arc::new(test_string) as Arc].iter(), + )) + .await; assert_eq!(result, 5); } #[tokio::test] async fn test_task_fn_with_complex_output() { // Test task_fn with complex output type - let task = task_fn(|input: i32| async move { vec![input, input * 2, input * 3] }); + let task = task_fn::(|&input: &i32| vec![input, input * 2, input * 3]); - let result = task.run(5).await; + let result = task + .run(TaskInput::new( + [Arc::new(5) as Arc].iter(), + )) + .await; assert_eq!(result, vec![5, 10, 15]); } @@ -62,9 +84,13 @@ async fn test_task_fn_with_complex_output() { async fn test_task_fn_stateful_closure() { // Test that task_fn can capture state let multiplier = 3; - let task = task_fn(move |input: i32| async move { input * multiplier }); + let task = task_fn::(move |input: &i32| input * multiplier); - let result = task.run(7).await; + let result = task + .run(TaskInput::new( + [Arc::new(7) as Arc].iter(), + )) + .await; assert_eq!(result, 21); } @@ -74,34 +100,30 @@ struct CustomTask { } impl Task for CustomTask { - type Input = i32; + type Input = (i32,); type Output = i32; #[allow(refining_impl_trait)] - fn run(mut self, input: Self::Input) -> Pin + Send>> { + fn run( + mut self, + input: TaskInput<'_, Self::Input>, + ) -> Pin + Send + '_>> { Box::pin(async move { - self.state += input; + self.state += input.next().0; self.state }) } - - #[allow(refining_impl_trait)] - fn extract_and_run( - self, - _dependencies: Vec>, - ) -> Pin> + Send>> { - Box::pin(async move { - // This is a test helper - just return an error - Err("extract_and_run not implemented for CustomTask (test helper)".to_string()) - }) - } } #[tokio::test] async fn test_custom_task_implementation() { let task = CustomTask { state: 10 }; - let result1 = task.run(5).await; + let result1 = task + .run(TaskInput::new( + [Arc::new(5) as Arc].iter(), + )) + .await; assert_eq!(result1, 15); // Can't call run again since task was consumed @@ -131,15 +153,21 @@ fn test_task_trait_bounds() { #[tokio::test] async fn test_task_fn_single_call() { // Test that task_fn works (can only call once due to ownership) - let task = task_fn(|x: i32| async move { x + 1 }); + let task = task_fn::(|x: &i32| x + 1); - assert_eq!(task.run(1).await, 2); + assert_eq!( + task.run(TaskInput::new( + [Arc::new(1) as Arc].iter() + )) + .await, + 2 + ); } #[tokio::test] async fn test_task_fn_with_result_type() { // Test task_fn returning Result - let task = task_fn(|x: i32| async move { + let task = task_fn::(|&x: &i32| { if x > 0 { Ok(x * 2) } else { @@ -147,30 +175,40 @@ async fn test_task_fn_with_result_type() { } }); - assert_eq!(task.run(5).await, Ok(10)); + assert_eq!( + task.run(TaskInput::new( + [Arc::new(5) as Arc].iter() + )) + .await, + Ok(10) + ); } #[tokio::test] async fn test_task_fn_with_option_type() { // Test task_fn returning Option - let task = task_fn(|x: i32| async move { - if x > 0 { - Some(x * 2) - } else { - None - } - }); + let task = task_fn::(|&x: &i32| if x > 0 { Some(x * 2) } else { None }); - assert_eq!(task.run(5).await, Some(10)); + assert_eq!( + task.run(TaskInput::new( + [Arc::new(5) as Arc].iter() + )) + .await, + Some(10) + ); } #[tokio::test] async fn test_task_fn_explicit_trait_call() { // Test explicit Task trait method call to cover line 121 - let task = task_fn(|x: i32| async move { x * 3 }); + let task = task_fn::(|x: &i32| x * 3); // Explicitly call the Task trait's run method - let result = Task::run(task, 10).await; + let result = Task::run( + task, + TaskInput::new([Arc::new(10) as Arc].iter()), + ) + .await; assert_eq!(result, 30); } diff --git a/src/types.rs b/src/types.rs index 4c6b115..0fab2c1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,7 +3,7 @@ //! This module defines the fundamental types used throughout the DAG system, //! including node identifiers, task handles, and type-state markers. -use crate::builder::{IsUnitType, TaskBuilder}; +use crate::builder::TaskBuilder; use crate::task::Task; /// Opaque node identifier @@ -63,10 +63,7 @@ pub struct Pending; // TaskBuilder can be converted to a TaskHandle ONLY if Input is () (unit type) // This enforces at compile time that tasks with non-unit inputs MUST call .depends_on() -impl<'a, Tk: Task, Deps> From> for TaskHandle -where - Tk::Input: IsUnitType, -{ +impl<'a, Tk: Task, Deps> From> for TaskHandle { fn from(node: TaskBuilder<'a, Tk, Deps>) -> Self { TaskHandle { id: node.id, diff --git a/src/types/coverage_tests.rs b/src/types/coverage_tests.rs index 49ed2e8..d5c0350 100644 --- a/src/types/coverage_tests.rs +++ b/src/types/coverage_tests.rs @@ -29,7 +29,7 @@ async fn test_unit_type_taskbuilder_to_handle_conversion() { // Explicitly convert to TaskHandle using From implementation // This triggers the From implementation at line 69-74, // specifically covering line 71 - let handle: TaskHandle = builder.into(); + let handle: TaskHandle<_> = builder.into(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -76,7 +76,7 @@ async fn test_unit_type_conversion_with_multiple_tasks() { // Convert each builder to handle explicitly let first_handle: TaskHandle = first_builder.into(); - let second_handle: TaskHandle = second_builder.into(); + let second_handle: TaskHandle<_> = second_builder.into(); let third_handle: TaskHandle = third_builder.into(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -116,7 +116,7 @@ async fn test_unit_struct_with_state() { let builder = dag.add_task(StatefulTask::new(21)); // Explicit conversion - triggers line 71 - let handle: TaskHandle = builder.into(); + let handle: TaskHandle<_> = builder.into(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -143,7 +143,6 @@ async fn test_taskbuilder_conversion_assignment() { // Conversion through type annotation and assignment // This also uses From implementation let handle: TaskHandle> = builder.into(); - let id = handle.id; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -151,13 +150,6 @@ async fn test_taskbuilder_conversion_assignment() { let result = dag.get(handle).unwrap(); assert_eq!(result, vec![1, 2, 3]); - - // Verify the ID was preserved correctly through conversion - let another_handle = TaskHandle::> { - id, - _phantom: std::marker::PhantomData, - }; - assert_eq!(dag.get(another_handle).unwrap(), vec![1, 2, 3]); } #[tokio::test] @@ -184,7 +176,7 @@ async fn test_mixed_unit_and_dependent_tasks() { // Source is unit-type, can be converted let source_builder = dag.add_task(Source); - let source_handle: TaskHandle = source_builder.into(); // Covers line 71 + let source_handle: TaskHandle<_> = source_builder.into(); // Covers line 71 // Doubler needs dependencies, so we use it differently let doubler = dag.add_task(Doubler).depends_on(source_handle); @@ -260,9 +252,9 @@ fn test_multiple_conversions_preserve_ids() { let id3 = builder3.id; // Convert all builders - let handle1: TaskHandle = builder1.into(); - let handle2: TaskHandle = builder2.into(); - let handle3: TaskHandle = builder3.into(); + let handle1: TaskHandle<_> = builder1.into(); + let handle2: TaskHandle<_> = builder2.into(); + let handle3: TaskHandle<_> = builder3.into(); // Verify IDs are preserved and sequential assert_eq!(handle1.id, id1); diff --git a/src/types/tests.rs b/src/types/tests.rs index 862121c..f1ac142 100644 --- a/src/types/tests.rs +++ b/src/types/tests.rs @@ -40,7 +40,7 @@ fn test_node_id_operations() { #[test] fn test_task_handle_clone() { - let handle: TaskHandle = TaskHandle { + let handle: TaskHandle<(i32,)> = TaskHandle { id: NodeId(42), _phantom: PhantomData, }; @@ -129,7 +129,7 @@ fn test_task_builder_from_conversion() { let builder_id = builder.id; // This conversion uses the From implementation - let handle: TaskHandle = builder.into(); + let handle: TaskHandle<_> = builder.into(); assert_eq!(handle.id, builder_id); } diff --git a/tests/boundaries/deep_chains.rs b/tests/boundaries/deep_chains.rs index 26d0826..8db76fb 100644 --- a/tests/boundaries/deep_chains.rs +++ b/tests/boundaries/deep_chains.rs @@ -11,12 +11,12 @@ async fn test_1000_level_deep_chain() -> DagResult<()> { // Test a linear chain of 1000 tasks let dag = DagRunner::new(); - let first = dag.add_task(task_fn(|_: ()| async { 0 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let mut current = first.into(); // Convert to TaskHandle for _i in 1..1000 { current = dag - .add_task(task_fn(move |prev: i32| async move { prev + 1 })) + .add_task(task_fn::(move |&prev: &i32| prev + 1)) .depends_on(current); } @@ -31,20 +31,20 @@ async fn test_deep_chain_with_branches() -> DagResult<()> { // Test a deep chain where each node has a side branch let dag = DagRunner::new(); - let first = dag.add_task(task_fn(|_: ()| async { 0 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let mut main_chain: dagx::TaskHandle = first.into(); let mut branches = Vec::new(); for i in 1..500 { // Main chain continues main_chain = dag - .add_task(task_fn(move |prev: i32| async move { prev + 1 })) + .add_task(task_fn::(move |&prev: &i32| prev + 1)) .depends_on(main_chain); // Create a branch at every 10th node if i % 10 == 0 { let branch = dag - .add_task(task_fn(move |main_val: i32| async move { main_val * 2 })) + .add_task(task_fn::(move |main_val: &i32| main_val * 2)) .depends_on(main_chain); branches.push(branch); } @@ -71,12 +71,12 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { let mut chain_ends = Vec::new(); for chain_id in 0..8 { - let first = dag.add_task(task_fn(move |_: ()| async move { chain_id * 1000 })); + let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000)); let mut current: dagx::TaskHandle = first.into(); for _ in 0..500 { current = dag - .add_task(task_fn(|prev: i32| async move { prev + 1 })) + .add_task(task_fn::(|&prev: &i32| prev + 1)) .depends_on(current); } @@ -85,8 +85,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> { // Add a final task that depends on all chains let final_task = dag - .add_task(task_fn( - |inputs: (i32, i32, i32, i32, i32, i32, i32, i32)| async move { + .add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32, i32), _, _>( + |inputs: (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| { inputs.0 + inputs.1 + inputs.2 @@ -126,28 +126,24 @@ async fn test_deep_chain_execution_order() -> DagResult<()> { let dag = DagRunner::new(); let order = Arc::new(AtomicUsize::new(0)); - let first = dag.add_task(task_fn({ + let first = dag.add_task(task_fn::<(), _, _>({ let order = order.clone(); move |_: ()| { let order = order.clone(); - async move { - assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0); - 0 - } + assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0); + 0 } })); let mut current: dagx::TaskHandle = first.into(); for expected in 1..100 { current = dag - .add_task(task_fn({ + .add_task(task_fn::({ let order = order.clone(); - move |prev: i32| { + move |&prev: &i32| { let order = order.clone(); - async move { - assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected); - prev + 1 - } + assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected); + prev + 1 } })) .depends_on(current); @@ -168,14 +164,16 @@ async fn test_deep_binary_tree() -> DagResult<()> { fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle { if depth == 0 { - let task = dag.add_task(task_fn(move |_: ()| async move { value })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value)); task.into() // Convert TaskBuilder to TaskHandle } else { let left = build_tree(dag, depth - 1, value * 2); let right = build_tree(dag, depth - 1, value * 2 + 1); - dag.add_task(task_fn(move |(l, r): (i32, i32)| async move { l + r })) - .depends_on((&left, &right)) + dag.add_task(task_fn::<(i32, i32), _, _>(move |(l, r): (&i32, &i32)| { + l + r + })) + .depends_on((&left, &right)) } } @@ -196,15 +194,15 @@ async fn test_fibonacci_chain() -> DagResult<()> { // Test a Fibonacci-like dependency pattern let dag = DagRunner::new(); - let f0_builder = dag.add_task(task_fn(|_: ()| async { 0i64 })); - let f1_builder = dag.add_task(task_fn(|_: ()| async { 1i64 })); + let f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64)); + let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64)); let mut prev2: dagx::TaskHandle = f0_builder.into(); let mut prev1: dagx::TaskHandle = f1_builder.into(); for _ in 2..50 { let next = dag - .add_task(task_fn(|(a, b): (i64, i64)| async move { a + b })) + .add_task(task_fn::<(i64, i64), _, _>(|(a, b): (&i64, &i64)| a + b)) .depends_on((&prev2, &prev1)); prev2 = prev1; @@ -225,12 +223,12 @@ async fn test_deep_chain_with_accumulator() -> DagResult<()> { let dag = DagRunner::new(); // Use a simpler accumulation pattern - just sum - let first = dag.add_task(task_fn(|_: ()| async { 0i64 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64)); let mut current: dagx::TaskHandle = first.into(); for i in 1..=200 { current = dag - .add_task(task_fn(move |sum: i64| async move { sum + i as i64 })) + .add_task(task_fn::(move |sum: &i64| sum + i as i64)) .depends_on(current); } @@ -247,8 +245,8 @@ async fn test_zigzag_dependencies() -> DagResult<()> { // Test a zigzag pattern: two chains that cross-reference each other let dag = DagRunner::new(); - let a_first = dag.add_task(task_fn(|_: ()| async { 1 })); - let b_first = dag.add_task(task_fn(|_: ()| async { 2 })); + let a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); + let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); let mut chain_a: dagx::TaskHandle = a_first.into(); let mut chain_b: dagx::TaskHandle = b_first.into(); @@ -257,19 +255,19 @@ async fn test_zigzag_dependencies() -> DagResult<()> { if i % 2 == 0 { // A depends on B chain_a = dag - .add_task(task_fn(move |b_val: i32| async move { b_val + 1 })) + .add_task(task_fn::(move |b_val: &i32| b_val + 1)) .depends_on(chain_b); } else { // B depends on A chain_b = dag - .add_task(task_fn(move |a_val: i32| async move { a_val + 2 })) + .add_task(task_fn::(move |a_val: &i32| a_val + 2)) .depends_on(chain_a); } } // Final task depends on both chains let final_task = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((&chain_a, &chain_b)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -286,12 +284,12 @@ async fn test_10000_level_chain_stress() -> DagResult<()> { // Ultimate deep chain test let dag = DagRunner::new(); - let first = dag.add_task(task_fn(|_: ()| async { 0u64 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0u64)); let mut current: dagx::TaskHandle = first.into(); for _ in 1..10_000 { current = dag - .add_task(task_fn(|prev: u64| async move { prev + 1 })) + .add_task(task_fn::(|prev: &u64| prev + 1)) .depends_on(current); } diff --git a/tests/boundaries/large_scale.rs b/tests/boundaries/large_scale.rs index 6e32c2b..542077c 100644 --- a/tests/boundaries/large_scale.rs +++ b/tests/boundaries/large_scale.rs @@ -16,12 +16,9 @@ async fn test_10000_independent_tasks() -> DagResult<()> { let mut tasks: Vec<_> = (0..10_000) .map(|i| { let counter = counter.clone(); - dag.add_task(task_fn(move |_: ()| { - let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::Relaxed); - i - } + dag.add_task(task_fn::<(), _, _>(move |_: ()| { + counter.fetch_add(1, Ordering::Relaxed); + i })) }) .collect(); @@ -48,13 +45,15 @@ async fn test_5000_node_pyramid() -> DagResult<()> { // Bottom layer: 2048 nodes - convert to TaskHandles let mut current_layer: Vec> = (0..2048) .map(|i| { - let task = dag.add_task(task_fn(move |_: ()| async move { - if i < 1024 { - 1 - } else { - 0 - } - })); + let task = dag.add_task(task_fn::<(), _, _>( + move |_: ()| { + if i < 1024 { + 1 + } else { + 0 + } + }, + )); task.into() }) .collect(); @@ -74,20 +73,20 @@ async fn test_5000_node_pyramid() -> DagResult<()> { let task = match deps.len() { 1 => dag - .add_task(task_fn(|a: i32| async move { a })) + .add_task(task_fn::(|&a: &i32| a)) .depends_on(deps[0]), 2 => dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| 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 }, + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(a, b, c): (&i32, &i32, &i32)| 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 - })) + .add_task(task_fn::<(i32, i32, i32, i32), _, _>( + |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d, + )) .depends_on((deps[0], deps[1], deps[2], deps[3])), _ => unreachable!(), }; @@ -98,7 +97,7 @@ async fn test_5000_node_pyramid() -> DagResult<()> { // Final layer combines the last 2 let root = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((¤t_layer[0], ¤t_layer[1])); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -119,13 +118,15 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> { // Bottom layer: 2048 nodes - convert to TaskHandles let mut current_layer: Vec> = (0..2048) .map(|i| { - let task = dag.add_task(task_fn(move |_: ()| async move { - if i < 1024 { - 1 - } else { - 0 - } - })); + let task = dag.add_task(task_fn::<(), _, _>( + move |_: ()| { + if i < 1024 { + 1 + } else { + 0 + } + }, + )); task.into() }) .collect(); @@ -145,20 +146,20 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> { let task = match deps.len() { 1 => dag - .add_task(task_fn(|a: i32| async move { a })) + .add_task(task_fn::(|&a: &i32| a)) .depends_on(deps[0]), 2 => dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| 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 }, + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(a, b, c): (&i32, &i32, &i32)| 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 - })) + .add_task(task_fn::<(i32, i32, i32, i32), _, _>( + |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d, + )) .depends_on((deps[0], deps[1], deps[2], deps[3])), _ => unreachable!(), }; @@ -169,7 +170,7 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> { // Final layer combines the last 2 let root = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((¤t_layer[0], ¤t_layer[1])); dag.run(|fut| fut).await?; @@ -189,7 +190,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> { // Create 1000 source tasks let sources: Vec<_> = (0..1000) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { i % 10 })) + dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10)) .into() }) .collect(); @@ -201,9 +202,9 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> { let idx2 = (i * 13) % 1000; let idx3 = (i * 17) % 1000; - dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move { - a + b + c + i as i32 - })) + dag.add_task(task_fn::<(i32, i32, i32), _, _>( + move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32, + )) .depends_on((&sources[idx1], &sources[idx2], &sources[idx3])) }) .collect(); @@ -231,28 +232,24 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> { let mut chain_ends = Vec::new(); for chain_id in 0..100 { - let first = dag.add_task(task_fn({ + let first = dag.add_task(task_fn::<(), _, _>({ let counter = counter.clone(); move |_: ()| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::Relaxed); - chain_id * 100 - } + counter.fetch_add(1, Ordering::Relaxed); + chain_id * 100 } })); let mut current: dagx::TaskHandle = first.into(); for _i in 1..100 { current = dag - .add_task(task_fn({ + .add_task(task_fn::({ let counter = counter.clone(); - move |prev: i32| { + move |&prev: &i32| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::Relaxed); - prev + 1 - } + counter.fetch_add(1, Ordering::Relaxed); + prev + 1 } })) .depends_on(current); @@ -291,12 +288,10 @@ async fn test_100000_nodes_stress() -> DagResult<()> { .map(|i| { let completed = completed.clone(); let task_id = batch * batch_size + i; - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let completed = completed.clone(); - async move { - completed.fetch_add(1, Ordering::Relaxed); - task_id - } + completed.fetch_add(1, Ordering::Relaxed); + task_id })) }) .collect(); diff --git a/tests/boundaries/type_limits.rs b/tests/boundaries/type_limits.rs index f795009..4665506 100644 --- a/tests/boundaries/type_limits.rs +++ b/tests/boundaries/type_limits.rs @@ -19,21 +19,19 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> { let dag = DagRunner::new(); // Task producing ZST - let t1 = dag.add_task(task_fn(|_: ()| async { Empty })); + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty)); // Task that also produces ZST (independent since we can't extract custom types) - let t2: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let t2: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Task with PhantomData - let _t3 = dag.add_task(task_fn(|_: ()| async { - PhantomWrapper:: { - _phantom: PhantomData, - } + let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper:: { + _phantom: PhantomData, })); // Task that depends on t2 (uses primitive type) let t4 = dag - .add_task(task_fn(|val: i32| async move { + .add_task(task_fn::(|&val: &i32| { assert_eq!(val, 42); "success" })) @@ -59,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> { let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { let large = LargeStruct { data: [42; 8192], more_data: vec![1; 1000], @@ -69,7 +67,7 @@ async fn test_large_value_types() -> DagResult<()> { })); let t2 = dag - .add_task(task_fn(|val: usize| async move { + .add_task(task_fn::(|&val: &usize| { val // Just pass through to verify })) .depends_on(t1); @@ -86,12 +84,16 @@ async fn test_unit_type_chains() -> DagResult<()> { // Test chains of unit type operations let dag = DagRunner::new(); - let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async {})).into(); - let t2 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t1); - let t3 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t2); + let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| {})).into(); + let t2 = dag + .add_task(task_fn::<(), _, _>(|_: ()| {})) + .depends_on((t1,)); + let t3 = dag + .add_task(task_fn::<(), _, _>(|_: ()| {})) + .depends_on((t2,)); let t4 = dag - .add_task(task_fn(|_: ()| async { "done" })) - .depends_on(t3); + .add_task(task_fn::<(), _, _>(|_: ()| "done")) + .depends_on((t3,)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -103,82 +105,43 @@ async fn test_unit_type_chains() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_nested_option_result_types() -> DagResult<()> { - // Test deeply nested generic types - type ComplexType = Result>, String>>, &'static str>; - - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn(|_: ()| async { - let val: ComplexType = Ok(Some(Ok(vec![Some(42), None, Some(13)]))); - val - })); - - let t2 = dag - .add_task(task_fn(|complex: ComplexType| async move { - match complex { - Ok(Some(Ok(vec))) => vec.iter().filter_map(|x| *x).sum::(), - _ => 0, - } - })) - .depends_on(t1); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - assert_eq!(dag.get(t2)?, 55); - - Ok(()) -} - -#[tokio::test] -async fn test_tuple_of_tuples() -> DagResult<()> { - // Test nested tuple types - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn(|_: ()| async { ((1, 2), (3, 4)) })); - let t2 = dag.add_task(task_fn(|_: ()| async { ((5, 6), (7, 8)) })); - - let t3 = dag - .add_task(task_fn( - |inputs: (((i32, i32), (i32, i32)), ((i32, i32), (i32, i32)))| async move { - let (((a, b), (c, d)), ((e, f), (g, h))) = inputs; - a + b + c + d + e + f + g + h - }, - )) - .depends_on((t1, t2)); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - assert_eq!(dag.get(t3)?, 36); - - Ok(()) -} - #[tokio::test] async fn test_string_types_variety() -> DagResult<()> { // Test various string types let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { "static str" })); - let t2 = dag.add_task(task_fn(|_: ()| async { String::from("owned string") })); - let t3 = dag.add_task(task_fn(|_: ()| async { + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| "static str")); + let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| String::from("owned string"))); + let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| { std::borrow::Cow::Borrowed("cow str") })); - let t4 = dag.add_task(task_fn(|_: ()| async { Box::new("boxed str") })); - let t5 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str 2") })); - let t6 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str") })); + let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| Box::new("boxed str"))); + let t5 = dag.add_task(task_fn::<(), _, _>(|_: ()| { + std::sync::Arc::new("arc str 2") + })); + let t6 = dag.add_task(task_fn::<(), _, _>(|_: ()| std::sync::Arc::new("arc str"))); let combined = dag - .add_task(task_fn( - |(s1, s2, s3, s4, s5, s6): ( + .add_task(task_fn::< + ( &str, String, std::borrow::Cow, Box<&str>, std::sync::Arc<&str>, std::sync::Arc<&str>, - )| async move { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) }, + ), + _, + _, + >( + |(s1, s2, s3, s4, s5, s6): ( + &&str, + &String, + &std::borrow::Cow, + &Box<&str>, + &std::sync::Arc<&str>, + &std::sync::Arc<&str>, + )| { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) }, )) .depends_on((t1, t2, t3, t4, t5, t6)); @@ -199,13 +162,17 @@ async fn test_generic_type_constraints() -> DagResult<()> { let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { Wrapper(42) })); - let t2 = dag.add_task(task_fn(|_: ()| async { Wrapper("hello") })); - let t3 = dag.add_task(task_fn(|_: ()| async { Wrapper(vec![1, 2, 3]) })); + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(42))); + let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper("hello"))); + let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(vec![1, 2, 3]))); let combined = dag - .add_task(task_fn( - |(w1, w2, w3): (Wrapper, Wrapper<&str>, Wrapper>)| async move { + .add_task(task_fn::< + (Wrapper, Wrapper<&str>, Wrapper>), + _, + _, + >( + |(w1, w2, w3): (&Wrapper, &Wrapper<&str>, &Wrapper>)| { format!("{:?} {:?} {:?}", w1, w2, w3) }, )) @@ -226,7 +193,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> { let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { let arc = Arc::new(Mutex::new(100)); // Extract value instead of passing Arc let guard = arc.lock().unwrap(); @@ -234,7 +201,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> { })); let t2 = dag - .add_task(task_fn(|val: i32| async move { val * 2 })) + .add_task(task_fn::(|&val: &i32| val * 2)) .depends_on(t1); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -249,17 +216,19 @@ async fn test_array_types() -> DagResult<()> { // Test with fixed-size arrays let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { [1, 2, 3, 4, 5] })); - let t2 = dag.add_task(task_fn(|_: ()| async { [6, 7, 8, 9, 10] })); + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| [1, 2, 3, 4, 5])); + let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| [6, 7, 8, 9, 10])); let t3 = dag - .add_task(task_fn(|(a1, a2): ([i32; 5], [i32; 5])| async move { - let mut sum = 0; - for i in 0..5 { - sum += a1[i] + a2[i]; - } - sum - })) + .add_task(task_fn::<([i32; 5], [i32; 5]), _, _>( + |(a1, a2): (&[i32; 5], &[i32; 5])| { + let mut sum = 0; + for i in 0..5 { + sum += a1[i] + a2[i]; + } + sum + }, + )) .depends_on((t1, t2)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -283,16 +252,16 @@ async fn test_enum_variants() -> DagResult<()> { let dag = DagRunner::new(); - let t1 = dag.add_task(task_fn(|_: ()| async { MyEnum::Unit })); - let t2 = dag.add_task(task_fn(|_: ()| async { + let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Unit)); + let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| { MyEnum::Tuple(42, "test".to_string()) })); - let t3 = dag.add_task(task_fn(|_: ()| async { MyEnum::Struct { x: 10, y: 20 } })); - let t4 = dag.add_task(task_fn(|_: ()| async { MyEnum::LargeVariant([0; 1024]) })); + let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 })); + let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024]))); let combined = dag - .add_task(task_fn( - |(e1, e2, e3, e4): (MyEnum, MyEnum, MyEnum, MyEnum)| async move { + .add_task(task_fn::<(MyEnum, MyEnum, MyEnum, MyEnum), _, _>( + |(e1, e2, e3, e4): (&MyEnum, &MyEnum, &MyEnum, &MyEnum)| { matches!(e1, MyEnum::Unit) && matches!(e2, MyEnum::Tuple(42, _)) && matches!(e3, MyEnum::Struct { x: 10, y: 20 }) diff --git a/tests/boundaries/zero_tasks.rs b/tests/boundaries/zero_tasks.rs index 24abe3e..dafc44c 100644 --- a/tests/boundaries/zero_tasks.rs +++ b/tests/boundaries/zero_tasks.rs @@ -5,6 +5,7 @@ use dagx::{task, DagResult, DagRunner}; use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +use tokio::task::yield_now; #[tokio::test] async fn test_empty_dag_execution() -> DagResult<()> { @@ -22,7 +23,7 @@ async fn test_single_task_no_deps() -> DagResult<()> { // Test the absolute minimum: one task with no dependencies let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 42 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -38,11 +39,9 @@ async fn test_single_task_unit_input_output() -> DagResult<()> { let executed = Arc::new(AtomicUsize::new(0)); let exec_clone = executed.clone(); - let task = dag.add_task(task_fn(move |_: ()| { + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| { let exec = exec_clone.clone(); - async move { - exec.fetch_add(1, Ordering::SeqCst); - } + exec.fetch_add(1, Ordering::SeqCst); })); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -60,7 +59,7 @@ async fn test_zero_sized_type_task() -> DagResult<()> { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { ZeroSized })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| ZeroSized)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -86,7 +85,7 @@ async fn test_dag_with_only_source_tasks() -> DagResult<()> { let dag = DagRunner::new(); let tasks: Vec<_> = (0..100) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -132,7 +131,7 @@ async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> { 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 })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; assert_eq!(dag.get(task)?, i); } @@ -151,7 +150,7 @@ async fn test_dag_with_never_type_simulation() -> DagResult<()> { let dag = DagRunner::new(); // This task completes immediately but returns a Result containing "never" type - let task = dag.add_task(task_fn(|_: ()| async { Result::::Ok(42) })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::::Ok(42))); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -164,11 +163,17 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> { // Test a task that does the absolute minimum async work let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { - // Just yield once to the runtime - tokio::task::yield_now().await; - 1337 - })); + struct YieldTask; + + #[task] + impl YieldTask { + async fn run() -> i32 { + yield_now().await; + 1337 + } + } + + let task = dag.add_task(YieldTask); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; diff --git a/tests/custom_type_test.rs b/tests/custom_type_test.rs index c9a7c4d..50e4c66 100644 --- a/tests/custom_type_test.rs +++ b/tests/custom_type_test.rs @@ -80,7 +80,7 @@ async fn test_custom_type_passthrough() { let person_result = dag.get(process).unwrap(); let company_result = dag.get(transform).unwrap(); - assert_eq!(person_result, "Alice is 30 years old"); + assert_eq!(person_result.as_str(), "Alice is 30 years old"); assert_eq!(company_result.name, "Alice's Company"); assert_eq!(company_result.employees.len(), 1); } @@ -103,7 +103,7 @@ async fn test_custom_type_diamond_pattern() { let result_a = dag.get(path_a).unwrap(); let result_b = dag.get(path_b).unwrap(); - assert_eq!(result_a, "Alice is 30 years old"); + assert_eq!(result_a.as_str(), "Alice is 30 years old"); assert_eq!(result_b.name, "Alice's Company"); } diff --git a/tests/cycle_prevention.rs b/tests/cycle_prevention.rs index 843410d..a6e2a41 100644 --- a/tests/cycle_prevention.rs +++ b/tests/cycle_prevention.rs @@ -54,7 +54,7 @@ fn test_handle_is_immutable_reference() { let builder = dag.add_task(NoInput); // Convert to handle (only works because Input = ()) - let handle: TaskHandle = builder.into(); + let handle: TaskHandle<_> = builder.into(); // TaskHandle is Copy/Clone - just an ID wrapper let _copy1 = handle; @@ -108,7 +108,7 @@ fn test_ordering_prevents_cycles() { // So we can only create DAGs, never cycles! // Let's create a valid DAG instead: A→B (B depends on A) - let a_handle: TaskHandle = a_builder.into(); // A has no deps (unit input) + let a_handle: TaskHandle<_> = a_builder.into(); // A has no deps (unit input) let _b_handle = b_builder.depends_on(a_handle); // Now we could try: a_handle.depends_on(b_handle)? // But TaskHandle has no such method! @@ -138,8 +138,8 @@ fn test_unit_tasks_cannot_create_cycles() { let task2 = dag.add_task(Task2); // Both have unit input, so we can convert to handles - let _handle1: TaskHandle = task1.into(); - let _handle2: TaskHandle = task2.into(); + let _handle1: TaskHandle<_> = task1.into(); + let _handle2: TaskHandle<_> = task2.into(); // Now we have two handles, but... // TaskHandle has no methods to add dependencies! diff --git a/tests/dependencies/resolution.rs b/tests/dependencies/resolution.rs index f29d1aa..8f4f6af 100644 --- a/tests/dependencies/resolution.rs +++ b/tests/dependencies/resolution.rs @@ -1,7 +1,7 @@ //! Tests for dependency resolution order and transitive dependencies use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner, TaskHandle}; use futures::FutureExt; use std::sync::{Arc, Mutex}; @@ -12,72 +12,69 @@ async fn test_dependency_resolution_order() -> DagResult<()> { let execution_log = Arc::new(Mutex::new(Vec::new())); + struct ATask { + log: Arc>>, + } + + #[task] + impl ATask { + async fn run(&self) -> i32 { + self.log.lock().unwrap().push(("A_start", 0)); + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + self.log.lock().unwrap().push(("A_end", 1)); + 1 + } + } + // Create a chain: A -> B -> C -> D -> E - let a = { - let log = execution_log.clone(); - dag.add_task(task_fn(move |_: ()| { - let log = log.clone(); - async move { - log.lock().unwrap().push(("A_start", 0)); - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - log.lock().unwrap().push(("A_end", 1)); - 1 - } - })) - }; + let a = dag.add_task(ATask { + log: execution_log.clone(), + }); let b = { let log = execution_log.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let log = log.clone(); - async move { - log.lock().unwrap().push(("B_start", x)); - assert_eq!(x, 1, "B should receive 1 from A"); - log.lock().unwrap().push(("B_end", x + 1)); - x + 1 - } + log.lock().unwrap().push(("B_start", x)); + assert_eq!(x, 1, "B should receive 1 from A"); + log.lock().unwrap().push(("B_end", x + 1)); + x + 1 })) .depends_on(a) }; let c = { let log = execution_log.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let log = log.clone(); - async move { - log.lock().unwrap().push(("C_start", x)); - assert_eq!(x, 2, "C should receive 2 from B"); - log.lock().unwrap().push(("C_end", x + 1)); - x + 1 - } + log.lock().unwrap().push(("C_start", x)); + assert_eq!(x, 2, "C should receive 2 from B"); + log.lock().unwrap().push(("C_end", x + 1)); + x + 1 })) .depends_on(b) }; let d = { let log = execution_log.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let log = log.clone(); - async move { - log.lock().unwrap().push(("D_start", x)); - assert_eq!(x, 3, "D should receive 3 from C"); - log.lock().unwrap().push(("D_end", x + 1)); - x + 1 - } + log.lock().unwrap().push(("D_start", x)); + assert_eq!(x, 3, "D should receive 3 from C"); + log.lock().unwrap().push(("D_end", x + 1)); + x + 1 })) .depends_on(c) }; let e = { let log = execution_log.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let log = log.clone(); - async move { - log.lock().unwrap().push(("E_start", x)); - assert_eq!(x, 4, "E should receive 4 from D"); - log.lock().unwrap().push(("E_end", x + 1)); - x + 1 - } + log.lock().unwrap().push(("E_start", x)); + assert_eq!(x, 4, "E should receive 4 from D"); + log.lock().unwrap().push(("E_end", x + 1)); + x + 1 })) .depends_on(d) }; @@ -108,12 +105,12 @@ async fn test_transitive_dependencies() -> DagResult<()> { // A -> B -> C (creates transitive A -> C relationship) let dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); + let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); let b = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(b); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -129,12 +126,12 @@ async fn test_transitive_dependencies() -> DagResult<()> { async fn test_deep_chain_50_levels() -> DagResult<()> { let dag = DagRunner::new(); - let first = dag.add_task(task_fn(|_: ()| async { 0 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let mut handles = vec![first.into()]; for _i in 1..=50 { let next = dag - .add_task(task_fn(move |x: i32| async move { x + 1 })) + .add_task(task_fn::(move |&x: &i32| x + 1)) .depends_on(handles.last().unwrap()); handles.push(next); } @@ -149,12 +146,12 @@ async fn test_deep_chain_50_levels() -> DagResult<()> { async fn test_deep_chain_200_levels() -> DagResult<()> { let dag = DagRunner::new(); - let first = dag.add_task(task_fn(|_: ()| async { 1 })); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let mut current = first.into(); for _ in 0..200 { current = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(current); } diff --git a/tests/dependencies/single.rs b/tests/dependencies/single.rs index 8191d8d..070d406 100644 --- a/tests/dependencies/single.rs +++ b/tests/dependencies/single.rs @@ -8,9 +8,9 @@ use futures::FutureExt; async fn test_single_dependency() -> DagResult<()> { let dag = DagRunner::new(); - let source = dag.add_task(task_fn(|_: ()| async { 42 })); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); let dependent = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(source); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -25,21 +25,23 @@ async fn test_dependency_data_flow() -> DagResult<()> { let dag = DagRunner::new(); let source: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })) + .add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])) .into(); let sum = dag - .add_task(task_fn(|v: Vec| async move { v.iter().sum::() })) + .add_task(task_fn::, _, _>(|v: &Vec| { + v.iter().sum::() + })) .depends_on(source); let product = dag - .add_task(task_fn( - |v: Vec| async move { v.iter().product::() }, - )) + .add_task(task_fn::, _, _>(|v: &Vec| { + v.iter().product::() + })) .depends_on(source); let final_result = dag - .add_task(task_fn(|(s, p): (i32, i32)| async move { s + p })) + .add_task(task_fn::<(i32, i32), _, _>(|(s, p): (&i32, &i32)| s + p)) .depends_on((&sum, &product)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -56,14 +58,14 @@ async fn test_dependencies_with_different_types() -> DagResult<()> { // Test that dependencies with different types work correctly let dag = DagRunner::new(); - let int_source = dag.add_task(task_fn(|_: ()| async { 42 })); - let string_source = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); - let bool_source = dag.add_task(task_fn(|_: ()| async { true })); + let int_source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); + let string_source = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); + let bool_source = dag.add_task(task_fn::<(), _, _>(|_: ()| true)); let combined = dag - .add_task(task_fn(|(i, s, b): (i32, String, bool)| async move { - format!("{}: {} ({})", s, i, b) - })) + .add_task(task_fn::<(i32, String, bool), _, _>( + |(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b), + )) .depends_on((int_source, string_source, bool_source)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -77,12 +79,12 @@ async fn test_shared_dependencies() -> DagResult<()> { // Test that a single node can be a dependency for multiple downstream nodes let dag = DagRunner::new(); - let shared: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let shared: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Create 10 tasks that all depend on the shared node let dependents: Vec<_> = (0..10) .map(|i| { - dag.add_task(task_fn(move |x: i32| async move { x * (i + 1) })) + dag.add_task(task_fn::(move |&x: &i32| x * (i + 1))) .depends_on(shared) }) .collect(); @@ -103,7 +105,7 @@ async fn test_multiple_source_nodes() -> DagResult<()> { let dag = DagRunner::new(); let sources: Vec<_> = (0..20) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 10 }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10))) .collect(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; diff --git a/tests/dependencies/tuples.rs b/tests/dependencies/tuples.rs index 299c22e..3f78291 100644 --- a/tests/dependencies/tuples.rs +++ b/tests/dependencies/tuples.rs @@ -8,10 +8,10 @@ use futures::FutureExt; async fn test_two_dependencies() -> DagResult<()> { let dag = DagRunner::new(); - let a = dag.add_task(task_fn(|_: ()| async { 10 })); - let b = dag.add_task(task_fn(|_: ()| async { 20 })); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); + let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); let sum = dag - .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) + .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((a, b)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -24,12 +24,12 @@ async fn test_two_dependencies() -> DagResult<()> { async fn test_three_dependencies() -> DagResult<()> { let dag = DagRunner::new(); - let a = dag.add_task(task_fn(|_: ()| async { 1 })); - let b = dag.add_task(task_fn(|_: ()| async { 2 })); - let c = dag.add_task(task_fn(|_: ()| async { 3 })); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); + let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); + let c = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); let sum = dag - .add_task(task_fn( - |(x, y, z): (i32, i32, i32)| async move { x + y + z }, + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(x, y, z): (&i32, &i32, &i32)| x + y + z, )) .depends_on((a, b, c)); @@ -44,13 +44,13 @@ async fn test_four_dependencies() -> DagResult<()> { let dag = DagRunner::new(); let deps: Vec<_> = (1..=4) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) .collect(); let sum = dag - .add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move { - a + b + c + d - })) + .add_task(task_fn::<(i32, i32, i32, i32), _, _>( + |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d, + )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3])); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -64,12 +64,12 @@ async fn test_five_dependencies() -> DagResult<()> { let dag = DagRunner::new(); let deps: Vec<_> = (1..=5) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) .collect(); let sum = dag - .add_task(task_fn( - |(a, b, c, d, e): (i32, i32, i32, i32, i32)| async move { a + b + c + d + e }, + .add_task(task_fn::<(i32, i32, i32, i32, i32), _, _>( + |(a, b, c, d, e): (&i32, &i32, &i32, &i32, &i32)| a + b + c + d + e, )) .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4])); @@ -84,16 +84,14 @@ async fn test_six_dependencies() -> DagResult<()> { let dag = DagRunner::new(); let deps: Vec<_> = (1..=6) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) .collect(); - let sum = dag.add_task(task_fn( - |(a, b, c, d, e, f): (i32, i32, i32, i32, i32, i32)| async move { - a + b + c + d + e + f - } - )).depends_on(( - &deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5] - )); + let sum = dag + .add_task(task_fn::<(i32, i32, i32, i32, i32, i32), _, _>( + |(a, b, c, d, e, f): (&i32, &i32, &i32, &i32, &i32, &i32)| a + b + c + d + e + f, + )) + .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5])); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -106,12 +104,12 @@ async fn test_seven_dependencies() -> DagResult<()> { let dag = DagRunner::new(); let deps: Vec<_> = (1..=7) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) .collect(); let sum = dag - .add_task(task_fn( - |(a, b, c, d, e, f, g): (i32, i32, i32, i32, i32, i32, i32)| async move { + .add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32), _, _>( + |(a, b, c, d, e, f, g): (&i32, &i32, &i32, &i32, &i32, &i32, &i32)| { a + b + c + d + e + f + g }, )) @@ -130,12 +128,12 @@ async fn test_eight_dependencies() -> DagResult<()> { let dag = DagRunner::new(); let deps: Vec<_> = (1..=8) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into()) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into()) .collect(); let sum = dag - .add_task(task_fn( - |(a, b, c, d, e, f, g, h): (i32, i32, i32, i32, i32, i32, i32, i32)| async move { + .add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32, i32), _, _>( + |(a, b, c, d, e, f, g, h): (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| { a + b + c + d + e + f + g + h }, )) diff --git a/tests/errors/error_types.rs b/tests/errors/error_types.rs index affe88c..5c8a880 100644 --- a/tests/errors/error_types.rs +++ b/tests/errors/error_types.rs @@ -8,7 +8,7 @@ use futures::FutureExt; async fn test_result_not_found_error() { let dag = DagRunner::new(); - let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Try to get result before running let result = dag.get(t1); @@ -27,11 +27,11 @@ async fn test_nested_result_error_handling() { let dag = DagRunner::new(); // Task that returns a Result - let task1 = dag.add_task(task_fn(|_: ()| async { + let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { Result::::Err("Internal error".to_string()) })); - let task2 = dag.add_task(task_fn(|_: ()| async { Result::::Ok(42) })); + let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::::Ok(42))); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -46,7 +46,7 @@ async fn test_nested_result_error_handling() { async fn test_option_none_not_error() { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { None:: })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await @@ -66,11 +66,11 @@ async fn test_custom_error_types() { let dag = DagRunner::new(); - let task1 = dag.add_task(task_fn(|_: ()| async { + let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { Result::::Err(CustomError::ValidationError("Invalid input".to_string())) })); - let task2 = dag.add_task(task_fn(|_: ()| async { + let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| { Result::::Err(CustomError::ProcessingError { code: 500, details: "Processing failed".to_string(), @@ -82,13 +82,13 @@ async fn test_custom_error_types() { .unwrap(); // Check custom errors are preserved - match dag.get(task1).unwrap() { + match dag.get(task1).unwrap().as_ref() { Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"), _ => panic!("Wrong error type"), } - match dag.get(task2).unwrap() { - Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500), + match dag.get(task2).unwrap().as_ref() { + Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500), _ => panic!("Wrong error type"), } } @@ -97,13 +97,13 @@ async fn test_custom_error_types() { async fn test_error_in_tuple_dependency() { let dag = DagRunner::new(); - let ok_task = dag.add_task(task_fn(|_: ()| async { 10 })); - let err_task = dag.add_task(task_fn(|_: ()| async { + let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); + let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| { panic!("Error in dependency"); })); let dependent = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((ok_task, err_task)); let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -116,11 +116,11 @@ async fn test_error_in_tuple_dependency() { async fn test_string_panic_vs_structured_panic() { let dag = DagRunner::new(); - let string_panic = dag.add_task(task_fn(|_: ()| async { + let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| { panic!("String panic message"); })); - let formatted_panic = dag.add_task(task_fn(|_: ()| async { + let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| { panic!("Formatted panic: value={}, code={}", 42, "ERROR"); })); @@ -136,7 +136,7 @@ async fn test_anyhow_like_error_chain() { // Test error chains with simple string-based errors let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| { // Simulate an error chain with nested Results let _inner: Result = Err("Inner error".to_string()); let outer: Result, String> = Err("Outer error".to_string()); @@ -152,7 +152,7 @@ async fn test_anyhow_like_error_chain() { let result = dag.get(task).unwrap(); assert!(result.is_err()); - if let Err(e) = result { + if let Err(e) = result.as_ref() { assert_eq!(e, "Outer error"); } } diff --git a/tests/errors/propagation.rs b/tests/errors/propagation.rs index 80e832b..060b308 100644 --- a/tests/errors/propagation.rs +++ b/tests/errors/propagation.rs @@ -1,26 +1,28 @@ //! Tests for error propagation through the DAG use crate::common::task_fn; -use dagx::{DagRunner, TaskHandle}; +use dagx::{task, DagRunner, TaskHandle}; use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +use std::time::Duration; +use tokio::time::sleep; #[tokio::test] async fn test_linear_error_propagation() { let dag = DagRunner::new(); - let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into(); + let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); let t2 = dag - .add_task(task_fn(|x: i32| async move { + .add_task(task_fn::(|&x: &i32| -> i32 { panic!("Error at t2 with input {}", x); })) .depends_on(t1); let t3 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(t2); let t4 = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(t3); let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -46,20 +48,20 @@ async fn test_diamond_error_propagation() { // \ / // sink - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); let left = dag - .add_task(task_fn(|x: i32| async move { x / 2 })) + .add_task(task_fn::(|&x: &i32| x / 2)) .depends_on(source); let right = dag - .add_task(task_fn(|x: i32| async move { + .add_task(task_fn::(|&x: &i32| { panic!("Right path fails with {}", x); })) .depends_on(source); let sink = dag - .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) + .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) .depends_on((&left, &right)); let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -77,40 +79,34 @@ async fn test_error_stops_at_boundary() { // Branch with error let error_source: TaskHandle<_> = dag - .add_task(task_fn({ + .add_task(task_fn::<(), _, _>({ let tracker = execution_tracker.clone(); - move |_: ()| { + move |_: ()| -> i32 { let tracker = tracker.clone(); - async move { - tracker.fetch_add(1, Ordering::SeqCst); - panic!("Error source"); - } + tracker.fetch_add(1, Ordering::SeqCst); + panic!("Error source"); } })) .into(); let error_dependent = dag - .add_task(task_fn({ + .add_task(task_fn::({ let tracker = execution_tracker.clone(); - move |_: i32| { + move |_: &i32| { let tracker = tracker.clone(); - async move { - tracker.fetch_add(100, Ordering::SeqCst); // Should not execute - 42 - } + tracker.fetch_add(100, Ordering::SeqCst); // Should not execute + 42 } })) .depends_on(error_source); // Independent branch - let independent = dag.add_task(task_fn({ + let independent = dag.add_task(task_fn::<(), _, _>({ let tracker = execution_tracker.clone(); move |_: ()| { let tracker = tracker.clone(); - async move { - tracker.fetch_add(10, Ordering::SeqCst); - 10 - } + tracker.fetch_add(10, Ordering::SeqCst); + 10 } })); @@ -133,22 +129,24 @@ async fn test_multiple_error_sources_convergence() { // Multiple error sources let error1 = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { panic!("Error source 1"); })) .into(); let error2 = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { panic!("Error source 2"); })) .into(); - let success = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let success = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Convergence point depends on all three let convergence = dag - .add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 })) + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(_, _, _): (&i32, &i32, &i32)| 100, + )) .depends_on((&error1, &error2, &success)); let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -169,7 +167,7 @@ async fn test_error_in_wide_fanout() { let dag = DagRunner::new(); let source: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| -> i32 { panic!("Source fails"); })) .into(); @@ -177,7 +175,7 @@ async fn test_error_in_wide_fanout() { // Create wide fanout from failing source let dependents: Vec<_> = (0..20) .map(|i| { - dag.add_task(task_fn(move |x: i32| async move { x + i })) + dag.add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(source) }) .collect(); @@ -199,24 +197,26 @@ async fn test_selective_error_propagation() { // Source that produces a Result let source: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { Result::::Err("Source error".to_string()) })) .into(); // Handler that processes the Result let handler = dag - .add_task(task_fn(|result: Result| async move { - match result { - Ok(val) => val * 2, - Err(_) => -1, // Default value on error - } - })) + .add_task(task_fn::, _, _>( + |result: &Result| { + match result { + Ok(val) => val * 2, + Err(_) => -1, // Default value on error + } + }, + )) .depends_on(source); // Further processing let final_task = dag - .add_task(task_fn(|val: i32| async move { + .add_task(task_fn::(|&val: &i32| { if val < 0 { "Handled error case".to_string() } else { @@ -244,41 +244,49 @@ async fn test_error_propagation_timing() { let dag = DagRunner::new(); let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new())); - let t1 = dag.add_task(task_fn({ - let order = propagation_order.clone(); - move |_: ()| { - let order = order.clone(); - async move { - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - order.lock().push("t1_complete"); - 1 - } + struct T1Task { + order: Arc>>, + } + + #[task] + impl T1Task { + async fn run(&self) -> i32 { + sleep(Duration::from_millis(10)).await; + self.order.lock().push("t1_complete"); + 1 } - })); + } + + let t1 = dag.add_task(T1Task { + order: propagation_order.clone(), + }); + + struct T2Task { + order: Arc>>, + } + + #[task] + impl T2Task { + async fn run(&self, x: &i32) -> i32 { + self.order.lock().push("t2_start"); + sleep(Duration::from_millis(10)).await; + panic!("t2 fails with {x}"); + } + } let t2 = dag - .add_task(task_fn({ - let order = propagation_order.clone(); - move |x: i32| { - let order = order.clone(); - async move { - order.lock().push("t2_start"); - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - panic!("t2 fails with {}", x); - } - } - })) + .add_task(T2Task { + order: propagation_order.clone(), + }) .depends_on(t1); let _t3 = dag - .add_task(task_fn({ + .add_task(task_fn::({ let order = propagation_order.clone(); - move |x: i32| { + move |x: &i32| { let order = order.clone(); - async move { - order.lock().push("t3_should_not_run"); - x + 1 - } + order.lock().push("t3_should_not_run"); + x + 1 } })) .depends_on(t2); @@ -307,53 +315,51 @@ async fn test_partial_branch_failure_propagation() { // \ | / // sink - let root: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into(); + let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); // Branch A let a1 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(root); let a2 = dag - .add_task(task_fn(|x: i32| async move { x + 2 })) + .add_task(task_fn::(|&x: &i32| x + 2)) .depends_on(a1); let a3 = dag - .add_task(task_fn(|x: i32| async move { x + 3 })) + .add_task(task_fn::(|&x: &i32| x + 3)) .depends_on(a2); // Branch B (with error) - use Result instead of panic let b1 = dag - .add_task(task_fn(|x: i32| async move { Ok(x * 2) })) + .add_task(task_fn::(|&x: &i32| Ok::(x * 2))) .depends_on(root); let b2 = dag - .add_task(task_fn(|_x: Result| async move { - Err("Branch B fails at b2") + .add_task(task_fn::, _, _>(|_x: &Result| { + Err::("Branch B fails at b2") })) .depends_on(b1); let b3 = dag - .add_task(task_fn( - |x: Result| async move { x.map(|v| v * 3) }, - )) + .add_task(task_fn::, _, _>(|x: &Result| { + x.map(|v| v * 3) + })) .depends_on(b2); // Branch C let c1 = dag - .add_task(task_fn(|x: i32| async move { x - 1 })) + .add_task(task_fn::(|&x: &i32| x - 1)) .depends_on(root); let c2 = dag - .add_task(task_fn(|x: i32| async move { x - 2 })) + .add_task(task_fn::(|&x: &i32| x - 2)) .depends_on(c1); let c3 = dag - .add_task(task_fn(|x: i32| async move { x - 3 })) + .add_task(task_fn::(|&x: &i32| x - 3)) .depends_on(c2); // Sink depends on all branches - handle Result from B let sink = dag - .add_task(task_fn( - |(a, b, c): (i32, Result, i32)| async move { - match b { - Ok(b_val) => Ok(a + b_val + c), - Err(e) => Err(e), - } + .add_task(task_fn::<(i32, Result<_, _>, _), _, _>( + |(a, b, c): (&i32, &Result, &i32)| match b { + Ok(b_val) => Ok(a + b_val + c), + Err(e) => Err(*e), }, )) .depends_on((&a3, &b3, &c3)); diff --git a/tests/errors/recovery.rs b/tests/errors/recovery.rs index d6226b2..50bd630 100644 --- a/tests/errors/recovery.rs +++ b/tests/errors/recovery.rs @@ -11,17 +11,17 @@ async fn test_fallback_on_error() -> DagResult<()> { let dag = DagRunner::new(); // Primary task that fails - let primary = dag.add_task(task_fn(|_: ()| async { + let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| { panic!("Primary task failed"); #[allow(unreachable_code)] 42 })); // Fallback task - let fallback = dag.add_task(task_fn(|_: ()| async { 100 })); + let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); // Selector that uses fallback if primary fails - let _selector = dag.add_task(task_fn(|_: ()| async {})); + let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {})); // Run DAG let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -41,22 +41,20 @@ async fn test_retry_pattern_simulation() -> DagResult<()> { let attempt_counter = Arc::new(AtomicUsize::new(0)); // Task that would fail on first attempts (simulated) - let task = dag.add_task(task_fn({ + let task = dag.add_task(task_fn::<(), _, _>({ let counter = attempt_counter.clone(); move |_: ()| { let counter = counter.clone(); - async move { - let attempt = counter.fetch_add(1, Ordering::SeqCst); + let attempt = counter.fetch_add(1, Ordering::SeqCst); - // In real retry, this would be retried - // Here we simulate by not panicking - if attempt == 0 { - // Would normally panic here - return Err::("Simulated failure"); - } - - Ok(42) + // In real retry, this would be retried + // Here we simulate by not panicking + if attempt == 0 { + // Would normally panic here + return Err::("Simulated failure"); } + + Ok(42) } })); @@ -79,26 +77,24 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> { .map(|i| { let failures = failure_count.clone(); let circuit = circuit_open.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let failures = failures.clone(); let circuit = circuit.clone(); - async move { - // Check if circuit is open - if circuit.load(Ordering::SeqCst) { - return Err("Circuit breaker open"); - } - - // Simulate some failures - if i < 3 { - let count = failures.fetch_add(1, Ordering::SeqCst); - if count >= 2 { - circuit.store(true, Ordering::SeqCst); - } - return Err("Task failed"); - } - - Ok(i) + // Check if circuit is open + if circuit.load(Ordering::SeqCst) { + return Err("Circuit breaker open"); } + + // Simulate some failures + if i < 3 { + let count = failures.fetch_add(1, Ordering::SeqCst); + if count >= 2 { + circuit.store(true, Ordering::SeqCst); + } + return Err("Task failed"); + } + + Ok(i) })) }) .collect(); @@ -120,7 +116,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> { // Some tasks that might fail let _tasks: Vec<_> = (0..10) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { if i % 3 == 0 { panic!("Task {} failed", i); } @@ -130,7 +126,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> { .collect(); // Aggregator that handles failures gracefully - let aggregator = dag.add_task(task_fn(|_: ()| async { + let aggregator = dag.add_task(task_fn::<(), _, _>(|_: ()| { // In real scenario, would check task results and use defaults vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks })); @@ -152,15 +148,13 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { let mut tasks: Vec<_> = (0..10) .map(|i| { let errors = errors.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let errors = errors.clone(); - async move { - if i % 2 == 0 { - errors.lock().push(format!("Error from task {}", i)); - return Err(format!("Task {} error", i)); - } - Ok(i) + if i % 2 == 0 { + errors.lock().push(format!("Error from task {}", i)); + return Err(format!("Task {} error", i)); } + Ok(i) })) }) .collect(); @@ -187,16 +181,16 @@ async fn test_graceful_degradation() -> DagResult<()> { let dag = DagRunner::new(); // Critical task - let critical = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })); + let critical = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])); // Optional enhancement that fails - returns Result - let _enhancement = dag.add_task(task_fn(|_: ()| async { + let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| { Err::, &str>("Enhancement failed") })); // Final task that works with what's available let final_task = dag - .add_task(task_fn(|base: Vec| async move { + .add_task(task_fn::, _, _>(|base: &Vec| { // Would combine with enhancement if available // Here just uses base base.iter().sum::() @@ -216,22 +210,22 @@ async fn test_error_boundary_isolation() -> DagResult<()> { let dag = DagRunner::new(); // Group A: contains an error - use Result - let a1 = dag.add_task(task_fn(|_: ()| async { 10 })); + let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); let a2 = dag - .add_task(task_fn(|_x: i32| async move { + .add_task(task_fn::(|_x: &i32| { Err::("Group A error") })) .depends_on(a1); // Group B: should be isolated from Group A's error - let b1 = dag.add_task(task_fn(|_: ()| async { 20 })); + let b1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); let b2 = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(b1); // Group C: depends on B but not A let c1 = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(b2); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -252,20 +246,18 @@ async fn test_compensation_action() -> DagResult<()> { let compensation_triggered = Arc::new(AtomicBool::new(false)); // Action that fails - let action = dag.add_task(task_fn(|_: ()| async { + let action = dag.add_task(task_fn::<(), _, _>(|_: ()| { // Perform some action that fails panic!("Action failed"); })); // Compensation task (runs independently) - let compensation = dag.add_task(task_fn({ + let compensation = dag.add_task(task_fn::<(), _, _>({ let triggered = compensation_triggered.clone(); move |_: ()| { let triggered = triggered.clone(); - async move { - triggered.store(true, Ordering::SeqCst); - "Compensation executed" - } + triggered.store(true, Ordering::SeqCst); + "Compensation executed" } })); diff --git a/tests/execution/basic.rs b/tests/execution/basic.rs index caade69..a21a7e2 100644 --- a/tests/execution/basic.rs +++ b/tests/execution/basic.rs @@ -15,7 +15,7 @@ async fn test_empty_dag() { #[tokio::test] async fn test_single_task() { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 42 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) .await .unwrap(); @@ -26,18 +26,18 @@ async fn test_single_task() { async fn test_deep_chain() { let dag = DagRunner::new(); - let a = dag.add_task(task_fn(|_: ()| async { 1 })); + let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); let b = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(b); let d = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(c); let e = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(d); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -54,7 +54,7 @@ async fn test_wide_parallel() { let dag = DagRunner::new(); let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) .collect(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -70,15 +70,15 @@ async fn test_wide_parallel() { async fn test_diamond_dependency() { let dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); + let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); let b = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(a); let d = dag - .add_task(task_fn(|(x, y): (i32, i32)| async move { x + y })) + .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) .depends_on((&b, &c)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -100,13 +100,11 @@ struct User { async fn test_different_output_types() { let dag = DagRunner::new(); - let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); - let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); - let struct_task = dag.add_task(task_fn(|_: ()| async { - User { - name: "Alice".to_string(), - age: 30, - } + let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); + let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3])); + let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User { + name: "Alice".to_string(), + age: 30, })); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -124,22 +122,22 @@ async fn test_different_output_types() { async fn test_multiple_sinks() { let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); // Branch 1 let branch1 = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(source); let sink1 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(branch1); // Branch 2 let branch2 = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(source); let sink2 = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(branch2); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -156,12 +154,12 @@ async fn test_single_task_inline_path() { let dag = DagRunner::new(); // Single task in its own layer - let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); + let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); let t2 = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(t1); let t3 = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(t2); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -176,17 +174,17 @@ async fn test_multi_task_spawn_path() { // Test multi-task layer spawn path (line 411-456 in runner.rs) let dag = DagRunner::new(); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); // Create multiple tasks in the same layer (all depend on source) let t1 = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(source); let t2 = dag - .add_task(task_fn(|x: i32| async move { x + 2 })) + .add_task(task_fn::(|&x: &i32| x + 2)) .depends_on(source); let t3 = dag - .add_task(task_fn(|x: i32| async move { x + 3 })) + .add_task(task_fn::(|&x: &i32| x + 3)) .depends_on(source); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -205,14 +203,14 @@ async fn test_producer_consumer_channels() { // Producer let producer: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })) + .add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])) .into(); // Consumer let consumer = dag - .add_task(task_fn( - |data: Vec| async move { data.iter().sum::() }, - )) + .add_task(task_fn::, _, _>(|data: &Vec| { + data.iter().sum::() + })) .depends_on(producer); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -227,17 +225,17 @@ async fn test_multi_consumer_fanout() { // Test fanout with multiple consumers from one producer let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); + let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); // Multiple consumers let c1 = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(producer); let c2 = dag - .add_task(task_fn(|x: i32| async move { x + 10 })) + .add_task(task_fn::(|&x: &i32| x + 10)) .depends_on(producer); let c3 = dag - .add_task(task_fn(|x: i32| async move { x - 5 })) + .add_task(task_fn::(|&x: &i32| x - 5)) .depends_on(producer); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -255,17 +253,17 @@ async fn test_compute_layers_with_dependents_tracking() { let dag = DagRunner::new(); // Create diamond pattern to ensure dependents are tracked correctly - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); let left = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(source); let right = dag - .add_task(task_fn(|x: i32| async move { x + 2 })) + .add_task(task_fn::(|&x: &i32| x + 2)) .depends_on(source); let sink = dag - .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) + .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) .depends_on((&left, &right)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -286,33 +284,27 @@ async fn test_layer_execution_order() { let dag = DagRunner::new(); let order1 = execution_order.clone(); - let t1 = dag.add_task(task_fn(move |_: ()| { + let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { let order = order1.clone(); - async move { - order.lock().push(1); - 1 - } + order.lock().push(1); + 1 })); let order2 = execution_order.clone(); let t2 = dag - .add_task(task_fn(move |x: i32| { + .add_task(task_fn::(move |&x: &i32| { let order = order2.clone(); - async move { - order.lock().push(2); - x + 1 - } + order.lock().push(2); + x + 1 })) .depends_on(t1); let order3 = execution_order.clone(); let t3 = dag - .add_task(task_fn(move |x: i32| { + .add_task(task_fn::(move |&x: &i32| { let order = order3.clone(); - async move { - order.lock().push(3); - x + 1 - } + order.lock().push(3); + x + 1 })) .depends_on(t2); diff --git a/tests/execution/examples.rs b/tests/execution/examples.rs deleted file mode 100644 index a7b2147..0000000 --- a/tests/execution/examples.rs +++ /dev/null @@ -1,82 +0,0 @@ -// Example pattern tests for DagRunner -use crate::common::task_fn; - -use crate::common::tasks::*; -use dagx::*; -use futures::FutureExt; - -#[tokio::test] -async fn test_example_a_fan_out() { - let dag = DagRunner::new(); - - let base: TaskHandle<_> = dag.add_task(Seed::new(10)).into(); // Input=() → no depends_on, returns TaskHandle - 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).map(Result::unwrap)) - .await - .unwrap(); // SPEC API with spawner - - assert_eq!(dag.get(plus1).unwrap(), 11); - assert_eq!(dag.get(plus2).unwrap(), 11); -} - -#[tokio::test] -async fn test_example_b_fan_in() { - let dag = DagRunner::new(); - - let s = dag.add_task(task_fn(|_: ()| async { "hi".to_owned() })); - let n = dag.add_task(task_fn(|_: ()| async { 42usize })); - let f = dag.add_task(task_fn(|_: ()| async { true })); - - let output = dag.add_task(OutputResult::new()).depends_on((s, n, f)); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - assert_eq!(&dag.get(output).unwrap(), "hi:42:true"); -} - -#[tokio::test] -async fn test_example_c_many_to_many() { - let dag = DagRunner::new(); - - let x = dag.add_task(Seed::new(2)); - let y = dag.add_task(Seed::new(3)).into(); - let z = dag.add_task(Seed::new(5)); - - let sum_xy = dag.add_task(Add::new()).depends_on((x, &y)); // 5 - 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).map(Result::unwrap)) - .await - .unwrap(); - assert_eq!(dag.get(total).unwrap(), 20); -} - -#[tokio::test] -async fn test_single_input_forms() { - let dag = DagRunner::new(); - - let s = dag - .add_task(task_fn(|_: ()| async { "hello".to_owned() })) - .into(); - - // Preferred form: &node - let upper1 = dag - .add_task(task_fn(|s: String| async move { s.to_uppercase() })) - .depends_on(s); - - // Also allowed: (&node,) - let upper2 = dag - .add_task(task_fn(|s: String| async move { s.to_uppercase() })) - .depends_on((&s,)); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - assert_eq!(&dag.get(upper1).unwrap(), "HELLO"); - assert_eq!(&dag.get(upper2).unwrap(), "HELLO"); -} diff --git a/tests/execution/integration.rs b/tests/execution/integration.rs index fe51891..524197c 100644 --- a/tests/execution/integration.rs +++ b/tests/execution/integration.rs @@ -170,7 +170,7 @@ async fn integration_test_wide_and_deep() -> DagResult<()> { for source in &sources { let step1 = dag.add_task(Multiply::new(2)).depends_on(source); let step2 = dag - .add_task(task_fn(|x: i32| async move { x + 5 })) + .add_task(task_fn::(|&x: &i32| x + 5)) .depends_on(step1); let step3 = dag.add_task(Multiply::new(3)).depends_on(step2); final_nodes.push(step3); @@ -276,7 +276,7 @@ async fn integration_test_parallel_execution_timing() -> DagResult<()> { // Create 5 independent tasks that each sleep for 100ms let tasks: Vec<_> = (0..num_parallel_tasks) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { sleep(sleep_duration).await; i })) @@ -314,7 +314,7 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> { // Layer 1: 4 parallel tasks (should take ~50ms) let parallel_tasks: Vec<_> = (0..4) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { sleep(sleep_duration).await; i * 10 })) @@ -324,21 +324,21 @@ 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 { + .add_task(task_fn::(move |&x: &i32| { sleep(sleep_duration).await; x + 1 })) .depends_on(parallel_tasks[0]); let seq2 = dag - .add_task(task_fn(move |x: i32| async move { + .add_task(task_fn::(move |&x: &i32| { sleep(sleep_duration).await; x + 1 })) .depends_on(seq1); let seq3 = dag - .add_task(task_fn(move |x: i32| async move { + .add_task(task_fn::(move |&x: &i32| { sleep(sleep_duration).await; x + 1 })) diff --git a/tests/execution/mod.rs b/tests/execution/mod.rs index 8ead374..3cd6d17 100644 --- a/tests/execution/mod.rs +++ b/tests/execution/mod.rs @@ -1,5 +1,4 @@ // Test module for execution tests mod basic; -mod examples; mod results; diff --git a/tests/execution/results.rs b/tests/execution/results.rs index ce0b81f..fe3a81e 100644 --- a/tests/execution/results.rs +++ b/tests/execution/results.rs @@ -9,8 +9,8 @@ use futures::FutureExt; async fn test_result_type() { let dag = DagRunner::new(); - let success_task = dag.add_task(task_fn(|_: ()| async { Ok::(42) })); - let error_task = dag.add_task(task_fn(|_: ()| async { + let success_task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::(42))); + let error_task = dag.add_task(task_fn::<(), _, _>(|_: ()| { Err::("error".to_string()) })); @@ -25,7 +25,7 @@ async fn test_result_type() { #[tokio::test] async fn test_dag_result_type_works() { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 100 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); // Test that DagResult<()> works with ? operator pattern async fn run_dag(dag: &DagRunner) -> DagResult<()> { @@ -41,7 +41,7 @@ async fn test_dag_result_type_works() { #[tokio::test] async fn test_task_not_executed_error() { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 42 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); // Try to get result before running - should error let result = dag.get(task); @@ -58,7 +58,7 @@ async fn test_task_not_executed_error() { async fn test_error_messages_are_helpful() { // Test ResultNotFound error message let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 42 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); let result = dag.get(task); assert!(result.is_err()); let err_msg = format!("{}", result.unwrap_err()); @@ -70,7 +70,7 @@ async fn test_error_messages_are_helpful() { #[tokio::test] async fn test_successful_execution_returns_ok() { let dag = DagRunner::new(); - let task = dag.add_task(task_fn(|_: ()| async { 42 })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); // Should return Ok(()) let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; @@ -84,7 +84,7 @@ async fn test_successful_execution_returns_ok() { async fn test_stateful_tasks() { let dag = DagRunner::new(); - let input = dag.add_task(task_fn(|_: ()| async { 5 })); + let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); let counter = dag.add_task(Counter::new(10)).depends_on(input); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -102,9 +102,9 @@ async fn test_task_fn_with_captured_state() { let dag = DagRunner::new(); let multiplier = 7; - let base = dag.add_task(task_fn(|_: ()| async { 5 })); + let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); let scaled = dag - .add_task(task_fn(move |x: i32| async move { x * multiplier })) + .add_task(task_fn::(move |&x: &i32| x * multiplier)) .depends_on(base); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/tests/parallelism/concurrent.rs b/tests/parallelism/concurrent.rs index d4f4ddf..1aa6d28 100644 --- a/tests/parallelism/concurrent.rs +++ b/tests/parallelism/concurrent.rs @@ -1,27 +1,30 @@ //! Concurrent access tests use crate::common::task_fn; -use dagx::DagRunner; +use dagx::{task, DagRunner}; use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinSet; +struct SleepTask(usize); + +#[task] +impl SleepTask { + async fn run(&self) -> usize { + tokio::time::sleep(Duration::from_millis(5)).await; + self.0 + } +} + #[tokio::test] async fn test_concurrent_run_calls() { // Test that multiple run() calls are handled correctly let dag = Arc::new(DagRunner::new()); // Add some tasks - let tasks: Vec<_> = (0..10) - .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { - tokio::time::sleep(Duration::from_millis(5)).await; - i - })) - }) - .collect(); + let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect(); // Try to run the DAG multiple times concurrently let mut handles = JoinSet::new(); @@ -59,7 +62,7 @@ async fn test_concurrent_execution_and_building() { // Start with some initial tasks let initial: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); // Execute the initial DAG @@ -81,7 +84,7 @@ async fn test_concurrent_execution_and_building() { for i in 10..20 { let dag = Arc::clone(&dag); handles.spawn(async move { - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); i }); } @@ -112,14 +115,14 @@ async fn test_dag_builder_thread_safety() { handles.spawn(async move { // Each thread creates a small pipeline - let source = dag.add_task(task_fn(move |_: ()| async move { thread_id })); + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| thread_id)); let transform1 = dag - .add_task(task_fn(move |x: i32| async move { x * 10 })) + .add_task(task_fn::(move |&x: &i32| x * 10)) .depends_on(source); let transform2 = dag - .add_task(task_fn(move |x: i32| async move { x + thread_id })) + .add_task(task_fn::(move |&x: &i32| x + thread_id)) .depends_on(transform1); counter.fetch_add(1, Ordering::SeqCst); @@ -155,7 +158,7 @@ async fn test_concurrent_get_operations() { // Create and execute tasks let tasks: Vec<_> = (0..100) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -188,7 +191,7 @@ async fn test_concurrent_get_operations() { async fn test_no_race_in_dependency_tracking() { // Test that concurrent dependency modifications don't cause races let dag = Arc::new(DagRunner::new()); - let source = dag.add_task(task_fn(|_: ()| async { 42 })); + let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); let mut handles = JoinSet::new(); @@ -199,7 +202,7 @@ async fn test_no_race_in_dependency_tracking() { let source_clone = source_handle; handles.spawn(async move { - dag.add_task(task_fn(move |x: i32| async move { x + i })) + dag.add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(source_clone) }); } @@ -239,20 +242,22 @@ async fn test_high_concurrency_stress() { match i % 3 { 0 => { // Add a source task - dag.add_task(task_fn(move |_: ()| async move { i })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); } 1 => { // Add a task with a simple dependency - let source = dag.add_task(task_fn(move |_: ()| async move { i })); - dag.add_task(task_fn(move |x: i32| async move { x * 2 })) + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); + dag.add_task(task_fn::(move |&x: &i32| x * 2)) .depends_on(source); } 2 => { // Add a more complex subgraph - let a = dag.add_task(task_fn(move |_: ()| async move { i })); - let b = dag.add_task(task_fn(move |_: ()| async move { i + 1 })); - dag.add_task(task_fn(move |(x, y): (i32, i32)| async move { x + y })) - .depends_on((a, b)); + let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); + let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1)); + dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| { + x + y + })) + .depends_on((a, b)); } _ => unreachable!(), } @@ -289,7 +294,7 @@ async fn test_lock_contention_measurement() { handles.spawn(async move { for j in 0..10 { - dag.add_task(task_fn(move |_: ()| async move { i * 10 + j })); + dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10 + j)); ops.fetch_add(1, Ordering::SeqCst); // Small yield to increase contention likelihood @@ -326,7 +331,7 @@ async fn test_atomic_dag_operations() { let dag = Arc::new(DagRunner::new()); // Create a base task - let base = dag.add_task(task_fn(|_: ()| async { 100 })); + let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); // Try to create dependent tasks concurrently let mut handles = JoinSet::new(); @@ -340,7 +345,7 @@ async fn test_atomic_dag_operations() { handles.spawn(async move { let dep = dag - .add_task(task_fn(move |x: i32| async move { x + i })) + .add_task(task_fn::(move |&x: &i32| x + i)) .depends_on(base_clone); success.fetch_add(1, Ordering::SeqCst); @@ -381,12 +386,12 @@ async fn test_no_deadlock_on_circular_waiting() { handles.spawn(async move { // Each thread creates a chain of tasks - let first = dag.add_task(task_fn(move |_: ()| async move { i })); + let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); let mut chain = vec![first.into()]; for j in 0..5 { let next = dag - .add_task(task_fn(move |x: i32| async move { x + j })) + .add_task(task_fn::(move |&x: &i32| x + j)) .depends_on(chain.last().unwrap()); chain.push(next); diff --git a/tests/parallelism/execution.rs b/tests/parallelism/execution.rs index 5dfcbf8..b78b3b9 100644 --- a/tests/parallelism/execution.rs +++ b/tests/parallelism/execution.rs @@ -1,7 +1,7 @@ //! Tests for execution correctness and parallelism use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner, TaskHandle}; use futures::FutureExt; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -14,37 +14,61 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> { let layer_timing = Arc::new(Mutex::new(Vec::new())); + struct Layer0Task { + timing: Arc>>, + idx: i32, + } + + #[task] + impl Layer0Task { + async fn run(&self) -> i32 { + let start = Instant::now(); + self.timing + .lock() + .unwrap() + .push((format!("L0_{}", self.idx), start)); + sleep(Duration::from_millis(20)).await; + self.idx * 10 + } + } + // Layer 0: 3 independent sources let sources: Vec<_> = (0..3) .map(|i| { - let timing = layer_timing.clone(); - dag.add_task(task_fn(move |_: ()| { - let timing = timing.clone(); - async move { - let start = Instant::now(); - timing.lock().unwrap().push((format!("L0_{}", i), start)); - sleep(Duration::from_millis(20)).await; - i * 10 - } - })) + dag.add_task(Layer0Task { + timing: layer_timing.clone(), + idx: i, + }) }) .collect(); + struct Layer1Task { + timing: Arc>>, + idx: usize, + } + + #[task] + impl Layer1Task { + async fn run(&self, x: &i32) -> i32 { + let start = Instant::now(); + self.timing + .lock() + .unwrap() + .push((format!("L1_{}", self.idx), start)); + sleep(Duration::from_millis(20)).await; + x + 1 + } + } + // Layer 1: Tasks depending on layer 0 let layer1: Vec<_> = sources .into_iter() .enumerate() .map(|(i, source)| { - let timing = layer_timing.clone(); - dag.add_task(task_fn(move |x: i32| { - let timing = timing.clone(); - async move { - let start = Instant::now(); - timing.lock().unwrap().push((format!("L1_{}", i), start)); - sleep(Duration::from_millis(20)).await; - x + 1 - } - })) + dag.add_task(Layer1Task { + idx: i, + timing: layer_timing.clone(), + }) .depends_on(source) }) .collect(); @@ -52,14 +76,14 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> { // Layer 2: Final aggregation let final_task = { let timing = layer_timing.clone(); - dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| { - let timing = timing.clone(); - async move { + dag.add_task(task_fn::<(i32, i32, i32), _, _>( + move |(a, b, c): (&i32, &i32, &i32)| { + let timing = timing.clone(); let start = Instant::now(); timing.lock().unwrap().push(("L2_final".to_string(), start)); a + b + c - } - })) + }, + )) .depends_on((&layer1[0], &layer1[1], &layer1[2])) }; @@ -134,51 +158,57 @@ async fn test_completion_order_matches_dependencies() -> DagResult<()> { let completion_order = Arc::new(Mutex::new(Vec::new())); - // Create a complex DAG with clear ordering requirements - let a = { - let order = completion_order.clone(); - dag.add_task(task_fn(move |_: ()| { - let order = order.clone(); - async move { - sleep(Duration::from_millis(10)).await; - order.lock().unwrap().push("A"); - 1 - } - })) - }; + struct ATask { + order: Arc>>, + } - let b = { - let order = completion_order.clone(); - dag.add_task(task_fn(move |_: ()| { - let order = order.clone(); - async move { - sleep(Duration::from_millis(5)).await; - order.lock().unwrap().push("B"); - 2 - } - })) - }; + #[task] + impl ATask { + async fn run(&self) -> i32 { + sleep(Duration::from_millis(10)).await; + self.order.lock().unwrap().push("A"); + 1 + } + } + + // Create a complex DAG with clear ordering requirements + let a = dag.add_task(ATask { + order: completion_order.clone(), + }); + + struct BTask { + order: Arc>>, + } + + #[task] + impl BTask { + async fn run(&self) -> i32 { + sleep(Duration::from_millis(5)).await; + self.order.lock().unwrap().push("B"); + 2 + } + } + + let b = dag.add_task(BTask { + order: completion_order.clone(), + }); let c = { let order = completion_order.clone(); - dag.add_task(task_fn(move |(x, y): (i32, i32)| { + dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| { let order = order.clone(); - async move { - order.lock().unwrap().push("C"); - x + y - } + order.lock().unwrap().push("C"); + x + y })) .depends_on((a, b)) }; let d = { let order = completion_order.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let order = order.clone(); - async move { - order.lock().unwrap().push("D"); - x * 2 - } + order.lock().unwrap().push("D"); + x * 2 })) .depends_on(c) }; @@ -215,54 +245,70 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> { let execution_order = Arc::new(Mutex::new(Vec::new())); - // Create a chain: A -> B -> C -> D - let a: TaskHandle<_> = { - let order = execution_order.clone(); - dag.add_task(task_fn(move |_: ()| { - let order = order.clone(); - async move { - sleep(Duration::from_millis(20)).await; - order.lock().unwrap().push("A"); - 1 - } - })) + struct ATask { + order: Arc>>, } - .into(); - let b = { - let order = execution_order.clone(); - dag.add_task(task_fn(move |x: i32| { - let order = order.clone(); - async move { - order.lock().unwrap().push("B"); - sleep(Duration::from_millis(10)).await; - x + 1 - } - })) - .depends_on(a) - }; + #[task] + impl ATask { + async fn run(&self) -> i32 { + sleep(Duration::from_millis(20)).await; + self.order.lock().unwrap().push("A"); + 1 + } + } - let c = { - let order = execution_order.clone(); - dag.add_task(task_fn(move |x: i32| { - let order = order.clone(); - async move { - order.lock().unwrap().push("C"); - sleep(Duration::from_millis(10)).await; - x + 1 - } - })) - .depends_on(b) - }; + // Create a chain: A -> B -> C -> D + let a: TaskHandle<_> = dag + .add_task(ATask { + order: execution_order.clone(), + }) + .into(); + + struct BTask { + order: Arc>>, + } + + #[task] + impl BTask { + async fn run(&self, x: &i32) -> i32 { + self.order.lock().unwrap().push("B"); + sleep(Duration::from_millis(10)).await; + x + 1 + } + } + + let b = dag + .add_task(BTask { + order: execution_order.clone(), + }) + .depends_on(a); + + struct CTask { + order: Arc>>, + } + + #[task] + impl CTask { + async fn run(&self, x: &i32) -> i32 { + self.order.lock().unwrap().push("C"); + sleep(Duration::from_millis(10)).await; + x + 1 + } + } + + let c = dag + .add_task(CTask { + order: execution_order.clone(), + }) + .depends_on(b); let d = { let order = execution_order.clone(); - dag.add_task(task_fn(move |x: i32| { + dag.add_task(task_fn::(move |&x: &i32| { let order = order.clone(); - async move { - order.lock().unwrap().push("D"); - x + 1 - } + order.lock().unwrap().push("D"); + x + 1 })) .depends_on(c) }; @@ -282,82 +328,3 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_independent_tasks_dont_wait() -> DagResult<()> { - // Prove that independent tasks don't wait for each other - let dag = DagRunner::new(); - - let task_started = Arc::new(Mutex::new(Vec::new())); - let task_finished = Arc::new(Mutex::new(Vec::new())); - - // Create tasks with different durations - let fast1 = { - let started = task_started.clone(); - let finished = task_finished.clone(); - dag.add_task(task_fn(move |_: ()| { - let started = started.clone(); - let finished = finished.clone(); - async move { - started.lock().unwrap().push(("fast1", Instant::now())); - sleep(Duration::from_millis(10)).await; - finished.lock().unwrap().push(("fast1", Instant::now())); - "fast1" - } - })) - }; - - let slow = { - let started = task_started.clone(); - let finished = task_finished.clone(); - dag.add_task(task_fn(move |_: ()| { - let started = started.clone(); - let finished = finished.clone(); - async move { - started.lock().unwrap().push(("slow", Instant::now())); - sleep(Duration::from_millis(100)).await; - finished.lock().unwrap().push(("slow", Instant::now())); - "slow" - } - })) - }; - - let fast2 = { - let started = task_started.clone(); - let finished = task_finished.clone(); - dag.add_task(task_fn(move |_: ()| { - let started = started.clone(); - let finished = finished.clone(); - async move { - started.lock().unwrap().push(("fast2", Instant::now())); - sleep(Duration::from_millis(10)).await; - finished.lock().unwrap().push(("fast2", Instant::now())); - "fast2" - } - })) - }; - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - // Check results - assert_eq!(dag.get(fast1)?, "fast1"); - assert_eq!(dag.get(slow)?, "slow"); - assert_eq!(dag.get(fast2)?, "fast2"); - - // Verify that fast tasks finished before slow task - let finished = task_finished.lock().unwrap(); - let fast1_finish = finished.iter().find(|x| x.0 == "fast1").unwrap().1; - let fast2_finish = finished.iter().find(|x| x.0 == "fast2").unwrap().1; - let slow_finish = finished.iter().find(|x| x.0 == "slow").unwrap().1; - - assert!( - fast1_finish < slow_finish, - "fast1 should finish before slow" - ); - assert!( - fast2_finish < slow_finish, - "fast2 should finish before slow" - ); - - Ok(()) -} diff --git a/tests/parallelism/mod.rs b/tests/parallelism/mod.rs index 2720fe9..984f444 100644 --- a/tests/parallelism/mod.rs +++ b/tests/parallelism/mod.rs @@ -2,7 +2,6 @@ pub mod concurrent; pub mod execution; -pub mod proof; pub mod spawning; pub mod threads; pub mod timing; diff --git a/tests/parallelism/proof.rs b/tests/parallelism/proof.rs deleted file mode 100644 index 77d5db8..0000000 --- a/tests/parallelism/proof.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Parallelism proof tests - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner}; -use futures::FutureExt; -use std::time::{Duration, Instant}; -use tokio::time::sleep; - -#[tokio::test] -async fn test_parallelism_proof_1000_tasks() -> DagResult<()> { - // Proof that 1000 tasks sleeping 100ms each complete in ~100ms (not 100 seconds) - let dag = DagRunner::new(); - - let sleep_duration = Duration::from_millis(100); - let num_tasks = 1000u32; - - // Create 1000 independent tasks that each sleep for 100ms - let tasks: Vec<_> = (0..num_tasks) - .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { - sleep(sleep_duration).await; - i - })) - }) - .collect(); - - let start = Instant::now(); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - let elapsed = start.elapsed(); - - // Verify all tasks completed - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i as u32); - } - - // Sequential execution would take: 1000 * 100ms = 100 seconds - let sequential_time = sleep_duration * num_tasks; - - println!( - "Parallelism proof: {} tasks × {}ms completed in {:?} (sequential would be {:?})", - num_tasks, - sleep_duration.as_millis(), - elapsed, - sequential_time - ); - - // Allow generous overhead: should complete in less than 500ms (not 100 seconds!) - assert!( - elapsed < Duration::from_millis(500), - "Tasks appear to be running sequentially! Took {:?} instead of ~100ms", - elapsed - ); - - // Verify we achieved massive speedup - let speedup = sequential_time.as_secs_f64() / elapsed.as_secs_f64(); - assert!( - speedup > 200.0, - "Speedup too low: {:.1}x (expected > 200x)", - speedup - ); - - Ok(()) -} diff --git a/tests/parallelism/spawning.rs b/tests/parallelism/spawning.rs index 0273175..9e918dd 100644 --- a/tests/parallelism/spawning.rs +++ b/tests/parallelism/spawning.rs @@ -5,9 +5,6 @@ use dagx::{DagResult, DagRunner}; use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use std::sync::Mutex; -use std::time::Duration; -use tokio::time::sleep; #[tokio::test] async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { @@ -18,14 +15,12 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { // Create 10 independent tasks let tasks: Vec<_> = (0..10) .map(|i| { - dag.add_task(task_fn({ + dag.add_task(task_fn::<(), _, _>({ let value = spawn_count.clone(); move |_: ()| { let value = value.clone(); - async move { - value.fetch_add(1, Ordering::SeqCst); - i - } + value.fetch_add(1, Ordering::SeqCst); + i } })) }) @@ -49,57 +44,3 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_tasks_not_running_inline() -> DagResult<()> { - // Verify that tasks are spawned and CAN run on different threads than the caller - // Note: Small/fast tasks may run on the same thread if the scheduler chooses - use std::thread; - - let dag = DagRunner::new(); - let calling_thread = thread::current().id(); - let task_threads = Arc::new(Mutex::new(Vec::new())); - - // Create tasks with some work to encourage distribution - let tasks: Vec<_> = (0..20) - .map(|i| { - let threads = task_threads.clone(); - dag.add_task(task_fn(move |_: ()| { - let threads = threads.clone(); - async move { - threads.lock().unwrap().push(thread::current().id()); - - // Add some async work - sleep(Duration::from_millis(2)).await; - - i - } - })) - }) - .collect(); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - // Verify results - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task)?, i as i32); - } - - // Check if tasks ran on different threads than the caller - // This is a capability test - the scheduler MAY choose to use the same thread - let task_thread_ids = task_threads.lock().unwrap(); - let different_threads = task_thread_ids - .iter() - .filter(|&&tid| tid != calling_thread) - .count(); - - if different_threads == 0 { - eprintln!( - "Warning: All tasks ran on the calling thread. This may indicate tasks aren't \ - being spawned, or the scheduler chose not to distribute (valid for very fast tasks)" - ); - } - - // The key thing is that tasks completed successfully via spawner - Ok(()) -} diff --git a/tests/parallelism/threads.rs b/tests/parallelism/threads.rs index 39b8743..7845144 100644 --- a/tests/parallelism/threads.rs +++ b/tests/parallelism/threads.rs @@ -1,10 +1,12 @@ //! Tests for multi-threading and concurrent execution use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner, TaskHandle}; use futures::FutureExt; +use std::collections::HashSet; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; +use std::thread::ThreadId; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -17,27 +19,36 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> { let dag = DagRunner::new(); let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new())); + struct LayerTask { + ids: Arc>>, + idx: i32, + } + + #[task] + impl LayerTask { + async fn run(&self) -> i32 { + let tid = thread::current().id(); + self.ids.lock().unwrap().insert(tid); + + // Yield to give tokio reason to distribute + sleep(Duration::from_millis(5)).await; + + // Some CPU work too + let mut sum = 0; + for j in 0..1000 { + sum += j; + } + self.idx + sum + } + } + // Create 50 tasks with actual work to encourage multi-threading let tasks: Vec<_> = (0..50) .map(|i| { - let ids = thread_ids.clone(); - dag.add_task(task_fn(move |_: ()| { - let ids = ids.clone(); - async move { - let tid = thread::current().id(); - ids.lock().unwrap().insert(tid); - - // Do some actual async work to give tokio reason to distribute - sleep(Duration::from_millis(5)).await; - - // Some CPU work too - let mut sum = 0; - for j in 0..1000 { - sum += j; - } - i + sum - } - })) + dag.add_task(LayerTask { + ids: thread_ids.clone(), + idx: i, + }) }) .collect(); @@ -71,42 +82,50 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { let concurrent_count = Arc::new(AtomicUsize::new(0)); let max_concurrent = Arc::new(AtomicUsize::new(0)); + struct LayerTask { + count: Arc, + max: Arc, + idx: usize, + } + + #[task] + impl LayerTask { + async fn run(&self) -> usize { + // Increment concurrent counter + let current = self.count.fetch_add(1, Ordering::SeqCst) + 1; + + // Track maximum concurrency + let mut prev_max = self.max.load(Ordering::SeqCst); + while current > prev_max { + match self.max.compare_exchange_weak( + prev_max, + current, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => break, + Err(x) => prev_max = x, + } + } + + // Simulate work + sleep(Duration::from_millis(10)).await; + + // Decrement counter + self.count.fetch_sub(1, Ordering::SeqCst); + + self.idx + } + } + // Create 20 tasks that track concurrent execution let tasks: Vec<_> = (0..20) .map(|i| { - let count = concurrent_count.clone(); - let max = max_concurrent.clone(); - - dag.add_task(task_fn(move |_: ()| { - let count = count.clone(); - let max = max.clone(); - async move { - // Increment concurrent counter - let current = count.fetch_add(1, Ordering::SeqCst) + 1; - - // Track maximum concurrency - let mut prev_max = max.load(Ordering::SeqCst); - while current > prev_max { - match max.compare_exchange_weak( - prev_max, - current, - Ordering::SeqCst, - Ordering::SeqCst, - ) { - Ok(_) => break, - Err(x) => prev_max = x, - } - } - - // Simulate work - sleep(Duration::from_millis(10)).await; - - // Decrement counter - count.fetch_sub(1, Ordering::SeqCst); - - i - } - })) + dag.add_task(LayerTask { + count: concurrent_count.clone(), + max: max_concurrent.clone(), + idx: i, + }) }) .collect(); @@ -141,44 +160,56 @@ async fn test_diamond_parallel_execution() -> DagResult<()> { let concurrent_middle = Arc::new(AtomicUsize::new(0)); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); + + struct ParallelTask1 { + counter: Arc, + } + + #[task] + impl ParallelTask1 { + async fn run(&self, x: &i32) -> i32 { + self.counter.fetch_add(1, Ordering::SeqCst); + sleep(Duration::from_millis(20)).await; + let count = self.counter.load(Ordering::SeqCst); + self.counter.fetch_sub(1, Ordering::SeqCst); + assert!(count > 0, "Should see concurrent execution"); + x * 2 + } + } // These should run in parallel - let parallel1 = { - let counter = concurrent_middle.clone(); - dag.add_task(task_fn(move |x: i32| { - let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - sleep(Duration::from_millis(20)).await; - let count = counter.load(Ordering::SeqCst); - counter.fetch_sub(1, Ordering::SeqCst); - assert!(count > 0, "Should see concurrent execution"); - x * 2 - } - })) - .depends_on(source) - }; + let parallel1 = dag + .add_task(ParallelTask1 { + counter: concurrent_middle.clone(), + }) + .depends_on(source); - let parallel2 = { - let counter = concurrent_middle.clone(); - dag.add_task(task_fn(move |x: i32| { - let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - sleep(Duration::from_millis(20)).await; - let count = counter.load(Ordering::SeqCst); - counter.fetch_sub(1, Ordering::SeqCst); - // Both should be running - assert!(count > 0, "Should see concurrent execution"); - x * 3 - } - })) - .depends_on(source) - }; + struct ParallelTask2 { + counter: Arc, + } + + #[task] + impl ParallelTask2 { + async fn run(&self, x: &i32) -> i32 { + self.counter.fetch_add(1, Ordering::SeqCst); + sleep(Duration::from_millis(20)).await; + let count = self.counter.load(Ordering::SeqCst); + self.counter.fetch_sub(1, Ordering::SeqCst); + // Both should be running + assert!(count > 0, "Should see concurrent execution"); + x * 3 + } + } + + let parallel2 = dag + .add_task(ParallelTask2 { + counter: concurrent_middle.clone(), + }) + .depends_on(source); let sink = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((¶llel1, ¶llel2)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -188,197 +219,6 @@ async fn test_diamond_parallel_execution() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_parallel_branches_with_dependencies() -> DagResult<()> { - // Test that parallel branches execute independently but respect their own dependencies - let dag = DagRunner::new(); - - let branch_progress = Arc::new(Mutex::new(Vec::new())); - - // Source node - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); - - // Branch 1: source -> b1_1 -> b1_2 - let b1_1 = { - let progress = branch_progress.clone(); - dag.add_task(task_fn(move |x: i32| { - let progress = progress.clone(); - async move { - progress - .lock() - .unwrap() - .push(("b1_1_start", Instant::now())); - sleep(Duration::from_millis(30)).await; - progress.lock().unwrap().push(("b1_1_end", Instant::now())); - x * 2 - } - })) - .depends_on(source) - }; - - let b1_2 = { - let progress = branch_progress.clone(); - dag.add_task(task_fn(move |x: i32| { - let progress = progress.clone(); - async move { - progress - .lock() - .unwrap() - .push(("b1_2_start", Instant::now())); - sleep(Duration::from_millis(10)).await; - progress.lock().unwrap().push(("b1_2_end", Instant::now())); - x + 1 - } - })) - .depends_on(b1_1) - }; - - // Branch 2: source -> b2_1 -> b2_2 - let b2_1 = { - let progress = branch_progress.clone(); - dag.add_task(task_fn(move |x: i32| { - let progress = progress.clone(); - async move { - progress - .lock() - .unwrap() - .push(("b2_1_start", Instant::now())); - sleep(Duration::from_millis(10)).await; - progress.lock().unwrap().push(("b2_1_end", Instant::now())); - x * 3 - } - })) - .depends_on(source) - }; - - let b2_2 = { - let progress = branch_progress.clone(); - dag.add_task(task_fn(move |x: i32| { - let progress = progress.clone(); - async move { - progress - .lock() - .unwrap() - .push(("b2_2_start", Instant::now())); - sleep(Duration::from_millis(30)).await; - progress.lock().unwrap().push(("b2_2_end", Instant::now())); - x + 2 - } - })) - .depends_on(b2_1) - }; - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - // Verify results - assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1 - assert_eq!(dag.get(b2_2)?, 32); // (10 * 3) + 2 - - // Analyze timing to prove parallel execution - let progress = branch_progress.lock().unwrap(); - - // Find timestamps - let find_time = |name: &str| -> Instant { - progress - .iter() - .find(|(n, _)| *n == name) - .map(|(_, t)| *t) - .unwrap_or_else(|| panic!("Couldn't find {}", name)) - }; - - let b1_1_start = find_time("b1_1_start"); - let b1_1_end = find_time("b1_1_end"); - let b1_2_start = find_time("b1_2_start"); - let b2_1_start = find_time("b2_1_start"); - - // Branch 1 and Branch 2 first tasks should start nearly simultaneously - let branch_start_diff = if b1_1_start > b2_1_start { - b1_1_start - b2_1_start - } else { - b2_1_start - b1_1_start - }; - - // Use a more generous threshold to account for coverage instrumentation overhead - // 50ms is enough to prove they're parallel (not sequential) while being robust to overhead - assert!( - branch_start_diff < Duration::from_millis(50), - "Branches should start in parallel, diff was {:?}", - branch_start_diff - ); - - // b1_2 should only start after b1_1 ends - assert!(b1_2_start >= b1_1_end, "b1_2 started before b1_1 finished"); - - Ok(()) -} - -#[tokio::test] -async fn test_wide_fanout_parallel_execution() -> DagResult<()> { - // Test that a single source can fan out to many parallel tasks - let dag = DagRunner::new(); - - let max_concurrent = Arc::new(AtomicUsize::new(0)); - let current_concurrent = Arc::new(AtomicUsize::new(0)); - - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into(); - - // Create 50 tasks that all depend on the source - let dependents: Vec<_> = (0..50) - .map(|i| { - let max_c = max_concurrent.clone(); - let curr_c = current_concurrent.clone(); - - dag.add_task(task_fn(move |x: i32| { - let max_c = max_c.clone(); - let curr_c = curr_c.clone(); - async move { - // Track concurrency - let current = curr_c.fetch_add(1, Ordering::SeqCst) + 1; - - // Update max - let mut prev_max = max_c.load(Ordering::SeqCst); - while current > prev_max { - match max_c.compare_exchange_weak( - prev_max, - current, - Ordering::SeqCst, - Ordering::SeqCst, - ) { - Ok(_) => break, - Err(v) => prev_max = v, - } - } - - // Do work - sleep(Duration::from_millis(5)).await; - - curr_c.fetch_sub(1, Ordering::SeqCst); - x + i - } - })) - .depends_on(source) - }) - .collect(); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - // Verify results - for (i, task) in dependents.iter().enumerate() { - assert_eq!(dag.get(task)?, 42 + i as i32); - } - - // Check that we saw significant parallelism - let max_seen = max_concurrent.load(Ordering::SeqCst); - println!("Max concurrent in fanout: {}", max_seen); - assert!( - max_seen > 5, - "Expected significant parallelism in fanout, got {}", - max_seen - ); - - Ok(()) -} - #[tokio::test] async fn test_massive_parallel_fanout() -> DagResult<()> { // Test extreme parallelism with 1000 parallel tasks @@ -388,44 +228,52 @@ async fn test_massive_parallel_fanout() -> DagResult<()> { let max_concurrent = Arc::new(AtomicUsize::new(0)); let current = Arc::new(AtomicUsize::new(0)); - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); + + struct FanoutTask { + comp: Arc, + max: Arc, + curr: Arc, + idx: i32, + } + + #[task] + impl FanoutTask { + async fn run(&self, x: &i32) -> i32 { + // Track concurrency + let c = self.curr.fetch_add(1, Ordering::SeqCst) + 1; + + let mut prev_max = self.max.load(Ordering::SeqCst); + while c > prev_max { + match self.max.compare_exchange_weak( + prev_max, + c, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => break, + Err(v) => prev_max = v, + } + } + + // Quick work + sleep(Duration::from_millis(100)).await; + + self.curr.fetch_sub(1, Ordering::SeqCst); + self.comp.fetch_add(1, Ordering::SeqCst); + x + self.idx + } + } // Create 1000 tasks all depending on the same source let tasks: Vec<_> = (0..1000) .map(|i| { - let comp = completed.clone(); - let max = max_concurrent.clone(); - let curr = current.clone(); - - dag.add_task(task_fn(move |x: i32| { - let comp = comp.clone(); - let max = max.clone(); - let curr = curr.clone(); - async move { - // Track concurrency - let c = curr.fetch_add(1, Ordering::SeqCst) + 1; - - let mut prev_max = max.load(Ordering::SeqCst); - while c > prev_max { - match max.compare_exchange_weak( - prev_max, - c, - Ordering::SeqCst, - Ordering::SeqCst, - ) { - Ok(_) => break, - Err(v) => prev_max = v, - } - } - - // Quick work - sleep(Duration::from_micros(100)).await; - - curr.fetch_sub(1, Ordering::SeqCst); - comp.fetch_add(1, Ordering::SeqCst); - x + i - } - })) + dag.add_task(FanoutTask { + comp: completed.clone(), + max: max_concurrent.clone(), + curr: current.clone(), + idx: i, + }) .depends_on(source) }) .collect(); diff --git a/tests/parallelism/timing.rs b/tests/parallelism/timing.rs index 1cba4ea..76b1b8d 100644 --- a/tests/parallelism/timing.rs +++ b/tests/parallelism/timing.rs @@ -1,14 +1,20 @@ //! Tests for timing and speedup verification -use crate::common::task_fn; - -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner}; use futures::FutureExt; -use std::sync::Arc; -use std::sync::Mutex; use std::time::{Duration, Instant}; use tokio::time::sleep; +struct SleepTask(usize, Duration); + +#[task] +impl SleepTask { + async fn run(&self) -> usize { + sleep(self.1).await; + self.0 + } +} + #[tokio::test] async fn test_parallel_execution_speedup() -> DagResult<()> { // Prove parallel execution by comparing timing @@ -19,12 +25,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { // Create independent tasks that each take 50ms let tasks: Vec<_> = (0..num_tasks) - .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { - sleep(work_duration).await; - i - })) - }) + .map(|i| dag.add_task(SleepTask(i, work_duration))) .collect(); let start = Instant::now(); @@ -55,79 +56,3 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { Ok(()) } - -struct SyncTask; -#[dagx::task] -impl SyncTask { - fn run(&self, x: &i32) -> i32 { - // Synchronous task - no async/await - std::thread::sleep(Duration::from_millis(1)); // Blocking sleep - x * 2 - } -} - -struct AsyncTask; -#[dagx::task] -impl AsyncTask { - async fn run(&self, x: &i32) -> i32 { - // Async task - sleep(Duration::from_millis(1)).await; // Non-blocking sleep - x * 3 - } -} - -#[tokio::test] -async fn test_sync_tasks_dont_block_async_runtime() -> DagResult<()> { - // Test that sync tasks don't block the async runtime - let dag = DagRunner::new(); - - let start_times = Arc::new(Mutex::new(Vec::new())); - - // Mix of sync and async tasks - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into(); - - // Sync tasks - let sync1 = dag.add_task(SyncTask).depends_on(source); - let sync2 = dag.add_task(SyncTask).depends_on(source); - - // Async tasks - let async1 = dag.add_task(AsyncTask).depends_on(source); - let async2 = dag.add_task(AsyncTask).depends_on(source); - - // Track timing - let timing_task = { - let times = start_times.clone(); - dag.add_task(task_fn(move |_: ()| { - let times = times.clone(); - async move { - for i in 0..5 { - times - .lock() - .unwrap() - .push((format!("tick_{}", i), Instant::now())); - sleep(Duration::from_millis(5)).await; - } - "timing" - } - })) - }; - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; - - // Verify results - assert_eq!(dag.get(sync1)?, 20); - assert_eq!(dag.get(sync2)?, 20); - assert_eq!(dag.get(async1)?, 30); - assert_eq!(dag.get(async2)?, 30); - assert_eq!(dag.get(timing_task)?, "timing"); - - // Check that timing task made progress (wasn't blocked by sync tasks) - let times = start_times.lock().unwrap(); - assert_eq!( - times.len(), - 5, - "Timing task should have completed all ticks" - ); - - Ok(()) -} diff --git a/tests/patterns/butterfly.rs b/tests/patterns/butterfly.rs index 17d26e5..1d90275 100644 --- a/tests/patterns/butterfly.rs +++ b/tests/patterns/butterfly.rs @@ -14,7 +14,7 @@ async fn test_butterfly_network() -> DagResult<()> { // Layer 0: inputs let inputs: Vec<_> = (0..8) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { i as f64 })) + dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64)) .into() }) .collect(); @@ -26,25 +26,29 @@ async fn test_butterfly_network() -> DagResult<()> { let idx2 = i * 2 + 1; // Sum operation layer1.push( - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a + b) / 2.0 })) - .depends_on((&inputs[idx1], &inputs[idx2])), + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| { + (a + b) / 2.0 + })) + .depends_on((&inputs[idx1], &inputs[idx2])), ); // Difference operation layer1.push( - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a - b) / 2.0 })) - .depends_on((&inputs[idx1], &inputs[idx2])), + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| { + (a - b) / 2.0 + })) + .depends_on((&inputs[idx1], &inputs[idx2])), ); } // Layer 2: crossover connections let layer2: Vec<_> = vec![ - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b })) + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b)) .depends_on((&layer1[0], &layer1[4])), - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b })) + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b)) .depends_on((&layer1[1], &layer1[5])), - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b })) + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b)) .depends_on((&layer1[2], &layer1[6])), - dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b })) + dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b)) .depends_on((&layer1[3], &layer1[7])), ]; @@ -76,11 +80,13 @@ async fn test_recursive_butterfly() -> DagResult<()> { // Butterfly connections for i in 0..mid { let sum = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((&inputs[i], &inputs[i + mid])); let diff = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { (a - b).abs() })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| { + (a - b).abs() + })) .depends_on((&inputs[i], &inputs[i + mid])); outputs.push(sum); @@ -94,7 +100,7 @@ async fn test_recursive_butterfly() -> DagResult<()> { let inputs: Vec> = vec![10, 20, 30, 40] .into_iter() .map(|val| { - let task = dag.add_task(task_fn(move |_: ()| async move { val })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val)); task.into() }) .collect(); @@ -119,28 +125,36 @@ async fn test_benes_network() -> DagResult<()> { // 4 inputs let inputs: Vec<_> = (0..4) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { 1 << i })) + dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i)) .into() }) // Powers of 2 .collect(); // First stage: sort pairs let stage1: Vec<_> = vec![ - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) })) - .depends_on((&inputs[0], &inputs[1])), - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) })) - .depends_on((&inputs[0], &inputs[1])), - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) })) - .depends_on((&inputs[2], &inputs[3])), - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) })) - .depends_on((&inputs[2], &inputs[3])), + dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { + a.min(b) + })) + .depends_on((&inputs[0], &inputs[1])), + dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { + a.max(b) + })) + .depends_on((&inputs[0], &inputs[1])), + dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { + a.min(b) + })) + .depends_on((&inputs[2], &inputs[3])), + dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { + a.max(b) + })) + .depends_on((&inputs[2], &inputs[3])), ]; // Second stage with crossover let outputs: Vec<_> = vec![ - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b })) + dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b)) .depends_on((&stage1[0], &stage1[2])), - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b })) + dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b)) .depends_on((&stage1[1], &stage1[3])), ]; @@ -162,12 +176,10 @@ async fn test_shuffle_exchange_network() -> DagResult<()> { let inputs: Vec> = (0..8) .map(|i| { let counter = counter.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::Relaxed); - i - } + counter.fetch_add(1, Ordering::Relaxed); + i })) .into() }) @@ -178,7 +190,7 @@ async fn test_shuffle_exchange_network() -> DagResult<()> { .map(|i| { // Shuffle pattern: rotate bits let source = ((i << 1) | (i >> 2)) & 0x7; - dag.add_task(task_fn(move |x: i32| async move { x })) + dag.add_task(task_fn::(move |&x: &i32| x)) .depends_on(inputs[source]) }) .collect(); @@ -188,21 +200,13 @@ async fn test_shuffle_exchange_network() -> DagResult<()> { .flat_map(|i| { let idx = i * 2; vec![ - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { - if a > b { - b - } else { - a - } - })) + dag.add_task(task_fn::<(i32, i32), _, _>( + |(&a, &b): (&i32, &i32)| if a > b { b } else { a }, + )) .depends_on((&shuffled[idx], &shuffled[idx + 1])), - dag.add_task(task_fn(|(a, b): (i32, i32)| async move { - if a > b { - a - } else { - b - } - })) + dag.add_task(task_fn::<(i32, i32), _, _>( + |(&a, &b): (&i32, &i32)| if a > b { a } else { b }, + )) .depends_on((&shuffled[idx], &shuffled[idx + 1])), ] }) diff --git a/tests/patterns/complex.rs b/tests/patterns/complex.rs index f052eac..bf00063 100644 --- a/tests/patterns/complex.rs +++ b/tests/patterns/complex.rs @@ -11,10 +11,10 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> { let dag = DagRunner::new(); // Input validation - let input = dag.add_task(task_fn(|_: ()| async { 42 })); + let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); let validate = dag - .add_task(task_fn(|x: i32| async move { + .add_task(task_fn::(|&x: &i32| { if x > 0 && x < 100 { Ok(x) } else { @@ -25,37 +25,37 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> { // Process valid input let process = dag - .add_task(task_fn(|result: Result| async move { - match result { - Ok(x) => x * 2, + .add_task(task_fn::, _, _>( + |result: &Result| match result { + Ok(x) => *x * 2, Err(_) => -1, - } - })) + }, + )) .depends_on(validate); // Parallel enhancement steps let enhance1 = dag - .add_task(task_fn(|x: i32| async move { x + 10 })) + .add_task(task_fn::(|&x: &i32| x + 10)) .depends_on(process); let enhance2 = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(process); let enhance3 = dag - .add_task(task_fn(|x: i32| async move { x - 5 })) + .add_task(task_fn::(|&x: &i32| x - 5)) .depends_on(process); // Combine enhancements let combine = dag - .add_task(task_fn(|(e1, e2, e3): (i32, i32, i32)| async move { - vec![e1, e2, e3] - })) + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(&e1, &e2, &e3): (&i32, &i32, &i32)| vec![e1, e2, e3], + )) .depends_on((&enhance1, &enhance2, &enhance3)); // Final aggregation let final_result = dag - .add_task(task_fn(|values: Vec| async move { + .add_task(task_fn::, _, _>(|values: &Vec| { values.iter().sum::() })) .depends_on(combine); @@ -76,7 +76,7 @@ async fn test_map_reduce_pattern() -> DagResult<()> { // Map phase: process 16 inputs in parallel let mappers: Vec>> = (0..16) .map(|i| { - let task = dag.add_task(task_fn(move |_: ()| async move { + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| { // Simulate word count vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)] })); @@ -94,14 +94,23 @@ async fn test_map_reduce_pattern() -> DagResult<()> { match deps.len() { 4 => dag - .add_task(task_fn( + .add_task(task_fn::< + ( + Vec<(&str, usize)>, + Vec<(&str, usize)>, + Vec<(&str, usize)>, + Vec<(&str, usize)>, + ), + _, + _, + >( move |inputs: ( - Vec<(&str, usize)>, - Vec<(&str, usize)>, - Vec<(&str, usize)>, - Vec<(&str, usize)>, - )| async move { - let mut combined = Vec::new(); + &Vec<(&str, usize)>, + &Vec<(&str, usize)>, + &Vec<(&str, usize)>, + &Vec<(&str, usize)>, + )| { + let mut combined = Vec::<(&str, usize)>::new(); combined.extend(inputs.0); combined.extend(inputs.1); combined.extend(inputs.2); @@ -119,11 +128,11 @@ async fn test_map_reduce_pattern() -> DagResult<()> { let reducers: Vec<_> = shuffled .iter() .map(|shuffled_data| { - dag.add_task(task_fn(|data: Vec<(&str, usize)>| async move { + dag.add_task(task_fn::, _, _>(|data: &Vec<(&str, usize)>| { // Sum values by key let mut totals = std::collections::HashMap::new(); for (key, value) in data { - *totals.entry(key).or_insert(0) += value; + *totals.entry(*key).or_insert(0) += value; } totals.len() })) @@ -133,8 +142,8 @@ async fn test_map_reduce_pattern() -> DagResult<()> { // Final aggregation let final_task = dag - .add_task(task_fn( - |(r1, r2, r3, r4): (usize, usize, usize, usize)| async move { r1 + r2 + r3 + r4 }, + .add_task(task_fn::<(_, _, _, _), _, _>( + |(r1, r2, r3, r4): (&usize, &usize, &usize, &usize)| r1 + r2 + r3 + r4, )) .depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3])); @@ -152,36 +161,30 @@ async fn test_pipeline_with_feedback() -> DagResult<()> { let iteration_count = Arc::new(AtomicUsize::new(0)); // Initial value - let init = dag.add_task(task_fn(|_: ()| async { 1 })); + let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); // Stage 1: Transform let stage1 = dag - .add_task(task_fn({ + .add_task(task_fn::({ let counter = iteration_count.clone(); - move |x: i32| { + move |x: &i32| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - x * 2 - } + counter.fetch_add(1, Ordering::SeqCst); + x * 2 } })) .depends_on(init); // Stage 2: Filter let stage2 = dag - .add_task(task_fn(|x: i32| async move { - if x < 100 { - Some(x) - } else { - None - } - })) + .add_task(task_fn::( + |&x: &i32| if x < 100 { Some(x) } else { None }, + )) .depends_on(stage1); // Stage 3: Accumulate let stage3 = dag - .add_task(task_fn(|opt: Option| async move { + .add_task(task_fn::, _, _>(|opt: &Option| { opt.map(|x| x + 10).unwrap_or(0) })) .depends_on(stage2); @@ -189,7 +192,7 @@ async fn test_pipeline_with_feedback() -> DagResult<()> { // Note: Can't create actual cycles in DAG, but we simulate feedback // by having a separate "feedback" task that would feed back in a real system let _feedback = dag - .add_task(task_fn(|x: i32| async move { + .add_task(task_fn::(|&x: &i32| { // In a real feedback loop, this would feed back to stage1 x / 2 })) @@ -210,7 +213,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> { // Scatter: distribute work to multiple workers let source: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { + .add_task(task_fn::<(), _, _>(|_: ()| { vec![10, 20, 30, 40, 50, 60, 70, 80] })) .into(); @@ -218,7 +221,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> { // Workers process chunks let workers: Vec<_> = (0..4) .map(|worker_id| { - dag.add_task(task_fn(move |data: Vec| async move { + dag.add_task(task_fn::, _, _>(move |data: &Vec| { // Each worker processes a chunk let chunk_size = data.len() / 4; let start = worker_id * chunk_size; @@ -236,8 +239,8 @@ async fn test_scatter_gather_pattern() -> DagResult<()> { // Gather: collect results from all workers let gather = dag - .add_task(task_fn( - |(w0, w1, w2, w3): (i32, i32, i32, i32)| async move { w0 + w1 + w2 + w3 }, + .add_task(task_fn::<(i32, i32, i32, i32), _, _>( + |(w0, w1, w2, w3): (&i32, &i32, &i32, &i32)| w0 + w1 + w2 + w3, )) .depends_on((&workers[0], &workers[1], &workers[2], &workers[3])); @@ -255,14 +258,12 @@ async fn test_fork_join_with_barriers() -> DagResult<()> { let phase_counter = Arc::new(AtomicUsize::new(0)); // Fork phase - let parent = dag.add_task(task_fn({ + let parent = dag.add_task(task_fn::<(), _, _>({ let counter = phase_counter.clone(); move |_: ()| { let counter = counter.clone(); - async move { - counter.store(1, Ordering::SeqCst); - 100 - } + counter.store(1, Ordering::SeqCst); + 100 } })); @@ -273,12 +274,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> { for i in 0..3 { let counter = phase_counter.clone(); let task = dag - .add_task(task_fn(move |x: i32| { + .add_task(task_fn::(move |&x: &i32| { let counter = counter.clone(); - async move { - assert_eq!(counter.load(Ordering::SeqCst), 1); - x + i - } + assert_eq!(counter.load(Ordering::SeqCst), 1); + x + i })) .depends_on(prev1); path1.push(task); @@ -290,12 +289,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> { for i in 0..3 { let counter = phase_counter.clone(); let task = dag - .add_task(task_fn(move |x: i32| { + .add_task(task_fn::(move |&x: &i32| { let counter = counter.clone(); - async move { - assert_eq!(counter.load(Ordering::SeqCst), 1); - x * 2 + i - } + assert_eq!(counter.load(Ordering::SeqCst), 1); + x * 2 + i })) .depends_on(prev2); path2.push(task); @@ -304,28 +301,24 @@ async fn test_fork_join_with_barriers() -> DagResult<()> { // Barrier: wait for all paths let barrier = dag - .add_task(task_fn({ + .add_task(task_fn::<(i32, i32), _, _>({ let counter = phase_counter.clone(); - move |(p1, p2): (i32, i32)| { + move |(p1, p2): (&i32, &i32)| { let counter = counter.clone(); - async move { - counter.store(2, Ordering::SeqCst); - p1 + p2 - } + counter.store(2, Ordering::SeqCst); + p1 + p2 } })) .depends_on((&path1[2], &path2[2])); // Join phase let join = dag - .add_task(task_fn({ + .add_task(task_fn::({ let counter = phase_counter.clone(); - move |x: i32| { + move |&x: &i32| { let counter = counter.clone(); - async move { - assert_eq!(counter.load(Ordering::SeqCst), 2); - x - } + assert_eq!(counter.load(Ordering::SeqCst), 2); + x } })) .depends_on(barrier); diff --git a/tests/patterns/grid.rs b/tests/patterns/grid.rs index 27c8c8e..40a2a8c 100644 --- a/tests/patterns/grid.rs +++ b/tests/patterns/grid.rs @@ -14,10 +14,10 @@ async fn test_2d_grid_pattern() -> DagResult<()> { // Initialize first row and column #[allow(clippy::needless_range_loop)] for i in 0..4 { - let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32)); grid[0][i] = Some(task.into()); if i > 0 { - let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 * 10 })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32 * 10)); grid[i][0] = Some(task.into()); } } @@ -29,7 +29,9 @@ async fn test_2d_grid_pattern() -> DagResult<()> { let left = grid[i][j - 1].as_ref().unwrap(); let task = dag - .add_task(task_fn(move |(a, l): (i32, i32)| async move { a + l })) + .add_task(task_fn::<(i32, i32), _, _>(move |(a, l): (&i32, &i32)| { + a + l + })) .depends_on((above, left)); grid[i][j] = Some(task); } @@ -54,7 +56,7 @@ async fn test_3d_grid_pattern() -> DagResult<()> { vec![vec![vec![None; size]; size]; size]; // Initialize origin - let origin = dag.add_task(task_fn(|_: ()| async { 1 })); + let origin = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); grid[0][0][0] = Some(origin.into()); // Fill the 3D grid @@ -76,18 +78,20 @@ async fn test_3d_grid_pattern() -> DagResult<()> { let task_handle: dagx::TaskHandle = match deps.len() { 0 => { - let t = dag.add_task(task_fn(move |_: ()| async move { x + y + z })); + let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| x + y + z)); t.into() } 1 => dag - .add_task(task_fn(move |prev: usize| async move { prev + 1 })) + .add_task(task_fn::(move |prev: &usize| prev + 1)) .depends_on(deps[0]), 2 => dag - .add_task(task_fn(move |(a, b): (usize, usize)| async move { a + b })) + .add_task(task_fn::<(usize, usize), _, _>( + move |(a, b): (&usize, &usize)| a + b, + )) .depends_on((&deps[0], &deps[1])), 3 => dag - .add_task(task_fn( - move |(a, b, c): (usize, usize, usize)| async move { a + b + c }, + .add_task(task_fn::<(usize, usize, usize), _, _>( + move |(a, b, c): (&usize, &usize, &usize)| a + b + c, )) .depends_on((&deps[0], &deps[1], &deps[2])), _ => unreachable!(), @@ -120,7 +124,7 @@ async fn test_hexagonal_grid() -> DagResult<()> { let mut hex_grid = std::collections::HashMap::new(); // Center hex - let center_builder = dag.add_task(task_fn(|_: ()| async { 100 })); + let center_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); let center: dagx::TaskHandle = center_builder.into(); hex_grid.insert((0, 0), center); @@ -129,9 +133,7 @@ async fn test_hexagonal_grid() -> DagResult<()> { for (i, &(dq, dr)) in directions.iter().enumerate() { let neighbor: dagx::TaskHandle = dag - .add_task(task_fn( - move |c: i32| async move { c + (i as i32 + 1) * 10 }, - )) + .add_task(task_fn::(move |c: &i32| c + (i as i32 + 1) * 10)) .depends_on(center); hex_grid.insert((dq, dr), neighbor); } @@ -159,15 +161,15 @@ async fn test_hexagonal_grid() -> DagResult<()> { .collect(); let task: dagx::TaskHandle = if neighbors.len() == 2 { - dag.add_task(task_fn(move |(a, b): (i32, i32)| async move { + dag.add_task(task_fn::<(i32, i32), _, _>(move |(a, b): (&i32, &i32)| { (a + b) / 2 + q + r })) .depends_on((&neighbors[0], &neighbors[1])) } else if neighbors.len() == 1 { - dag.add_task(task_fn(move |n: i32| async move { n + q + r })) + dag.add_task(task_fn::(move |n: &i32| n + q + r)) .depends_on(neighbors[0]) } else { - let t = dag.add_task(task_fn(move |_: ()| async move { q + r })); + let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| q + r)); t.into() }; @@ -193,7 +195,7 @@ async fn test_toroidal_grid() -> DagResult<()> { #[allow(clippy::needless_range_loop)] for i in 0..SIZE { for j in 0..SIZE { - let task = dag.add_task(task_fn(move |_: ()| async move { (i * SIZE + j) as i32 })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| (i * SIZE + j) as i32)); grid[i][j] = Some(task.into()); } } @@ -211,8 +213,8 @@ async fn test_toroidal_grid() -> DagResult<()> { let right = grid[i][(j + 1) % SIZE].as_ref().unwrap(); let task = dag - .add_task(task_fn( - move |(c, u, d, l, r): (i32, i32, i32, i32, i32)| async move { + .add_task(task_fn::<(_, _, _, _, _), _, _>( + move |(c, u, d, l, r): (&i32, &i32, &i32, &i32, &i32)| { // Average of self and 4 neighbors (c + u + d + l + r) / 5 }, diff --git a/tests/patterns/tree.rs b/tests/patterns/tree.rs index a970d60..2346697 100644 --- a/tests/patterns/tree.rs +++ b/tests/patterns/tree.rs @@ -10,7 +10,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> { // Create 8 leaf nodes let leaves: Vec<_> = (0..8) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { 1 << i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i))) .collect(); // Build binary tree reduction @@ -23,7 +23,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> { for chunk in current_level.chunks(2) { if chunk.len() == 2 { let task = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((&chunk[0], &chunk[1])); next_level.push(task); } else { @@ -51,7 +51,7 @@ async fn test_n_ary_tree() -> DagResult<()> { // Build a 3-ary tree (each node has 3 children) fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle { if depth == 0 { - let task = dag.add_task(task_fn(move |_: ()| async move { value })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value)); return task.into(); } @@ -59,9 +59,9 @@ async fn test_n_ary_tree() -> DagResult<()> { .map(|i| build_tree(dag, depth - 1, value * 3 + i)) .collect(); - dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move { - value + a + b + c - })) + dag.add_task(task_fn::<(i32, i32, i32), _, _>( + move |(a, b, c): (&i32, &i32, &i32)| value + a + b + c, + )) .depends_on((&children[0], &children[1], &children[2])) } @@ -88,30 +88,30 @@ async fn test_unbalanced_tree() -> DagResult<()> { // / \ // f g - let f: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into(); - let g = dag.add_task(task_fn(|_: ()| async { 2 })); + let f: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); + let g = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); let c = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(f); - let d_builder = dag.add_task(task_fn(|_: ()| async { 3 })); + let d_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); let d: dagx::TaskHandle = d_builder.into(); let e = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(g); let a = dag - .add_task(task_fn(|(c, d): (i32, i32)| async move { c + d })) + .add_task(task_fn::<(i32, i32), _, _>(|(c, d): (&i32, &i32)| c + d)) .depends_on((&c, &d)); let b = dag - .add_task(task_fn(|e: i32| async move { e + 10 })) + .add_task(task_fn::(|e: &i32| e + 10)) .depends_on(e); let root = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b)) .depends_on((&a, &b)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -131,37 +131,39 @@ async fn test_trie_like_structure() -> DagResult<()> { // Build a trie-like structure for paths let root: TaskHandle<_> = dag - .add_task(task_fn(|_: ()| async { "root".to_string() })) + .add_task(task_fn::<(), _, _>(|_: ()| "root".to_string())) .into(); // First level let usr = dag - .add_task(task_fn(|r: String| async move { format!("{}/usr", r) })) + .add_task(task_fn::(|r: &String| format!("{}/usr", r))) .depends_on(root); let etc = dag - .add_task(task_fn(|r: String| async move { format!("{}/etc", r) })) + .add_task(task_fn::(|r: &String| format!("{}/etc", r))) .depends_on(root); let var = dag - .add_task(task_fn(|r: String| async move { format!("{}/var", r) })) + .add_task(task_fn::(|r: &String| format!("{}/var", r))) .depends_on(root); // Second level let usr_bin = dag - .add_task(task_fn(|u: String| async move { format!("{}/bin", u) })) + .add_task(task_fn::(|u: &String| format!("{}/bin", u))) .depends_on(usr); let usr_lib = dag - .add_task(task_fn(|u: String| async move { format!("{}/lib", u) })) + .add_task(task_fn::(|u: &String| format!("{}/lib", u))) .depends_on(usr); let etc_config = dag - .add_task(task_fn(|e: String| async move { format!("{}/config", e) })) + .add_task(task_fn::(|e: &String| { + format!("{}/config", e) + })) .depends_on(etc); let var_log = dag - .add_task(task_fn(|v: String| async move { format!("{}/log", v) })) + .add_task(task_fn::(|v: &String| format!("{}/log", v))) .depends_on(var); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -189,7 +191,7 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> { node_id: usize, ) -> dagx::TaskHandle { if depth == 0 { - let task = dag.add_task(task_fn(move |_: ()| async move { node_id })); + let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| node_id)); return task.into(); } @@ -199,20 +201,18 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> { match k { 2 => dag - .add_task(task_fn(move |(a, b): (usize, usize)| async move { - node_id + a + b - })) + .add_task(task_fn::<(usize, usize), _, _>( + move |(a, b): (&usize, &usize)| node_id + a + b, + )) .depends_on((&children[0], &children[1])), 3 => dag - .add_task(task_fn( - move |(a, b, c): (usize, usize, usize)| async move { node_id + a + b + c }, + .add_task(task_fn::<(usize, usize, usize), _, _>( + move |(a, b, c): (&usize, &usize, &usize)| node_id + a + b + c, )) .depends_on((&children[0], &children[1], &children[2])), 4 => dag - .add_task(task_fn( - move |(a, b, c, d): (usize, usize, usize, usize)| async move { - node_id + a + b + c + d - }, + .add_task(task_fn::<(usize, usize, usize, usize), _, _>( + move |(a, b, c, d): (&usize, &usize, &usize, &usize)| node_id + a + b + c + d, )) .depends_on((&children[0], &children[1], &children[2], &children[3])), _ => panic!("Unsupported k value"), @@ -236,7 +236,7 @@ async fn test_merkle_tree_pattern() -> DagResult<()> { // Simulate a Merkle tree for 8 data blocks let data_blocks: Vec<_> = (0..8) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { // Simulate hash of data block format!("hash_{}", i) })) @@ -253,9 +253,9 @@ async fn test_merkle_tree_pattern() -> DagResult<()> { for pair in current_level.chunks(2) { if pair.len() == 2 { let hash_task = dag - .add_task(task_fn(|(left, right): (String, String)| async move { - format!("hash({}_{})", left, right) - })) + .add_task(task_fn::<(String, String), _, _>( + |(left, right): (&String, &String)| format!("hash({}_{})", left, right), + )) .depends_on((&pair[0], &pair[1])); next_level.push(hash_task); } else { diff --git a/tests/performance/memory.rs b/tests/performance/memory.rs deleted file mode 100644 index 6f5c49c..0000000 --- a/tests/performance/memory.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Memory usage tests - -use crate::common::task_fn; -use dagx::DagRunner; -use futures::FutureExt; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -#[tokio::test] -async fn test_memory_usage_10000_nodes() { - let dag = Arc::new(DagRunner::new()); - - // Track approximate memory usage - let before_build = memory_usage_hint(); - - // Create 10,000 nodes - let mut tasks: Vec<_> = (0..10_000) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) - .collect(); - - let after_build = memory_usage_hint(); - let build_memory = after_build.saturating_sub(before_build); - - println!("Memory for 10k nodes: ~{} bytes", build_memory); - - // Execute - 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); - - println!("Additional memory for execution: ~{} bytes", exec_memory); - - // Verify some results - assert_eq!(dag.get(tasks.swap_remove(9999)).unwrap(), 9999); - assert_eq!(dag.get(tasks.swap_remove(0)).unwrap(), 0); - - // Memory should be reasonable (< 100MB for 10k simple nodes) - assert!( - build_memory < 100_000_000, - "Used too much memory: {} bytes", - build_memory - ); -} - -fn memory_usage_hint() -> usize { - // Simple memory hint (not precise) - static COUNTER: AtomicUsize = AtomicUsize::new(0); - COUNTER.fetch_add(100_000, Ordering::SeqCst) -} diff --git a/tests/performance/mod.rs b/tests/performance/mod.rs index a9a7426..f402a53 100644 --- a/tests/performance/mod.rs +++ b/tests/performance/mod.rs @@ -1,4 +1,102 @@ -//! Performance tests module +//! Stress tests and benchmarks -pub mod memory; -pub mod stress; +use crate::common::task_fn; +use dagx::DagRunner; +use futures::FutureExt; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +#[tokio::test] +async fn test_deep_chain_100_levels() { + let dag = Arc::new(DagRunner::new()); + + // Create a chain 100 levels deep + let start = Instant::now(); + let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); + let mut handles = vec![first.into()]; + + for _ in 1..=100 { + let next = dag + .add_task(task_fn::(move |&x: &i32| x + 1)) + .depends_on(handles.last().unwrap()); + handles.push(next); + } + let build_time = start.elapsed(); + + println!("Built 100-level chain in {:?}", build_time); + + // Execute + let start = Instant::now(); + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); + let exec_time = start.elapsed(); + + println!("Executed 100-level chain in {:?}", exec_time); + assert!( + exec_time < Duration::from_secs(5), + "Execution took too long" + ); + + // Verify result + assert_eq!(dag.get(handles.last().unwrap()).unwrap(), 100); +} + +#[tokio::test] +async fn test_stress_mixed_patterns() { + // Stress test with mixed DAG patterns + let dag = Arc::new(DagRunner::new()); + let mut all_tasks = vec![]; + + // Add various patterns + for i in 0..10 { + // Linear chains + let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 1000)); + let mut chain_handles = vec![first.into()]; + for j in 1..10 { + let next = dag + .add_task(task_fn::(move |&x: &i32| x + j)) + .depends_on(chain_handles.last().unwrap()); + chain_handles.push(next); + } + all_tasks.push(*chain_handles.last().unwrap()); + + // Diamonds + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 100)); + let source_handle: dagx::TaskHandle = source.into(); + let left = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(source_handle); + let right = dag + .add_task(task_fn::(|&x: &i32| x * 3)) + .depends_on(source_handle); + let merge = dag + .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) + .depends_on((&left, &right)); + all_tasks.push(merge); + + // Fan-outs + let hub = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); + let hub_handle: dagx::TaskHandle = hub.into(); + for j in 0..5 { + let fan = dag + .add_task(task_fn::(move |&x: &i32| x + j * 10)) + .depends_on(hub_handle); + all_tasks.push(fan); + } + } + + // Execute + let start = Instant::now(); + 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); + + // Verify some results + for task in all_tasks.iter().take(10) { + assert!(dag.get(task).is_ok()); + } +} diff --git a/tests/performance/stress.rs b/tests/performance/stress.rs deleted file mode 100644 index 6f96275..0000000 --- a/tests/performance/stress.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Stress tests and benchmarks - -use crate::common::task_fn; -use dagx::DagRunner; -use futures::FutureExt; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -#[tokio::test] -async fn test_deep_chain_100_levels() { - let dag = Arc::new(DagRunner::new()); - - // Create a chain 100 levels deep - let start = Instant::now(); - let first = dag.add_task(task_fn(|_: ()| async { 0 })); - let mut handles = vec![first.into()]; - - for _ in 1..=100 { - let next = dag - .add_task(task_fn(move |x: i32| async move { x + 1 })) - .depends_on(handles.last().unwrap()); - handles.push(next); - } - let build_time = start.elapsed(); - - println!("Built 100-level chain in {:?}", build_time); - - // Execute - let start = Instant::now(); - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - let exec_time = start.elapsed(); - - println!("Executed 100-level chain in {:?}", exec_time); - assert!( - exec_time < Duration::from_secs(5), - "Execution took too long" - ); - - // Verify result - assert_eq!(dag.get(handles.last().unwrap()).unwrap(), 100); -} - -#[tokio::test] -async fn test_stress_mixed_patterns() { - // Stress test with mixed DAG patterns - let dag = Arc::new(DagRunner::new()); - let mut all_tasks = vec![]; - - // Add various patterns - for i in 0..10 { - // Linear chains - let first = dag.add_task(task_fn(move |_: ()| async move { i * 1000 })); - let mut chain_handles = vec![first.into()]; - for j in 1..10 { - let next = dag - .add_task(task_fn(move |x: i32| async move { x + j })) - .depends_on(chain_handles.last().unwrap()); - chain_handles.push(next); - } - all_tasks.push(*chain_handles.last().unwrap()); - - // Diamonds - let source = dag.add_task(task_fn(move |_: ()| async move { i * 100 })); - let source_handle: dagx::TaskHandle = source.into(); - let left = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) - .depends_on(source_handle); - let right = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) - .depends_on(source_handle); - let merge = dag - .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) - .depends_on((&left, &right)); - all_tasks.push(merge); - - // Fan-outs - let hub = dag.add_task(task_fn(move |_: ()| async move { i })); - let hub_handle: dagx::TaskHandle = hub.into(); - for j in 0..5 { - let fan = dag - .add_task(task_fn(move |x: i32| async move { x + j * 10 })) - .depends_on(hub_handle); - all_tasks.push(fan); - } - } - - // Execute - let start = Instant::now(); - 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); - - // Verify some results - for task in all_tasks.iter().take(10) { - assert!(dag.get(task).is_ok()); - } -} diff --git a/tests/runtimes/async_std.rs b/tests/runtimes/async_std.rs index 028e15d..9ceb24e 100644 --- a/tests/runtimes/async_std.rs +++ b/tests/runtimes/async_std.rs @@ -37,7 +37,7 @@ async fn test_parallel_execution_async_std() { let dag = DagRunner::new(); let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) .collect(); dag.run(async_std::task::spawn).await.unwrap(); @@ -55,10 +55,10 @@ async fn test_complex_dependencies_async_std() { let b = dag.add_task(Value(20)); let sum = dag.add_task(Add).depends_on((a, b)); let double = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(sum); dag.run(async_std::task::spawn).await.unwrap(); - assert_eq!(dag.get(double).unwrap(), 60); + assert_eq!(dag.get(double).unwrap().clone(), 60); } diff --git a/tests/runtimes/quirky.rs b/tests/runtimes/quirky.rs index 9bf6a1a..74e6f47 100644 --- a/tests/runtimes/quirky.rs +++ b/tests/runtimes/quirky.rs @@ -32,10 +32,10 @@ fn test_async_executor_basic() { let future = async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 10 })); - let y = dag.add_task(task_fn(|_: ()| async { 20 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); let sum = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((x, y)); dag.run(|fut| executor.spawn(fut)).await.unwrap(); @@ -54,18 +54,18 @@ fn test_async_executor_complex_dag() { let dag = DagRunner::new(); // Create a diamond pattern - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 5 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)).into(); let left = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(source); let right = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) + .add_task(task_fn::(|&x: &i32| x * 3)) .depends_on(source); let sink = dag - .add_task(task_fn(|(l, r): (i32, i32)| async move { l + r })) + .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) .depends_on((&left, &right)); dag.run(|fut| executor.spawn(fut)).await.unwrap(); @@ -88,12 +88,10 @@ fn test_async_executor_parallel_tasks() { let tasks: Vec<_> = (0..10) .map(|i| { let c = counter.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let c = c.clone(); - async move { - c.fetch_add(1, Ordering::SeqCst); - i - } + c.fetch_add(1, Ordering::SeqCst); + i })) }) .collect(); @@ -120,7 +118,7 @@ fn test_async_executor_with_local_spawn() { let future = async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 42 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); dag.run(|fut| executor.spawn(fut)).await.unwrap(); @@ -138,14 +136,14 @@ fn test_async_executor_deep_chain() { let dag = DagRunner::new(); // Build a chain of 50 tasks - let start = dag.add_task(task_fn(|_: ()| async { 0 })); + let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let mut current = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(start); for _ in 1..50 { current = dag - .add_task(task_fn(|x: i32| async move { x + 1 })) + .add_task(task_fn::(|&x: &i32| x + 1)) .depends_on(current); } @@ -167,9 +165,9 @@ fn test_pollster_basic() { pollster::block_on(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 100 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); let doubled = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(x); dag.run(|fut| async { pollster::block_on(fut) }) @@ -186,11 +184,11 @@ fn test_pollster_complex() { let dag = DagRunner::new(); // Fan-out pattern - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 7 })).into(); + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)).into(); let tasks: Vec<_> = (0..5) .map(|i| { - dag.add_task(task_fn(move |x: i32| async move { x * i })) + dag.add_task(task_fn::(move |&x: &i32| x * i)) .depends_on(source) }) .collect(); @@ -211,7 +209,7 @@ fn test_pollster_error_handling() { let dag = DagRunner::new(); // Task that returns Result - let task = dag.add_task(task_fn(|_: ()| async { Ok::(42) })); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::(42))); dag.run(|fut| async { pollster::block_on(fut) }) .await @@ -230,10 +228,10 @@ fn test_futures_executor_basic() { futures_executor::block_on(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 15 })); - let y = dag.add_task(task_fn(|_: ()| async { 25 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25)); let product = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a * b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b)) .depends_on((x, y)); let spawner = ThreadPool::new().unwrap(); @@ -256,7 +254,7 @@ fn test_futures_executor_threadpool() { // Create multiple independent tasks let tasks: Vec<_> = (0..20) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * i))) .collect(); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) @@ -281,18 +279,18 @@ fn test_futures_executor_diamond_pattern() { futures_executor::block_on(async { let dag = DagRunner::new(); - let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 8 })).into(); + let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 8)).into(); let b = dag - .add_task(task_fn(|x: i32| async move { x + 2 })) + .add_task(task_fn::(|&x: &i32| x + 2)) .depends_on(a); let c = dag - .add_task(task_fn(|x: i32| async move { x - 2 })) + .add_task(task_fn::(|&x: &i32| x - 2)) .depends_on(a); let d = dag - .add_task(task_fn(|(x, y): (i32, i32)| async move { x * y })) + .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x * y)) .depends_on((&b, &c)); let spawner = ThreadPool::new().unwrap(); @@ -318,7 +316,7 @@ fn test_all_runtimes_compatibility() { let executor = async_executor::Executor::new(); executor.run(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 123 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); dag.run(|fut| executor.spawn(fut)).await.unwrap(); assert_eq!(dag.get(x).unwrap(), 123); }); @@ -328,7 +326,7 @@ fn test_all_runtimes_compatibility() { { pollster::block_on(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 123 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); dag.run(|fut| async { pollster::block_on(fut) }) .await .unwrap(); @@ -340,7 +338,7 @@ fn test_all_runtimes_compatibility() { { futures_executor::block_on(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 123 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); let spawner = ThreadPool::new().unwrap(); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) .await @@ -360,7 +358,7 @@ fn test_runtime_edge_cases() { let dag = DagRunner::new(); // This tests that our API works even with !Send futures - let x = dag.add_task(task_fn(|_: ()| async { + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| { // Local state 42 @@ -386,7 +384,7 @@ fn test_async_executor_stress() { // Create 100 tasks let tasks: Vec<_> = (0..100) .map(|i| { - dag.add_task(task_fn(move |_: ()| async move { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { // Small delay to test executor scheduling i })) @@ -406,7 +404,7 @@ fn test_pollster_nested_blocks() { // Test nested blocking - edge case pollster::block_on(async { let dag1 = DagRunner::new(); - let x = dag1.add_task(task_fn(|_: ()| async { 10 })); + let x = dag1.add_task(task_fn::<(), _, _>(|_: ()| 10)); dag1.run(|fut| async { pollster::block_on(fut) }) .await @@ -425,7 +423,7 @@ fn test_futures_executor_multi_pool() { pool1.spawn_ok(async move { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 50 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 50)); dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) .await @@ -459,8 +457,8 @@ fn test_sync_tasks_on_async_executor() { executor.run(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 5 })); - let y = dag.add_task(task_fn(|_: ()| async { 7 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)); let sum = dag.add_task(SyncAdd).depends_on((x, y)); dag.run(|fut| executor.spawn(fut)).await.unwrap(); @@ -483,7 +481,7 @@ fn test_sync_tasks_on_pollster() { pollster::block_on(async { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 21 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 21)); let doubled = dag.add_task(SyncDouble).depends_on(x); dag.run(|fut| async { pollster::block_on(fut) }) diff --git a/tests/runtimes/smol.rs b/tests/runtimes/smol.rs index 6d6ad27..35624e2 100644 --- a/tests/runtimes/smol.rs +++ b/tests/runtimes/smol.rs @@ -40,7 +40,7 @@ fn test_parallel_execution_smol() { let dag = DagRunner::new(); let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) .collect(); dag.run(smol::spawn).await.unwrap(); @@ -60,11 +60,11 @@ fn test_complex_dependencies_smol() { let b = dag.add_task(Value(20)); let sum = dag.add_task(Add).depends_on((a, b)); let double = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) + .add_task(task_fn::(|&x: &i32| x * 2)) .depends_on(sum); dag.run(smol::spawn).await.unwrap(); - assert_eq!(dag.get(double).unwrap(), 60); + assert_eq!(dag.get(double).unwrap().clone(), 60); }); } diff --git a/tests/task_patterns/combined.rs b/tests/task_patterns/combined.rs index e670fbb..d29b81f 100644 --- a/tests/task_patterns/combined.rs +++ b/tests/task_patterns/combined.rs @@ -41,8 +41,8 @@ async fn test_all_three_task_patterns() { let dag = DagRunner::new(); // Create initial values - let x = dag.add_task(task_fn(|_: ()| async { 5 })); - let y = dag.add_task(task_fn(|_: ()| async { 3 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); // Pattern 1: Stateless addition let sum = dag.add_task(StatelessAdd).depends_on((x, y)); // 5 + 3 = 8 @@ -72,28 +72,22 @@ async fn test_parallel_execution() { let c2 = counter.clone(); let c3 = counter.clone(); - let t1 = dag.add_task(task_fn(move |_: ()| { + let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { let c = c1.clone(); - async move { - c.fetch_add(1, Ordering::SeqCst); - 1 - } + c.fetch_add(1, Ordering::SeqCst); + 1 })); - let t2 = dag.add_task(task_fn(move |_: ()| { + let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { let c = c2.clone(); - async move { - c.fetch_add(1, Ordering::SeqCst); - 2 - } + c.fetch_add(1, Ordering::SeqCst); + 2 })); - let t3 = dag.add_task(task_fn(move |_: ()| { + let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { let c = c3.clone(); - async move { - c.fetch_add(1, Ordering::SeqCst); - 3 - } + c.fetch_add(1, Ordering::SeqCst); + 3 })); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -112,9 +106,9 @@ async fn test_task_fn_with_captured_state() { let dag = DagRunner::new(); let multiplier = 7; - let base = dag.add_task(task_fn(|_: ()| async { 5 })); + let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); let scaled = dag - .add_task(task_fn(move |x: i32| async move { x * multiplier })) + .add_task(task_fn::(move |&x: &i32| x * multiplier)) .depends_on(base); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) diff --git a/tests/task_patterns/readonly.rs b/tests/task_patterns/readonly.rs index 0764c3f..eb076e0 100644 --- a/tests/task_patterns/readonly.rs +++ b/tests/task_patterns/readonly.rs @@ -54,7 +54,7 @@ async fn test_readonly_state_with_input() { // Test read-only state task with input using &self let dag = DagRunner::new(); - let input = dag.add_task(task_fn(|_: ()| async { 5 })); + let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -68,8 +68,8 @@ async fn test_readonly_state_multiple_uses() { // Test that read-only state can be used multiple times let dag = DagRunner::new(); - let input1 = dag.add_task(task_fn(|_: ()| async { 10 })); - let input2 = dag.add_task(task_fn(|_: ()| async { 20 })); + let input1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); + let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); let config = ReadOnlyConfig { multiplier: 3, diff --git a/tests/task_patterns/stateless.rs b/tests/task_patterns/stateless.rs index 912f087..3b4d890 100644 --- a/tests/task_patterns/stateless.rs +++ b/tests/task_patterns/stateless.rs @@ -82,8 +82,8 @@ async fn test_stateless_basic() { // Test basic stateless task with two inputs let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 10 })); - let y = dag.add_task(task_fn(|_: ()| async { 20 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); let sum = dag.add_task(StatelessAdd).depends_on((x, y)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -97,7 +97,7 @@ async fn test_stateless_single_input() { // Test stateless task with single input let dag = DagRunner::new(); - let input = dag.add_task(task_fn(|_: ()| async { 21 })); + let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 21)); let doubled = dag.add_task(StatelessDouble).depends_on(input); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) @@ -124,9 +124,9 @@ async fn test_stateless_multiple_inputs() { // Test stateless task with multiple inputs of different types let dag = DagRunner::new(); - let num = dag.add_task(task_fn(|_: ()| async { 42 })); - let text = dag.add_task(task_fn(|_: ()| async { "test".to_string() })); - let flag = dag.add_task(task_fn(|_: ()| async { true })); + let num = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); + let text = dag.add_task(task_fn::<(), _, _>(|_: ()| "test".to_string())); + let flag = dag.add_task(task_fn::<(), _, _>(|_: ()| true)); let result = dag.add_task(StatelessFormat).depends_on((num, text, flag)); @@ -160,8 +160,8 @@ async fn test_stateless_mixed_with_stateful() { let dag = DagRunner::new(); - let x = dag.add_task(task_fn(|_: ()| async { 5 })); - let y = dag.add_task(task_fn(|_: ()| async { 3 })); + let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); + let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); // Stateless multiplication let product = dag.add_task(StatelessMultiply).depends_on((x, y)); @@ -182,7 +182,7 @@ async fn test_stateless_chain() { // Test chaining stateless tasks let dag = DagRunner::new(); - let start = dag.add_task(task_fn(|_: ()| async { 0 })); + let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); let step1 = dag.add_task(StatelessInc).depends_on(start); let step2 = dag.add_task(StatelessInc).depends_on(step1); let step3 = dag.add_task(StatelessInc).depends_on(step2); @@ -203,7 +203,7 @@ async fn test_stateless_parallel() { let tasks: Vec<_> = (1..=5) .map(|i| { - let source = dag.add_task(task_fn(move |_: ()| async move { i })); + let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); dag.add_task(StatelessSquare).depends_on(source) }) .collect(); @@ -224,8 +224,8 @@ async fn test_stateless_with_string_types() { // Test stateless tasks with complex types let dag = DagRunner::new(); - let hello = dag.add_task(task_fn(|_: ()| async { "Hello, ".to_string() })); - let world = dag.add_task(task_fn(|_: ()| async { "World!".to_string() })); + let hello = dag.add_task(task_fn::<(), _, _>(|_: ()| "Hello, ".to_string())); + let world = dag.add_task(task_fn::<(), _, _>(|_: ()| "World!".to_string())); let greeting = dag.add_task(StatelessConcat).depends_on((hello, world)); diff --git a/tests/timing/concurrent_access.rs b/tests/timing/concurrent_access.rs index bc72735..07207f8 100644 --- a/tests/timing/concurrent_access.rs +++ b/tests/timing/concurrent_access.rs @@ -1,10 +1,11 @@ //! Tests for concurrent access patterns and race conditions use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner, TaskHandle}; use futures::FutureExt; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; +use std::time::Duration; use tokio::sync::RwLock; #[tokio::test] @@ -12,13 +13,20 @@ async fn test_concurrent_result_retrieval() -> DagResult<()> { // Test concurrent get() calls after execution let dag = Arc::new(DagRunner::new()); + struct DelayTask(usize, Duration); + + #[task] + impl DelayTask { + async fn run(&self) -> usize { + tokio::time::sleep(self.1).await; + self.0 + } + } + // Add tasks with minimal delays let tasks: Vec> = (0..10) .map(|i| { - let task = dag.add_task(task_fn(move |_: ()| async move { - tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; - i as usize - })); + let task = dag.add_task(DelayTask(i, Duration::from_millis(1))); task.into() }) .collect(); @@ -54,7 +62,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> { // Start with a few initial tasks let initial: Vec<_> = (0..5) - .map(|i| dag.add_task(task_fn(move |_: ()| async move { i }))) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i))) .collect(); // Start building more tasks in background @@ -63,7 +71,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> { let build_handle = tokio::spawn(async move { let mut tasks: Vec> = Vec::new(); for i in 5..50 { - let task = dag_build.add_task(task_fn(move |_: ()| async move { i })); + let task = dag_build.add_task(task_fn::<(), _, _>(move |_: ()| i)); tasks.push(task.into()); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; } @@ -108,14 +116,12 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> { // Create a source task let source: TaskHandle<_> = dag - .add_task(task_fn({ + .add_task(task_fn::<(), _, _>({ let counter = resolution_counter.clone(); move |_: ()| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - 42 - } + counter.fetch_add(1, Ordering::SeqCst); + 42 } })) .into(); @@ -124,12 +130,10 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> { let dependents: Vec<_> = (0..100) .map(|i| { let counter = resolution_counter.clone(); - dag.add_task(task_fn(move |val: i32| { + dag.add_task(task_fn::(move |val: &i32| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - val + i - } + counter.fetch_add(1, Ordering::SeqCst); + val + i })) .depends_on(source) }) @@ -154,38 +158,56 @@ async fn test_read_write_race_patterns() -> DagResult<()> { let dag = DagRunner::new(); let shared_state = Arc::new(RwLock::new(Vec::new())); + struct WriterTask { + state: Arc>>, + idx: i32, + } + + #[task] + impl WriterTask { + async fn run(&self) -> i32 { + let mut guard = self.state.write().await; + guard.push(self.idx); + tokio::task::yield_now().await; // Force potential race + guard.push(self.idx * 10); + self.idx + } + } + // Writers let writers: Vec<_> = (0..10) .map(|i| { - let state = shared_state.clone(); - dag.add_task(task_fn(move |_: ()| { - let state = state.clone(); - async move { - let mut guard = state.write().await; - guard.push(i); - tokio::task::yield_now().await; // Force potential race - guard.push(i * 10); - i - } - })) + dag.add_task(WriterTask { + state: shared_state.clone(), + idx: i, + }) .into() }) .collect(); + struct ReaderTask { + state: Arc>>, + } + + struct ReaderTaskOutput((i32, bool, usize)); + + #[task] + impl ReaderTask { + async fn run(&self, writer_id: &i32) -> ReaderTaskOutput { + let guard = self.state.read().await; + let contains_writer = guard.contains(&writer_id); + let len = guard.len(); + ReaderTaskOutput((*writer_id, contains_writer, len)) + } + } + // Readers (depend on writers to ensure ordering) let readers: Vec<_> = writers .iter() .map(|writer| { - let state = shared_state.clone(); - dag.add_task(task_fn(move |writer_id: i32| { - let state = state.clone(); - async move { - let guard = state.read().await; - let contains_writer = guard.contains(&writer_id); - let len = guard.len(); - (writer_id, contains_writer, len) - } - })) + dag.add_task(ReaderTask { + state: shared_state.clone(), + }) .depends_on(writer) }) .collect(); @@ -194,7 +216,7 @@ async fn test_read_write_race_patterns() -> DagResult<()> { // Verify readers saw their writer's data for reader in &readers { - let (writer_id, saw_writer, _len) = dag.get(reader)?; + let ReaderTaskOutput((writer_id, saw_writer, _len)) = dag.get(reader)?; assert!(saw_writer, "Reader didn't see writer {}", writer_id); } @@ -217,30 +239,27 @@ async fn test_atomic_counter_races() -> DagResult<()> { .map(|_| { let counter = counter.clone(); let max_seen = max_seen.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let counter = counter.clone(); let max_seen = max_seen.clone(); - async move { - let mut local_max = 0; - for _ in 0..100 { - let val = counter.fetch_add(1, Ordering::SeqCst); - local_max = local_max.max(val); + let mut local_max = 0; + for _ in 0..100 { + let val = counter.fetch_add(1, Ordering::SeqCst); + local_max = local_max.max(val); - // Update global max - let mut current_max = max_seen.load(Ordering::SeqCst); - while val > current_max { - match max_seen.compare_exchange_weak( - current_max, - val, - Ordering::SeqCst, - Ordering::SeqCst, - ) { - Ok(_) => break, - Err(x) => current_max = x, - } + // Update global max + let mut current_max = max_seen.load(Ordering::SeqCst); + while val > current_max { + match max_seen.compare_exchange_weak( + current_max, + val, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => break, + Err(x) => current_max = x, } } - local_max } })) }) @@ -272,12 +291,10 @@ async fn test_multiple_dag_runners_interaction() -> DagResult<()> { let tasks: Vec<_> = (0..20) .map(|task_id| { let counter = counter.clone(); - dag.add_task(task_fn(move |_: ()| { + dag.add_task(task_fn::<(), _, _>(move |_: ()| { let counter = counter.clone(); - async move { - counter.fetch_add(1, Ordering::SeqCst); - (dag_id, task_id) - } + counter.fetch_add(1, Ordering::SeqCst); + (dag_id, task_id) })) }) .collect(); diff --git a/tests/timing/interleaving.rs b/tests/timing/interleaving.rs index 867c204..5b4c31a 100644 --- a/tests/timing/interleaving.rs +++ b/tests/timing/interleaving.rs @@ -1,7 +1,7 @@ //! Tests for forced execution interleaving patterns use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner, TaskHandle}; use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -14,56 +14,47 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> { let barrier = Arc::new(Barrier::new(3)); let order = Arc::new(parking_lot::Mutex::new(Vec::new())); + struct InterleaveTask { + barrier: Arc, + order: Arc>>, + prefix: &'static str, + value: i32, + } + + #[task] + impl InterleaveTask { + async fn run(&self) -> i32 { + self.order.lock().push(format!("{}_start", self.prefix)); + self.barrier.wait().await; + self.order.lock().push(format!("{}_end", self.prefix)); + self.value + } + } + // Three parallel tasks that synchronize at barrier - let t1 = dag.add_task(task_fn({ - let barrier = barrier.clone(); - let order = order.clone(); - move |_: ()| { - let barrier = barrier.clone(); - let order = order.clone(); - async move { - order.lock().push("t1_start"); - barrier.wait().await; - order.lock().push("t1_end"); - 1 - } - } - })); - - let t2 = dag.add_task(task_fn({ - let barrier = barrier.clone(); - let order = order.clone(); - move |_: ()| { - let barrier = barrier.clone(); - let order = order.clone(); - async move { - order.lock().push("t2_start"); - barrier.wait().await; - order.lock().push("t2_end"); - 2 - } - } - })); - - let t3 = dag.add_task(task_fn({ - let barrier = barrier.clone(); - let order = order.clone(); - move |_: ()| { - let barrier = barrier.clone(); - let order = order.clone(); - async move { - order.lock().push("t3_start"); - barrier.wait().await; - order.lock().push("t3_end"); - 3 - } - } - })); + let t1 = dag.add_task(InterleaveTask { + barrier: barrier.clone(), + order: order.clone(), + prefix: "t1", + value: 1, + }); + let t2 = dag.add_task(InterleaveTask { + barrier: barrier.clone(), + order: order.clone(), + prefix: "t2", + value: 2, + }); + let t3 = dag.add_task(InterleaveTask { + barrier: barrier.clone(), + order: order.clone(), + prefix: "t2", + value: 3, + }); // Collector let collector = dag - .add_task(task_fn( - |(a, b, c): (i32, i32, i32)| async move { a + b + c }, + .add_task(task_fn::<(i32, i32, i32), _, _>( + |(a, b, c): (&i32, &i32, &i32)| a + b + c, )) .depends_on((t1, t2, t3)); @@ -93,76 +84,105 @@ async fn test_alternating_execution_pattern() -> DagResult<()> { let events: Arc>> = Arc::new(parking_lot::Mutex::new(Vec::new())); - // Two chains that alternate execution - let first_a = dag.add_task(task_fn({ - let turn = turn.clone(); - let events = events.clone(); - move |_: ()| { - let turn = turn.clone(); - let events = events.clone(); - async move { - while turn.load(Ordering::SeqCst) != 0 { - tokio::task::yield_now().await; - } - events.lock().push("A0".to_string()); - turn.store(1, Ordering::SeqCst); - 0 + struct FirstA { + turn: Arc, + events: Arc>>, + } + + #[task] + impl FirstA { + async fn run(&self) -> i32 { + while self.turn.load(Ordering::SeqCst) != 0 { + tokio::task::yield_now().await; } + self.events.lock().push("A0".to_string()); + self.turn.store(1, Ordering::SeqCst); + 0 } - })); + } + + let first_a = dag.add_task(FirstA { + turn: turn.clone(), + events: events.clone(), + }); + let mut chain_a: dagx::TaskHandle = first_a.into(); - let first_b = dag.add_task(task_fn({ - let turn = turn.clone(); - let events = events.clone(); - move |_: ()| { - let turn = turn.clone(); - let events = events.clone(); - async move { - while turn.load(Ordering::SeqCst) != 1 { - tokio::task::yield_now().await; - } - events.lock().push("B0".to_string()); - turn.store(2, Ordering::SeqCst); - 0 + struct FirstB { + turn: Arc, + events: Arc>>, + } + + #[task] + impl FirstB { + async fn run(&self) -> i32 { + while self.turn.load(Ordering::SeqCst) != 1 { + tokio::task::yield_now().await; } + self.events.lock().push("B0".to_string()); + self.turn.store(2, Ordering::SeqCst); + 0 } - })); + } + + let first_b = dag.add_task(FirstB { + turn: turn.clone(), + events: events.clone(), + }); + let mut chain_b: dagx::TaskHandle = first_b.into(); + struct ChainA { + turn: Arc, + events: Arc>>, + idx: usize, + } + + #[task] + impl ChainA { + async fn run(&self, x: &i32) -> i32 { + while self.turn.load(Ordering::SeqCst) != self.idx * 2 { + tokio::task::yield_now().await; + } + self.events.lock().push(format!("A{}", self.idx)); + self.turn.store(self.idx * 2 + 1, Ordering::SeqCst); + x + 1 + } + } + + struct ChainB { + turn: Arc, + events: Arc>>, + idx: usize, + } + + #[task] + impl ChainB { + async fn run(&self, x: &i32) -> i32 { + while self.turn.load(Ordering::SeqCst) != self.idx * 2 + 1 { + tokio::task::yield_now().await; + } + self.events.lock().push(format!("B{}", self.idx)); + self.turn.store(self.idx * 2 + 2, Ordering::SeqCst); + x + 1 + } + } + for i in 1..5 { - let turn_clone = turn.clone(); - let events_clone = events.clone(); chain_a = dag - .add_task(task_fn(move |x: i32| { - let turn_clone = turn_clone.clone(); - let events_clone = events_clone.clone(); - async move { - while turn_clone.load(Ordering::SeqCst) != i * 2 { - tokio::task::yield_now().await; - } - events_clone.lock().push(format!("A{}", i)); - turn_clone.store(i * 2 + 1, Ordering::SeqCst); - x + 1 - } - })) + .add_task(ChainA { + events: events.clone(), + turn: turn.clone(), + idx: i, + }) .depends_on(chain_a); - let turn_clone = turn.clone(); - let events_clone = events.clone(); chain_b = dag - .add_task(task_fn(move |x: i32| { - let turn_clone = turn_clone.clone(); - let events_clone = events_clone.clone(); - async move { - while turn_clone.load(Ordering::SeqCst) != i * 2 + 1 { - tokio::task::yield_now().await; - } - events_clone.lock().push(format!("B{}", i)); - turn_clone.store(i * 2 + 2, Ordering::SeqCst); - x + 1 - } - })) + .add_task(ChainB { + turn: turn.clone(), + events: events.clone(), + idx: i, + }) .depends_on(chain_b); } @@ -184,55 +204,62 @@ async fn test_layered_interleaving() -> DagResult<()> { let dag = DagRunner::new(); let layer_active = Arc::new(AtomicUsize::new(0)); + struct Layer1Task { + active: Arc, + idx: usize, + } + + #[task] + impl Layer1Task { + async fn run(&self) -> i32 { + assert_eq!(self.active.fetch_add(1, Ordering::SeqCst), self.idx); + tokio::task::yield_now().await; + self.active.fetch_sub(1, Ordering::SeqCst); + self.idx as i32 + } + } + // Layer 1: 4 tasks let layer1: Vec<_> = (0..4) .map(|i| { - let active = layer_active.clone(); - dag.add_task(task_fn(move |_: ()| { - let active = active.clone(); - async move { - assert_eq!(active.fetch_add(1, Ordering::SeqCst), i); - tokio::task::yield_now().await; - active.fetch_sub(1, Ordering::SeqCst); - i as i32 - } - })) + dag.add_task(Layer1Task { + active: layer_active.clone(), + idx: i, + }) .into() }) .collect(); + struct Layer23Task { + active: Arc, + } + + #[task] + impl Layer23Task { + async fn run(&self, a: &i32, b: &i32) -> i32 { + self.active.fetch_add(1, Ordering::SeqCst); + tokio::task::yield_now().await; + self.active.fetch_sub(1, Ordering::SeqCst); + a + b + } + } + // Layer 2: 2 tasks, each depends on 2 from layer1 let layer2: Vec<_> = (0..2) .map(|i| { - let active = layer_active.clone(); let idx = i * 2; - dag.add_task(task_fn(move |(a, b): (i32, i32)| { - let active = active.clone(); - async move { - active.fetch_add(1, Ordering::SeqCst); - tokio::task::yield_now().await; - active.fetch_sub(1, Ordering::SeqCst); - a + b - } - })) + dag.add_task(Layer23Task { + active: layer_active.clone(), + }) .depends_on((&layer1[idx], &layer1[idx + 1])) }) .collect(); // Layer 3: 1 task depends on both layer2 let layer3 = dag - .add_task(task_fn({ - let active = layer_active.clone(); - move |(a, b): (i32, i32)| { - let active = active.clone(); - async move { - active.fetch_add(1, Ordering::SeqCst); - tokio::task::yield_now().await; - active.fetch_sub(1, Ordering::SeqCst); - a + b - } - } - })) + .add_task(Layer23Task { + active: layer_active.clone(), + }) .depends_on((&layer2[0], &layer2[1])); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; @@ -252,82 +279,93 @@ async fn test_cross_branch_synchronization() -> DagResult<()> { // Branch A let a1: TaskHandle<_> = dag - .add_task(task_fn({ + .add_task(task_fn::<(), _, _>({ let sync = sync_point.clone(); let events = events.clone(); move |_: ()| { - let sync = sync.clone(); - let events = events.clone(); - async move { - events.lock().push("A1"); - sync.fetch_add(1, Ordering::SeqCst); - 1 - } + events.lock().push("A1"); + sync.fetch_add(1, Ordering::SeqCst); + 1 } })) .into(); - let a2 = dag - .add_task(task_fn({ - let sync = sync_point.clone(); - let events = events.clone(); - move |x: i32| { - let sync = sync.clone(); - let events = events.clone(); - async move { - // Wait for B1 - while sync.load(Ordering::SeqCst) < 2 { - tokio::task::yield_now().await; - } - events.lock().push("A2"); - sync.fetch_add(1, Ordering::SeqCst); - x + 1 - } + struct A2Task { + sync: Arc, + events: Arc>>, + } + + #[task] + impl A2Task { + async fn run(&self, x: &i32) -> i32 { + // Wait for B1 + while self.sync.load(Ordering::SeqCst) < 2 { + tokio::task::yield_now().await; } - })) + self.events.lock().push("A2"); + self.sync.fetch_add(1, Ordering::SeqCst); + x + 1 + } + } + + let a2 = dag + .add_task(A2Task { + sync: sync_point.clone(), + events: events.clone(), + }) .depends_on(a1); - // Branch B - let b1 = dag.add_task(task_fn({ - let sync = sync_point.clone(); - let events = events.clone(); - move |_: ()| { - let sync = sync.clone(); - let events = events.clone(); - async move { - // Wait for A1 - while sync.load(Ordering::SeqCst) < 1 { - tokio::task::yield_now().await; - } - events.lock().push("B1"); - sync.fetch_add(1, Ordering::SeqCst); - 10 + struct B1Task { + sync: Arc, + events: Arc>>, + } + + #[task] + impl B1Task { + async fn run(&self) -> i32 { + // Wait for A1 + while self.sync.load(Ordering::SeqCst) < 1 { + tokio::task::yield_now().await; } + self.events.lock().push("B1"); + self.sync.fetch_add(1, Ordering::SeqCst); + 10 } - })); + } + + // Branch B + let b1 = dag.add_task(B1Task { + events: events.clone(), + sync: sync_point.clone(), + }); + + struct B2Task { + sync: Arc, + events: Arc>>, + } + + #[task] + impl B2Task { + async fn run(&self, x: &i32) -> i32 { + // Wait for A2 + while self.sync.load(Ordering::SeqCst) < 3 { + tokio::task::yield_now().await; + } + self.events.lock().push("B2"); + x + 1 + } + } let b2 = dag - .add_task(task_fn({ - let sync = sync_point.clone(); - let events = events.clone(); - move |x: i32| { - let sync = sync.clone(); - let events = events.clone(); - async move { - // Wait for A2 - while sync.load(Ordering::SeqCst) < 3 { - tokio::task::yield_now().await; - } - events.lock().push("B2"); - x + 1 - } - } - })) + .add_task(B2Task { + events: events.clone(), + sync: sync_point.clone(), + }) .depends_on(b1); // Merge let merge = dag - .add_task(task_fn(|(a, b): (i32, i32)| async move { a + b })) + .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) .depends_on((&a2, &b2)); dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; diff --git a/tests/type_safety/complex.rs b/tests/type_safety/complex.rs deleted file mode 100644 index f4c166c..0000000 --- a/tests/type_safety/complex.rs +++ /dev/null @@ -1,272 +0,0 @@ -// Complex type safety tests for DagRunner -use crate::common::task_fn; - -use dagx::*; -use futures::FutureExt; - -#[tokio::test] -async fn test_complex_type_safety() { - // Verify type safety with complex types - #[derive(Debug, Clone, PartialEq)] - struct CustomType { - value: i32, - name: String, - } - - let dag = DagRunner::new(); - - let custom_task = dag.add_task(task_fn(|_: ()| async { - CustomType { - value: 42, - name: "test".to_string(), - } - })); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Correct type works - let custom = dag.get(custom_task).unwrap(); - assert_eq!(custom.value, 42); - - // Type safety is enforced at compile time - we cannot get with wrong types -} - -#[tokio::test] -async fn test_generic_type_safety() { - // Verify type safety with generics - let dag = DagRunner::new(); - - // Task returning Vec - let vec_i32 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); - - // Task returning Vec - let vec_string = dag.add_task(task_fn(|_: ()| async { - vec!["a".to_string(), "b".to_string()] - })); - - 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]); - assert!(dag.get(vec_string).is_ok()); -} - -#[tokio::test] -async fn test_option_result_type_safety() { - // Verify type safety with wrapped types - let dag = DagRunner::new(); - - // Task returning Option - let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) })); - - // Task returning Result - let result_task = dag.add_task(task_fn(|_: ()| async { Ok::(42) })); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Correct wrapped types work - assert_eq!(dag.get(opt_task).unwrap(), Some(42)); - assert!(dag.get(result_task).is_ok()); -} - -#[tokio::test] -async fn test_zero_sized_type_safety() { - // Verify ZSTs are handled correctly - #[derive(Clone, Copy)] - struct Zst1; - - #[derive(Clone, Copy)] - struct Zst2; - - let dag = DagRunner::new(); - - 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).map(Result::unwrap)) - .await - .unwrap(); - - // Correct ZSTs work - assert!(dag.get(zst1_task).is_ok()); -} - -#[tokio::test] -async fn test_type_safety_with_many_types() { - // Verify type safety with many different types - let dag = DagRunner::new(); - - let i32_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); - let i64_task = dag.add_task(task_fn(|_: ()| async { 42i64 })); - let u32_task = dag.add_task(task_fn(|_: ()| async { 42u32 })); - let f32_task = dag.add_task(task_fn(|_: ()| async { 42.0f32 })); - let f64_task = dag.add_task(task_fn(|_: ()| async { 42.0f64 })); - 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).map(Result::unwrap)) - .await - .unwrap(); - - // Each type works correctly - assert_eq!(dag.get(i32_task).unwrap(), 42i32); - assert_eq!(dag.get(i64_task).unwrap(), 42i64); - assert_eq!(dag.get(u32_task).unwrap(), 42u32); - assert_eq!(dag.get(f32_task).unwrap(), 42.0f32); - assert_eq!(dag.get(f64_task).unwrap(), 42.0f64); - assert_eq!(dag.get(string_task).unwrap(), "42".to_string()); - assert!(dag.get(bool_task).unwrap()); -} - -#[tokio::test] -async fn test_type_safety_single_dependency() { - // Verify that single dependency type matching works - let dag = DagRunner::new(); - - let source = dag.add_task(task_fn(|_: ()| async { 42i32 })); - - // This compiles because types match (i32 -> i32) - let transform = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) - .depends_on(source); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - assert_eq!(dag.get(transform).unwrap(), 84); -} - -#[tokio::test] -async fn test_type_safety_tuple_dependencies() { - // Verify that tuple dependency type matching works - let dag = DagRunner::new(); - - let a = dag.add_task(task_fn(|_: ()| async { 10i32 })); - let b = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); - let c = dag.add_task(task_fn(|_: ()| async { true })); - - // This compiles because types match (i32, String, bool) - let combine = dag - .add_task(task_fn(|(x, s, b): (i32, String, bool)| async move { - format!("{}: {} ({})", s, x, b) - })) - .depends_on((a, b, c)); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - assert_eq!(dag.get(combine).unwrap(), "hello: 10 (true)"); -} - -#[tokio::test] -async fn test_type_safety_different_output_types() { - // Verify that different types can flow through the DAG - let dag = DagRunner::new(); - - let int_source = dag.add_task(task_fn(|_: ()| async { 42i32 })); - 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).map(Result::unwrap)) - .await - .unwrap(); - - assert_eq!(dag.get(int_source).unwrap(), 42); - assert_eq!(dag.get(str_source).unwrap(), "test"); - assert_eq!(dag.get(vec_source).unwrap(), vec![1, 2, 3]); -} - -#[tokio::test] -async fn test_type_safety_complex_types() { - // Verify that complex types work correctly as outputs - // Complex inputs need ExtractInput implementation - let dag = DagRunner::new(); - - // Tasks can return complex types like tuples - let tuple_task = dag.add_task(task_fn(|_: ()| async { (42, "test".to_string()) })); - - // Tasks can return vectors - let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })); - - // Tasks can return nested structures - let nested_task = dag.add_task(task_fn(|_: ()| async { - vec![Some(1), None, Some(2), Some(3)] - })); - - 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); - assert_eq!(tuple_result.1, "test"); - - let vec_result = dag.get(vec_task).unwrap(); - assert_eq!(vec_result.len(), 5); - - let nested_result = dag.get(nested_task).unwrap(); - assert_eq!(nested_result.len(), 4); -} - -#[tokio::test] -async fn test_type_safety_with_generics() { - // Verify that generic types work correctly - // Using Option as it already implements necessary traits - let dag = DagRunner::new(); - - let source = dag.add_task(task_fn(|_: ()| async { Some(42i32) })); - - let transform = dag - .add_task(task_fn( - |opt: Option| async move { opt.map(|x| x * 2) }, - )) - .depends_on(source); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - assert_eq!(dag.get(transform).unwrap(), Some(84)); - - // Test with None variant - let source2 = dag.add_task(task_fn(|_: ()| async { None:: })); - let transform2 = dag - .add_task(task_fn( - |opt: Option| async move { opt.map(|x| x * 2) }, - )) - .depends_on(source2); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - assert_eq!(dag.get(transform2).unwrap(), None); -} - -#[tokio::test] -async fn test_large_tuple_input() { - let dag = DagRunner::new(); - - let a = dag.add_task(task_fn(|_: ()| async { 1 })); - let b = dag.add_task(task_fn(|_: ()| async { 2 })); - let c = dag.add_task(task_fn(|_: ()| async { 3 })); - let d = dag.add_task(task_fn(|_: ()| async { 4 })); - let e = dag.add_task(task_fn(|_: ()| async { 5 })); - - let sum = dag - .add_task(task_fn( - |(a, b, c, d, e): (i32, i32, i32, i32, i32)| async move { a + b + c + d + e }, - )) - .depends_on((a, b, c, d, e)); - - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 15); -} diff --git a/tests/type_safety/handles.rs b/tests/type_safety/handles.rs deleted file mode 100644 index 052167e..0000000 --- a/tests/type_safety/handles.rs +++ /dev/null @@ -1,72 +0,0 @@ -// 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). - -use dagx::*; - -#[tokio::test] -async fn test_get_with_wrong_type_returns_none() { - // Verify that accessing with wrong type returns error, not panic - let dag = DagRunner::new(); - - // Add task that returns i32 - let int_task = dag.add_task(task_fn(|_: ()| async { 42i32 })); - - // Run DAG - dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) - .await - .unwrap(); - - // Verify correct type works - assert_eq!(dag.get(int_task), Ok(42i32)); - - // The type safety is enforced at compile time - // We cannot create fake handles with wrong types without accessing private fields - // This is actually better - it means type safety is compile-time enforced! -} - -#[tokio::test] -async fn test_handle_preserves_type() { - // Verify TaskHandle preserves type information correctly - let dag = DagRunner::new(); - - // Create typed handles - 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).map(Result::unwrap)) - .await - .unwrap(); - - // Get with correct types - assert_eq!(dag.get(string_task).unwrap(), "hello".to_string()); - assert_eq!(dag.get(int_task).unwrap(), 42); - - // Type safety is enforced at compile time - we cannot create wrong-typed handles -} - -#[tokio::test] -async fn test_type_safety_handle_reuse() { - // Verify that handles maintain type safety across reuse - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100i32 })).into(); - - // Use the same source handle multiple times - let double = dag - .add_task(task_fn(|x: i32| async move { x * 2 })) - .depends_on(source); - let triple = dag - .add_task(task_fn(|x: i32| async move { x * 3 })) - .depends_on(source); - - 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); -} diff --git a/tests/type_safety/mod.rs b/tests/type_safety/mod.rs index 3236375..55465dc 100644 --- a/tests/type_safety/mod.rs +++ b/tests/type_safety/mod.rs @@ -1,4 +1,72 @@ -// Test module for type safety tests +// 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). -mod complex; -mod handles; +use dagx::*; + +#[tokio::test] +async fn test_get_with_wrong_type_returns_none() { + // Verify that accessing with wrong type returns error, not panic + let dag = DagRunner::new(); + + // Add task that returns i32 + let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32)); + + // Run DAG + dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) + .await + .unwrap(); + + // Verify correct type works + assert_eq!(dag.get(int_task), Ok(42i32)); + + // The type safety is enforced at compile time + // We cannot create fake handles with wrong types without accessing private fields + // This is actually better - it means type safety is compile-time enforced! +} + +#[tokio::test] +async fn test_handle_preserves_type() { + // Verify TaskHandle preserves type information correctly + let dag = DagRunner::new(); + + // Create typed handles + let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); + let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32)); + + 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()); + assert_eq!(dag.get(int_task).unwrap(), 42); + + // Type safety is enforced at compile time - we cannot create wrong-typed handles +} + +#[tokio::test] +async fn test_type_safety_handle_reuse() { + // Verify that handles maintain type safety across reuse + let dag = DagRunner::new(); + + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100i32)).into(); + + // Use the same source handle multiple times + let double = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(source); + let triple = dag + .add_task(task_fn::(|&x: &i32| x * 3)) + .depends_on(source); + + 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); +}