Merge pull request #7 from TechnoPorg/push-nmmvpvqkpsmr

Remove clone bound from task data
This commit is contained in:
TechnoPorg authored and GitHub committed 2026-02-11 22:35:23 +01:00
commit f342e33f16
85 files changed
+2040 -4131

No files matched your search

-109
View File
@@ -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.
+2 -3
View File
@@ -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 cycle prevention**: Type system makes cycles impossible — no runtime checks
- **Compile-time type safety**: Dependencies validated at compile time, no runtime type errors - **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 - **Runtime-agnostic**: Works with Tokio, async-std, smol, or any async runtime
- **True parallelism**: Tasks spawn to multiple threads for genuine parallel execution - **True parallelism**: Tasks spawn to multiple threads for genuine parallel execution
- **Type-state pattern**: API prevents incorrect wiring through compile-time errors - **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 ### 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 ```rust
#[derive(Clone)] // Just derive Clone!
struct User { struct User {
name: String, name: String,
age: u32, age: u32,
+21 -21
View File
@@ -12,7 +12,7 @@ pub fn bench_dag_execution(c: &mut Criterion) {
rt.block_on(async { rt.block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..10 { 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -26,7 +26,7 @@ pub fn bench_dag_execution(c: &mut Criterion) {
rt.block_on(async { rt.block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..100 { 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -51,61 +51,61 @@ pub fn bench_dag_execution(c: &mut Criterion) {
let t0 = dag.add_task(Value(0)); let t0 = dag.add_task(Value(0));
let t1 = dag let t1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t0); .depends_on(t0);
let t2 = dag let t2 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t1); .depends_on(t1);
let t3 = dag let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t2); .depends_on(t2);
let t4 = dag let t4 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t3); .depends_on(t3);
let t5 = dag let t5 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t4); .depends_on(t4);
let t6 = dag let t6 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t5); .depends_on(t5);
let t7 = dag let t7 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t6); .depends_on(t6);
let t8 = dag let t8 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t7); .depends_on(t7);
let t9 = dag let t9 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t8); .depends_on(t8);
let t10 = dag let t10 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t9); .depends_on(t9);
let t11 = dag let t11 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t10); .depends_on(t10);
let t12 = dag let t12 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t11); .depends_on(t11);
let t13 = dag let t13 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t12); .depends_on(t12);
let t14 = dag let t14 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t13); .depends_on(t13);
let t15 = dag let t15 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t14); .depends_on(t14);
let t16 = dag let t16 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t15); .depends_on(t15);
let t17 = dag let t17 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t16); .depends_on(t16);
let t18 = dag let t18 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t17); .depends_on(t17);
let _t19 = dag let _t19 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t18); .depends_on(t18);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+1 -1
View File
@@ -17,7 +17,7 @@ pub fn bench_dag_scaling(c: &mut Criterion) {
rt.block_on(async { rt.block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..size { 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
+3 -3
View File
@@ -18,14 +18,14 @@ pub fn bench_deep_chain(c: &mut Criterion) {
let dag = DagRunner::new(); 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 let mut prev = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(first); .depends_on(first);
for i in 2..100 { for i in 2..100 {
prev = dag prev = dag
.add_task(task_fn(move |x: i32| async move { x + i })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(prev); .depends_on(prev);
} }
+4 -4
View File
@@ -18,15 +18,15 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
let dag = DagRunner::new(); 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 let b = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(a); .depends_on(a);
let d = dag 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)); .depends_on((&b, &c));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+19 -16
View File
@@ -18,34 +18,34 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Extract from 3 sources // Extract from 3 sources
let source1 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] })); let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
let source2 = dag.add_task(task_fn(|_: ()| async { vec![6, 7, 8, 9, 10] })); let source2 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![6, 7, 8, 9, 10]));
let source3 = dag.add_task(task_fn(|_: ()| async { vec![11, 12, 13, 14, 15] })); let source3 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![11, 12, 13, 14, 15]));
// Transform each source // Transform each source
let transform1 = dag let transform1 = dag
.add_task(task_fn(|data: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>() data.iter().map(|x| x * 2).collect::<Vec<_>>()
})) }))
.depends_on(source1); .depends_on(source1);
let transform2 = dag let transform2 = dag
.add_task(task_fn(|data: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>() data.iter().map(|x| x * 2).collect::<Vec<_>>()
})) }))
.depends_on(source2); .depends_on(source2);
let transform3 = dag let transform3 = dag
.add_task(task_fn(|data: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>() data.iter().map(|x| x * 2).collect::<Vec<_>>()
})) }))
.depends_on(source3); .depends_on(source3);
// Merge all transformed data // Merge all transformed data
let merge = dag let merge = dag
.add_task(task_fn( .add_task(task_fn::<(Vec<_>, Vec<_>, Vec<_>), _, _>(
|(d1, d2, d3): (Vec<i32>, Vec<i32>, Vec<i32>)| async move { |(d1, d2, d3): (&Vec<i32>, &Vec<i32>, &Vec<i32>)| {
let mut result = d1; let mut result = d1.clone();
result.extend(d2); result.extend(d2);
result.extend(d3); result.extend(d3);
result result
@@ -55,15 +55,18 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// Validate // Validate
let validate = dag let validate = dag
.add_task(task_fn(|data: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().filter(|&x| x > 0).collect::<Vec<_>>() data.iter()
.copied()
.filter(|&x| x > 0)
.collect::<Vec<i32>>()
})) }))
.depends_on(merge); .depends_on(merge);
// Load (compute sum) // Load (compute sum)
let load = dag let load = dag
.add_task(task_fn(|data: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().sum::<i32>() data.iter().sum::<i32>()
})) }))
.depends_on(validate); .depends_on(validate);
+1 -1
View File
@@ -19,7 +19,7 @@ pub fn bench_large_scale(c: &mut Criterion) {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..10_000 { 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+5 -5
View File
@@ -17,18 +17,18 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
let dag = DagRunner::new(); let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 })); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b = dag let b = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(b); .depends_on(b);
let d = dag let d = dag
.add_task(task_fn(|x: i32| async move { x - 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
.depends_on(c); .depends_on(c);
let e = dag let e = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(d); .depends_on(d);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+2 -2
View File
@@ -17,11 +17,11 @@ pub fn bench_wide_fanout(c: &mut Criterion) {
use dagx::{task_fn, DagRunner}; use dagx::{task_fn, DagRunner};
let dag = DagRunner::new(); 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 // Create 100 tasks that all depend on source
for i in 0..100 { for i in 0..100 {
dag.add_task(task_fn(move |x: i32| async move { x + i })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source); .depends_on(source);
} }
+3 -3
View File
@@ -14,7 +14,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..10_000 { 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 { rt.block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..10_000 { 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -39,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
// Pre-build the DAG outside the measurement // Pre-build the DAG outside the measurement
let dag = DagRunner::new(); let dag = DagRunner::new();
for i in 0..10_000 { for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
} }
b.iter(|| { b.iter(|| {
+4 -4
View File
@@ -14,16 +14,16 @@ pub fn bench_diamond(c: &mut Criterion) {
for i in 0..50 { for i in 0..50 {
let source: TaskHandle<_> = let source: TaskHandle<_> =
dag.add_task(task_fn(move |_: ()| async move { i })).into(); dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into();
let left = dag let left = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source); .depends_on(source);
let right = dag let right = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(source); .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)); .depends_on((&left, &right));
} }
+7 -6
View File
@@ -12,10 +12,10 @@ pub fn bench_fanout(c: &mut Criterion) {
b.iter(|| { b.iter(|| {
rt.block_on(async { rt.block_on(async {
let dag = DagRunner::new(); 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 { for i in 0..100 {
dag.add_task(task_fn(move |x: i32| async move { x + i })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source); .depends_on(source);
} }
@@ -32,14 +32,14 @@ pub fn bench_fanout(c: &mut Criterion) {
rt.block_on(async { rt.block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let source: TaskHandle<_> = dag let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
// Create 1000 strings - framework wraps in Arc automatically // Create 1000 strings - framework wraps in Arc automatically
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>() (0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
})) }))
.into(); .into();
for i in 0..100 { for i in 0..100 {
dag.add_task(task_fn(move |data: Vec<String>| async move { dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
// Each consumer gets data extracted from Arc // Each consumer gets data extracted from Arc
data.iter().filter(|s| s.contains(&i.to_string())).count() 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) // Source produces a simple Copy type (usize)
// Framework wraps in Arc<usize> internally // Framework wraps in Arc<usize> 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 // 100 dependents - Arc overhead visible on Copy types
for i in 0..100 { for i in 0..100 {
dag.add_task(task_fn(move |x: usize| async move { x + i })) dag.add_task(task_fn::<usize, _, _>(move |x: &usize| x + i))
.depends_on(source); .depends_on(source);
} }
+16 -14
View File
@@ -21,7 +21,7 @@ pub fn bench_config_broadcast(c: &mut Criterion) {
// Configuration source - framework wraps HashMap in Arc automatically // Configuration source - framework wraps HashMap in Arc automatically
let config: TaskHandle<_> = dag let config: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
HashMap::from([ HashMap::from([
("max_connections".to_string(), "100".to_string()), ("max_connections".to_string(), "100".to_string()),
("timeout_ms".to_string(), "5000".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 // Workers receive and process based on shared config
for worker_id in 0..workers { for worker_id in 0..workers {
dag.add_task(task_fn(move |cfg: HashMap<String, String>| async move { dag.add_task(task_fn::<HashMap<_, _>, _, _>(
// Simulate work based on configuration move |cfg: &HashMap<String, String>| {
let batch_size: usize = cfg // Simulate work based on configuration
.get("batch_size") let batch_size: usize = cfg
.and_then(|s| s.parse().ok()) .get("batch_size")
.unwrap_or(10); .and_then(|s| s.parse().ok())
let timeout: usize = cfg .unwrap_or(10);
.get("timeout_ms") let timeout: usize = cfg
.and_then(|s| s.parse().ok()) .get("timeout_ms")
.unwrap_or(1000); .and_then(|s| s.parse().ok())
.unwrap_or(1000);
// Simulate some computation // Simulate some computation
black_box(worker_id * batch_size + timeout / 100) black_box(worker_id * batch_size + timeout / 100)
})) },
))
.depends_on(config); .depends_on(config);
} }
+22 -20
View File
@@ -21,24 +21,26 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Extract: Load JSON-like data (framework wraps in Arc automatically) // Extract: Load JSON-like data (framework wraps in Arc automatically)
let extract: TaskHandle<_> = dag.add_task(task_fn(move |_: ()| async move { let extract: TaskHandle<_> = dag
(0..count) .add_task(task_fn::<(), _, _>(move |_: ()| {
.map(|i| { (0..count)
format!( .map(|i| {
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}", format!(
i, "{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
i, i,
i * 10 i,
) i * 10
}) )
.collect::<Vec<_>>() })
})).into(); .collect::<Vec<_>>()
}))
.into();
// Transform: Three parallel transformations // Transform: Three parallel transformations
// T1: Parse and validate // T1: Parse and validate
let validate = dag let validate = dag
.add_task(task_fn(move |data: Vec<String>| async move { .add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter() data.iter()
.filter(|s| { .filter(|s| {
s.contains("\"id\"") s.contains("\"id\"")
@@ -51,7 +53,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// T2: Extract scores // T2: Extract scores
let scores = dag let scores = dag
.add_task(task_fn(move |data: Vec<String>| async move { .add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter() data.iter()
.filter_map(|s| { .filter_map(|s| {
s.split("\"score\":").nth(1).and_then(|part| { s.split("\"score\":").nth(1).and_then(|part| {
@@ -64,7 +66,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// T3: Extract names // T3: Extract names
let names = dag let names = dag
.add_task(task_fn(move |data: Vec<String>| async move { .add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter() data.iter()
.filter_map(|s| { .filter_map(|s| {
s.split("\"name\":\"") s.split("\"name\":\"")
@@ -77,8 +79,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
.depends_on(extract); .depends_on(extract);
// Load: Aggregate all transformations // Load: Aggregate all transformations
dag.add_task(task_fn( dag.add_task(task_fn::<(usize, Vec<_>, Vec<_>), _, _>(
move |(valid, scores, names): (usize, Vec<i32>, Vec<String>)| async move { move |(valid, scores, names): (&usize, &Vec<i32>, &Vec<String>)| {
let avg_score = let avg_score =
scores.iter().sum::<i32>() / scores.len().max(1) as i32; scores.iter().sum::<i32>() / scores.len().max(1) as i32;
format!( format!(
@@ -91,9 +93,9 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
)) ))
.depends_on((&validate, &scores, &names)); .depends_on((&validate, &scores, &names));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
.unwrap(); .unwrap();
}) })
}); });
}, },
+2 -2
View File
@@ -20,7 +20,7 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
let dag = DagRunner::new(); let dag = DagRunner::new();
let source: TaskHandle<_> = dag 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 // Create MB of data - framework wraps in Arc automatically
vec![0u8; mb * 1024 * 1024] 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 // 20 consumers share the data via automatic Arc wrapping
for i in 0..20 { for i in 0..20 {
dag.add_task(task_fn(move |data: Vec<u8>| async move { dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<u8>| {
// Process a small portion (data extracted from Arc) // Process a small portion (data extracted from Arc)
let sample_sum: u32 = data let sample_sum: u32 = data
.iter() .iter()
+10 -8
View File
@@ -14,7 +14,7 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
// Stage 1: Load data (framework wraps in Arc automatically) // Stage 1: Load data (framework wraps in Arc automatically)
let data: TaskHandle<_> = dag let data: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
(0..1000) (0..1000)
.map(|i| format!("Record-{:04}", i)) .map(|i| format!("Record-{:04}", i))
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -23,31 +23,33 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
// Stage 2: Fan-out for parallel analysis // Stage 2: Fan-out for parallel analysis
let analysis1 = dag let analysis1 = dag
.add_task(task_fn(|d: Vec<String>| async move { .add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| {
d.iter().filter(|s| s.contains("00")).count() d.iter().filter(|s| s.contains("00")).count()
})) }))
.depends_on(data); .depends_on(data);
let analysis2 = dag let analysis2 = dag
.add_task(task_fn(|d: Vec<String>| async move { .add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| {
d.iter().map(|s| s.len()).sum::<usize>() d.iter().map(|s| s.len()).sum::<usize>()
})) }))
.depends_on(data); .depends_on(data);
let analysis3 = dag let analysis3 = dag
.add_task(task_fn(|d: Vec<String>| async move { d.len() })) .add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| d.len()))
.depends_on(data); .depends_on(data);
// Stage 3: Fan-in to aggregate // Stage 3: Fan-in to aggregate
let summary = dag let summary = dag
.add_task(task_fn(|(a1, a2, a3): (usize, usize, usize)| async move { .add_task(task_fn::<(usize, usize, usize), _, _>(
format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3) |(a1, a2, a3): (&usize, &usize, &usize)| {
})) format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3)
},
))
.depends_on((&analysis1, &analysis2, &analysis3)); .depends_on((&analysis1, &analysis2, &analysis3));
// Stage 4: Fan-out final report to multiple destinations // Stage 4: Fan-out final report to multiple destinations
for i in 0..3 { for i in 0..3 {
dag.add_task(task_fn(move |report: String| async move { dag.add_task(task_fn::<String, _, _>(move |report: &String| {
format!("Destination {}: {}", i, report) format!("Destination {}: {}", i, report)
})) }))
.depends_on(summary); .depends_on(summary);
+50 -197
View File
@@ -4,22 +4,22 @@
//! the `Task` trait by deriving Input and Output types from the `run()` method signature. //! the `Task` trait by deriving Input and Output types from the `run()` method signature.
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote; 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. /// Attribute macro to automatically implement the `Task` trait.
/// ///
/// Apply this to an `impl` block containing a `run()` method (sync or async). The macro: /// Apply this to an `impl` block containing a `run()` method (sync or async). The macro:
/// - Derives `Input` and `Output` types from the `run()` signature /// - Derives `Input` and `Output` types from the `run()` signature
/// - Automatically implements the `Task` trait /// - 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 both sync and async run methods
/// - Supports stateless (no self) and stateful (&self, &mut self) tasks /// - Supports stateless (no self) and stateful (&self, &mut self) tasks
/// - Handles various input patterns (no inputs, single input, multiple inputs) /// - Handles various input patterns (no inputs, single input, multiple inputs)
/// ///
/// **Key Feature**: Custom types work automatically without implementing any traits! /// **Key Feature**: Custom types work automatically without implementing any traits!
/// The macro generates inline extraction logic in `extract_and_run()` specific to your /// The macro generates inline extraction logic specific to your task's parameter types.
/// task's parameter types. Just derive `Clone` on your types and they'll work seamlessly.
/// ///
/// # Task Patterns /// # 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 impl block must contain exactly one `async fn run()` method
/// - The `run()` method can be stateless (no self parameter) or stateful (`&mut self`) /// - The `run()` method can be stateless (no self parameter) or stateful (`&mut self`)
/// - All input parameters must be references (e.g., `&i32`, not `i32`) /// - 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 /// # 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) // Extract parameters (excluding self)
let params_result: Result<Vec<_>, _> = run_method let params_result: Result<Vec<_>, _> = run_method
.sig .sig
@@ -280,32 +275,39 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Extract return type // Extract return type
let output_type = match &run_method.sig.output { let output_type = match &run_method.sig.output {
ReturnType::Default => syn::parse_quote!(()), 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 // Build Input type based on parameter count
let input_type = match params.len() { let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
0 => quote! { () }, let input_type = quote! { ( #(#types,)* ) };
1 => {
let (_name, ty) = &params[0];
quote! { #ty }
}
_ => {
let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
quote! { ( #(#types),* ) }
}
};
// Generate parameter destructuring for the wrapper run() method // Generate parameter references for the wrapper run() method
let (param_destructure, param_refs) = if params.is_empty() { let (param_destructure, param_refs) = {
(quote! { _ }, quote! {}) let (destructure, refs): (Vec<_>, Vec<_>) = params
} else if params.len() == 1 { .iter()
let (name, _) = &params[0]; .map(|(ident, _ty)| {
(quote! { #name }, quote! { &#name }) let ident = Ident::new(&format!("__dagx_extract_{}", ident), Span::mixed_site());
} else { (
let names: Vec<_> = params.iter().map(|(name, _)| name).collect(); quote! {
let refs: Vec<_> = params.iter().map(|(name, _)| quote! { &#name }).collect(); let (#ident, input) = input.next();
(quote! { ( #(#names),* ) }, quote! { #(#refs),* }) },
ident,
)
})
.unzip();
(quote! { #(#destructure)* }, quote! { #(#refs),* })
}; };
// Clone the run method and rename it to run_impl // Clone the run method and rename it to run_impl
@@ -319,179 +321,30 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
.iter() .iter()
.any(|arg| matches!(arg, FnArg::Receiver(_))); .any(|arg| matches!(arg, FnArg::Receiver(_)));
// Generate extract_and_run implementation based on parameter count let is_async = run_method.sig.asyncness.is_some();
let extract_and_run_impl = match params.len() {
0 => {
// Zero parameters - no extraction needed
quote! {
fn extract_and_run(
self,
_dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
async move {
let input = ();
Ok(self.run(input).await)
}
}
}
}
1 => {
// Single parameter
let param_type = &params[0].1;
quote! {
fn extract_and_run(
self,
mut dependencies: Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
async move {
use std::sync::Arc;
if dependencies.len() != 1 { let run_call = match (has_self_receiver, is_async) {
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); (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 },
let arc_value = dependencies.pop() (false, false) => quote! { Self::run_impl(#param_refs) },
.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<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> impl std::future::Future<Output = Result<Self::Output, String>> + 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)
}
}
}
}
}; };
// Generate the Task trait implementation based on whether we have self and async/sync // Generate the Task trait implementation based on whether we have self and async/sync
let expanded = if has_self_receiver { quote! {
// Stateful task - consumes self but delegates to a method that borrows impl ::dagx::Task for #struct_name {
if is_async { type Input = #input_type;
// Async with self 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 { async fn run(mut self, mut input: ::dagx::TaskInput<'_, Self::Input>) -> Self::Output {
let #param_destructure = input; #param_destructure
self.run_impl(#param_refs).await #run_call
}
#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
}
} }
} }
} 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 { impl #struct_name {
let #param_destructure = input; #run_impl_method
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
}
}
} }
}; }
.into()
TokenStream::from(expanded)
} }
+1 -1
View File
@@ -227,7 +227,7 @@ impl Task<String, Bytes> for MyTask {
let mut dag = Dag::default(); let mut dag = Dag::default();
dag.add_vertex("one", || async move { /* task */ }); dag.add_vertex("one", || async move { /* task */ });
dag.add_edge("one", "two"); dag.add_edge("one", "two");
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run().await?;
``` ```
### tasksitter (Dynamic Workflows) ### tasksitter (Dynamic Workflows)
+1 -2
View File
@@ -113,11 +113,10 @@ async fn main() {
// Step 7: Retrieve results // Step 7: Retrieve results
// //
// Use .get() with a TaskHandle to retrieve the output. // 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()); println!("\nResult: {}", dag.get(sum).unwrap());
// Note: You can also retrieve intermediate results // Note: You can also retrieve intermediate results
assert_eq!(dag.get(x).unwrap(), 2); assert_eq!(dag.get(x).unwrap(), 2);
assert_eq!(dag.get(y).unwrap(), 3); assert_eq!(dag.get(y).unwrap(), 3);
assert_eq!(dag.get(sum).unwrap(), 5);
} }
+5 -5
View File
@@ -52,7 +52,7 @@
//! This proves dagx runs tasks on multiple CPU cores simultaneously. //! 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 futures::FutureExt;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -152,10 +152,10 @@ async fn main() -> DagResult<()> {
let task3 = ComputeSum::new(501, 751, WORK_DELAY); let task3 = ComputeSum::new(501, 751, WORK_DELAY);
let task4 = ComputeSum::new(751, 1001, WORK_DELAY); let task4 = ComputeSum::new(751, 1001, WORK_DELAY);
let r1 = task1.run(()).await; let r1 = task1.run(TaskInput::empty()).await;
let r2 = task2.run(()).await; let r2 = task2.run(TaskInput::empty()).await;
let r3 = task3.run(()).await; let r3 = task3.run(TaskInput::empty()).await;
let r4 = task4.run(()).await; let r4 = task4.run(TaskInput::empty()).await;
let result = r1 + r2 + r3 + r4; let result = r1 + r2 + r3 + r4;
let elapsed = start.elapsed(); let elapsed = start.elapsed();
+2 -5
View File
@@ -1,20 +1,18 @@
//! # Custom Types: Using Your Own Structs //! # Custom Types: Using Your Own Structs
//! //!
//! This example demonstrates that **ANY custom type** works in dagx as long as it implements //! 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 //! ## What You'll Learn
//! - How to use custom structs as task outputs //! - How to use custom structs as task outputs
//! - How custom types flow through the DAG automatically //! - How custom types flow through the DAG automatically
//! - How one task's output can be consumed by multiple downstream tasks //! - How one task's output can be consumed by multiple downstream tasks
//! - No trait implementations required (just derive Clone!)
//! //!
//! ## Key Insight //! ## Key Insight
//! //!
//! **dagx works with ANY type automatically!** The `#[task]` macro generates type-specific //! **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 //! extraction logic, so you don't need to implement any special traits. Just make sure your
//! type implements: //! type implements:
//! - `Clone` (so it can be extracted from Arc)
//! - `Send + Sync` (required for async tasks) //! - `Send + Sync` (required for async tasks)
//! - `'static` (no lifetime constraints) //! - `'static` (no lifetime constraints)
//! //!
@@ -39,8 +37,7 @@
use dagx::{task, DagRunner, TaskHandle}; use dagx::{task, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
// Custom type - just derive Clone! #[derive(Debug)]
#[derive(Clone, Debug)]
struct User { struct User {
name: String, name: String,
age: u32, age: u32,
+1 -1
View File
@@ -446,7 +446,7 @@ async fn main() {
.unwrap(); .unwrap();
// Check results // Check results
match dag.get(payment).unwrap() { match dag.get(payment).unwrap().as_ref() {
Ok(_) => println!("\nPayment validation succeeded"), Ok(_) => println!("\nPayment validation succeeded"),
Err(trace) => { Err(trace) => {
println!("\n[{}] Payment validation failed:", correlation_id); println!("\n[{}] Payment validation failed:", correlation_id);
+3 -3
View File
@@ -227,7 +227,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e), Err(e) => eprintln!("Final error: {}\n", e),
} }
@@ -253,7 +253,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e), Err(e) => eprintln!("Final error: {}\n", e),
} }
@@ -279,7 +279,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
match dag.get(logged).unwrap() { match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result), Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e), Err(e) => eprintln!("Final error: {}\n", e),
} }
-130
View File
@@ -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 ===");
}
-9
View File
@@ -11,15 +11,6 @@ use crate::types::{NodeId, TaskHandle};
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use tracing::debug; 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. /// Node builder that tracks dependency completion via type state.
/// ///
/// A `TaskBuilder<Tk, Deps>` is returned from [`DagRunner::add_task`] and uses the type-state /// A `TaskBuilder<Tk, Deps>` is returned from [`DagRunner::add_task`] and uses the type-state
+6 -19
View File
@@ -1,6 +1,5 @@
//! Unit tests for builder module //! Unit tests for builder module
use crate::builder::IsUnitType;
use crate::runner::DagRunner; use crate::runner::DagRunner;
use crate::types::TaskHandle; use crate::types::TaskHandle;
@@ -52,24 +51,12 @@ fn test_task_builder_from_conversion() {
let builder = dag.add_task(TestTask { value: 42 }); let builder = dag.add_task(TestTask { value: 42 });
// Convert builder to TaskHandle // Convert builder to TaskHandle
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<_> = builder.into();
// Check that the ID is preserved // Check that the ID is preserved
assert_eq!(handle.id.0, 0); // First task should have ID 0 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<T: IsUnitType>() {}
// 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] #[test]
fn test_task_builder_depends_on_returns_handle() { fn test_task_builder_depends_on_returns_handle() {
init_tracing(); init_tracing();
@@ -79,7 +66,7 @@ fn test_task_builder_depends_on_returns_handle() {
let dependent = dag.add_task(TestTaskWithInput).depends_on(source); let dependent = dag.add_task(TestTaskWithInput).depends_on(source);
// depends_on should return a TaskHandle // depends_on should return a TaskHandle
let _handle: TaskHandle<i32> = dependent; let _handle: TaskHandle<_> = dependent;
} }
#[test] #[test]
@@ -92,9 +79,9 @@ fn test_task_builder_chain() {
let t3 = dag.add_task(TestTaskWithInput).depends_on(t2); let t3 = dag.add_task(TestTaskWithInput).depends_on(t2);
// All should return TaskHandles // All should return TaskHandles
let _: TaskHandle<i32> = t1; let _: TaskHandle<_> = t1;
let _: TaskHandle<i32> = t2; let _: TaskHandle<_> = t2;
let _: TaskHandle<i32> = t3; let _: TaskHandle<_> = t3;
} }
#[test] #[test]
@@ -118,7 +105,7 @@ fn test_multiple_dependencies() {
let sum = dag.add_task(AddTask).depends_on((a, b)); let sum = dag.add_task(AddTask).depends_on((a, b));
// Should return a TaskHandle // Should return a TaskHandle
let _: TaskHandle<i32> = sum; let _: TaskHandle<_> = sum;
} }
#[test] #[test]
+1 -1
View File
@@ -50,7 +50,7 @@ impl DepsTuple<()> for () {
} }
// Implementation for single dependency (all forms) // Implementation for single dependency (all forms)
impl<T> DepsTuple<T::Output> for T impl<T> DepsTuple<(T::Output,)> for T
where where
T: IntoNodeRef, T: IntoNodeRef,
{ {
+1 -1
View File
@@ -207,7 +207,7 @@ fn test_deps_handle_ref_explicit() {
// Create a reference and explicitly call to_node_ids // Create a reference and explicitly call to_node_ids
let handle_ref = &handle; let handle_ref = &handle;
let node_ids = DepsTuple::<i32>::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.len(), 1);
assert_eq!(node_ids[0], NodeId(77)); assert_eq!(node_ids[0], NodeId(77));
+80 -154
View File
@@ -5,8 +5,8 @@
//! **NOTE**: This module is now primarily for internal use. The `ExtractInput` trait is only //! **NOTE**: This module is now primarily for internal use. The `ExtractInput` trait is only
//! used by the internal `task_fn` test helper function. //! used by the internal `task_fn` test helper function.
//! //!
//! **For regular tasks**: The `#[task]` macro generates inline `extract_and_run()` methods with //! **For regular tasks**: The `#[task]` macro generates inline `run()` methods with
//! type-specific extraction logic. This means **ANY type** implementing `Clone + Send + Sync` //! type-specific extraction logic. This means **ANY type** implementing `Send + Sync`
//! works automatically without needing `ExtractInput` implementations! //! works automatically without needing `ExtractInput` implementations!
//! //!
//! # How it works //! # How it works
@@ -15,8 +15,8 @@
//! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones). //! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones).
//! //!
//! **Modern approach (via `#[task]` macro)**: //! **Modern approach (via `#[task]` macro)**:
//! - The macro generates custom extraction logic inline in `extract_and_run()` //! - The macro generates custom extraction logic inline in `run()`
//! - Works with ANY type (just needs `Clone + Send + Sync`) //! - Works with ANY type (just needs `Send + Sync + 'static`)
//! - No trait implementations needed! //! - No trait implementations needed!
//! //!
//! **Legacy approach (via `ExtractInput` trait)**: //! **Legacy approach (via `ExtractInput` trait)**:
@@ -32,27 +32,30 @@
//! //!
//! This is standard practice in Rust for working with tuples of different arities. //! This is standard practice in Rust for working with tuples of different arities.
use std::any::Any; use std::{collections::HashMap, hash::Hash, sync::Arc};
use std::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 /// 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. /// 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)] #[allow(dead_code)]
pub(crate) trait ExtractInput: Sized + Clone { pub trait ExtractInput: Sized + Clone {
fn extract_from_deps( type Input: Send;
deps: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input>;
) -> impl std::future::Future<Output = Result<Self, String>> + Send;
fn extract_from_task_input<'input>(
input: TaskInput<'input, Self::Input>,
) -> Result<Self::Retv<'input>, String>;
} }
// Unit type - no dependencies // Unit type - no dependencies
impl ExtractInput for () { impl ExtractInput for () {
async fn extract_from_deps(_deps: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> { type Input = ();
type Retv<'input> = ();
fn extract_from_task_input(_input: TaskInput<'_, Self>) -> Result<Self, String> {
Ok(()) Ok(())
} }
} }
@@ -65,15 +68,10 @@ macro_rules! impl_extract_single {
($($t:ty),+) => { ($($t:ty),+) => {
$( $(
impl ExtractInput for $t { impl ExtractInput for $t {
async fn extract_from_deps(mut dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> { type Input = ($t,);
if dependencies.len() != 1 { type Retv<'input> = &'input $t;
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); fn extract_from_task_input<'input>(input: TaskInput<'input, Self::Input>) -> Result<Self::Retv<'input>, String> {
} Ok(input.next().0)
let arc_value = dependencies.pop()
.unwrap()
.downcast::<$t>()
.map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<$t>()))?;
Ok((*arc_value).clone())
} }
} }
)+ )+
@@ -90,24 +88,13 @@ where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
E: 'static + Clone + Send + Sync, E: 'static + Clone + Send + Sync,
{ {
async fn extract_from_deps( type Input = (Self,);
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input> = &'input Result<T, E>;
) -> Result<Self, String> {
if dependencies.len() != 1 { fn extract_from_task_input<'input>(
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); input: TaskInput<'input, (Self,)>,
} ) -> Result<Self::Retv<'input>, String> {
let arc_value = dependencies Ok(input.next().0)
.pop()
.unwrap()
.downcast::<Result<T, E>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Result<{}, {}>>",
std::any::type_name::<T>(),
std::any::type_name::<E>()
)
})?;
Ok((*arc_value).clone())
} }
} }
@@ -116,23 +103,13 @@ impl<T> ExtractInput for Option<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_deps( type Input = (Self,);
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input> = &'input Option<T>;
) -> Result<Self, String> {
if dependencies.len() != 1 { fn extract_from_task_input<'input>(
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); input: TaskInput<'input, (Self,)>,
} ) -> Result<Self::Retv<'input>, String> {
let arc_value = dependencies Ok(input.next().0)
.pop()
.unwrap()
.downcast::<Option<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Option<{}>>",
std::any::type_name::<T>()
)
})?;
Ok((*arc_value).clone())
} }
} }
@@ -141,77 +118,43 @@ impl<T> ExtractInput for Vec<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_deps( type Input = (Self,);
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input> = &'input Vec<T>;
) -> Result<Self, String> {
if dependencies.len() != 1 { fn extract_from_task_input<'input>(
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); input: TaskInput<'input, (Self,)>,
} ) -> Result<Self::Retv<'input>, String> {
let arc_value = dependencies Ok(input.next().0)
.pop()
.unwrap()
.downcast::<Vec<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Vec<{}>>",
std::any::type_name::<T>()
)
})?;
Ok((*arc_value).clone())
} }
} }
// Implement ExtractInput for HashMap types // Implement ExtractInput for HashMap types
impl<K, V> ExtractInput for std::collections::HashMap<K, V> impl<K, V> ExtractInput for HashMap<K, V>
where where
K: 'static + Clone + Send + Sync + Eq + std::hash::Hash, K: 'static + Clone + Send + Sync + Eq + Hash,
V: 'static + Clone + Send + Sync, V: 'static + Clone + Send + Sync,
{ {
async fn extract_from_deps( type Input = (Self,);
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input> = &'input HashMap<K, V>;
) -> Result<Self, String> {
if dependencies.len() != 1 { fn extract_from_task_input<'input>(
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); input: TaskInput<'input, (Self,)>,
} ) -> Result<Self::Retv<'input>, String> {
let arc_value = dependencies Ok(input.next().0)
.pop()
.unwrap()
.downcast::<std::collections::HashMap<K, V>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<HashMap<{}, {}>>",
std::any::type_name::<K>(),
std::any::type_name::<V>()
)
})?;
Ok((*arc_value).clone())
} }
} }
// 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<T> ExtractInput for Arc<T> impl<T> ExtractInput for Arc<T>
where where
T: 'static + Clone + Send + Sync, T: 'static + Clone + Send + Sync,
{ {
async fn extract_from_deps( type Input = (Self,);
mut dependencies: Vec<Arc<dyn Any + Send + Sync>>, type Retv<'input> = &'input Arc<T>;
) -> Result<Self, String> {
if dependencies.len() != 1 { fn extract_from_task_input<'input>(
return Err(format!("Expected 1 dependency, got {}", dependencies.len())); input: TaskInput<'input, (Self,)>,
} ) -> Result<Self::Retv<'input>, String> {
let arc_arc_value = dependencies Ok(input.next().0)
.pop()
.unwrap()
.downcast::<Arc<T>>()
.map_err(|_| {
format!(
"Type mismatch: expected Arc<Arc<{}>>",
std::any::type_name::<T>()
)
})?;
// Return the inner Arc (clone is cheap - just refcount increment)
Ok(Arc::clone(&*arc_arc_value))
} }
} }
@@ -228,35 +171,19 @@ where
/// ///
/// Therefore, no runtime validation is needed - if it compiles, the dependency count is correct. /// Therefore, no runtime validation is needed - if it compiles, the dependency count is correct.
macro_rules! impl_extract_tuple { macro_rules! impl_extract_tuple {
($($T:ident : $idx:tt),+) => { ($($T:ident),+) => {
impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) { impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) {
async fn extract_from_deps(dependencies: Vec<Arc<dyn Any + Send + Sync>>) -> Result<Self, String> { type Input = Self;
// Verify we have the correct number of dependencies type Retv<'input> = ($(&'input $T,)+);
let expected_count = [$($idx),+].len();
if dependencies.len() != expected_count {
return Err(format!("Expected {} dependencies, got {}", expected_count, dependencies.len()));
}
// Convert Vec to iterator so we can consume it fn extract_from_task_input<'input>(input: TaskInput<'input, Self::Input>) -> Result<Self::Retv<'input>, String> {
let mut iter = dependencies.into_iter(); $(
let ($T, input): (&'input $T, _) = input.next();
)+
// dependencies now contain Arc-wrapped values for efficient fanout let _input: TaskInput<'input, ()> = input;
// 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>()))?,
)+
);
// Clone inner values from Arc (cheap for small types, necessary for ownership) Ok(($($T,)+))
Ok(($(
(*arc_results.$idx).clone()
,)+))
} }
} }
}; };
@@ -265,16 +192,15 @@ macro_rules! impl_extract_tuple {
// Generate ExtractInput implementations for tuples of size 2-8. // Generate ExtractInput implementations for tuples of size 2-8.
// Each invocation creates an impl for a specific tuple size. // Each invocation creates an impl for a specific tuple size.
// Supporting up to 8 elements covers the vast majority of use cases. // Supporting up to 8 elements covers the vast majority of use cases.
impl_extract_tuple!(A:0, B:1); #[allow(non_snake_case)]
impl_extract_tuple!(A:0, B:1, C:2); mod impls {
impl_extract_tuple!(A:0, B:1, C:2, D:3); use super::*;
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);
#[cfg(test)] impl_extract_tuple!(A, B);
mod tests; impl_extract_tuple!(A, B, C);
impl_extract_tuple!(A, B, C, D);
#[cfg(test)] impl_extract_tuple!(A, B, C, D, E);
mod coverage_tests; 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);
}
-290
View File
@@ -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<T,E> type mismatch error message
// Create a dependency that provides the WRONG type (String instead of Result<i32, String>)
// Try to extract as Result<i32, String> - this will fail with type mismatch
let deps = vec![Arc::new("hello") as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
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<T> type mismatch error message
// Create a dependency that provides the WRONG type (i32 instead of Option<i32>)
// Try to extract as Option<i32> - this will fail with type mismatch
let deps = vec![Arc::new(42) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
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<T> type mismatch error message
// Create a dependency that provides the WRONG type (String instead of Vec<u8>)
// Try to extract as Vec<u8> - this will fail with type mismatch
let deps = vec![Arc::new("not a vec".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Vec::<u8>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
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<K,V> type mismatch error message
// Create a dependency that provides the WRONG type (Vec<i32> instead of HashMap)
// Try to extract as HashMap<String, i32> - this will fail with type mismatch
let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, i32>::extract_from_deps(deps).await;
// Verify the error message contains the expected type information
assert!(result.is_err());
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<T> type mismatch error message
// Arc is special: it expects Arc<Arc<T>> from channel
// Create a dependency that provides Arc<String> instead of Arc<Arc<String>>
let deps = vec![Arc::new("not double arc".to_string()) as Arc<dyn Any + Send + Sync>];
// Try to extract as Arc<String> - this will fail with type mismatch
// because it expects Arc<Arc<String>> in the channel
let result = Arc::<String>::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<i32, String> - wrong Ok type
let deps = vec![Arc::new(Ok::<_, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<i32, String> - wrong Err type
let deps = vec![Arc::new(Err::<i32, i32>(42)) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<i32> - wrong inner type
let deps = vec![Arc::new(Some("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<i32> - wrong element type
let deps = vec![Arc::new(vec!["a".to_string(), "b".to_string()]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<String, String> - wrong key type
let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<String, String> - wrong value type
let deps = vec![Arc::new(map) as Arc<dyn Any + Send + Sync>];
let result = HashMap::<String, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<i32> but expect Arc<String>
// Try to extract as Arc<String> - wrong inner type
let deps = vec![Arc::new(Arc::new(Arc::new(42))) as Arc<dyn Any + Send + Sync>];
let result = Arc::<String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
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<dyn Any + Send + Sync>];
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<String, String>, expect Result<Option<i32>, String>
// Try to extract as Result<Option<i32>, String> - nested type mismatch
let deps =
vec![Arc::new(Ok::<String, String>("test".to_string())) as Arc<dyn Any + Send + Sync>];
let result = Result::<Option<i32>, String>::extract_from_deps(deps).await;
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(
err_msg.contains("Result") && err_msg.contains("Type mismatch"),
"Expected Result type mismatch, got: {}",
err_msg
);
}
-296
View File
@@ -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<T,E> with wrong dependency count
let deps = vec![
Arc::new(Ok::<i32, String>(5)) as Arc<dyn Any + Send + Sync>,
Arc::new(Err::<i32, String>("test".to_string())) as Arc<dyn Any + Send + Sync>,
]; // Two dependencies when expecting 1
let result = Result::<i32, String>::extract_from_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<T,E> successful extraction
let deps = vec![Arc::new(Ok::<i32, String>(42)) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), Ok(42));
}
#[tokio::test]
async fn test_extract_option_type_single_dependency_error() {
// Test Option<T> with wrong dependency count
let deps = vec![
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
Arc::new(None::<String>) as Arc<dyn Any + Send + Sync>,
]; // Three dependencies when expecting 1
let result = Option::<String>::extract_from_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<T> successful extraction
let deps = vec![Arc::new(Some(99)) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(99));
}
#[tokio::test]
async fn test_extract_vec_type_single_dependency_error() {
// Test Vec<T> with wrong dependency count
let deps = vec![]; // Zero dependencies when expecting 1
let result = Vec::<u8>::extract_from_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<T> successful extraction
let deps = vec![Arc::new(vec![1, 2, 3]) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![1, 2, 3]);
}
#[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<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
]; // 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<dyn Any + Send + Sync>,
Arc::new(String::default()) as Arc<dyn Any + Send + Sync>,
];
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<dyn Any + Send + Sync>,
Arc::new(0.0) as Arc<dyn Any + Send + Sync>,
];
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<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
];
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<dyn Any + Send + Sync>,
Arc::new(100) as Arc<dyn Any + Send + Sync>,
];
// 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<dyn Any + Send + Sync>];
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<dyn Any + Send + Sync>];
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<dyn Any + Send + Sync>,
Arc::new(1) as Arc<dyn Any + Send + Sync>,
Arc::new(2) as Arc<dyn Any + Send + Sync>,
Arc::new(3) as Arc<dyn Any + Send + Sync>,
Arc::new(4) as Arc<dyn Any + Send + Sync>,
];
// Extract a 5-element tuple
let result = <(i32, i32, i32, i32, i32)>::extract_from_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<T> 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<dyn Any + Send + Sync>];
let result = Arc::<String>::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::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
Arc::new(HashMap::<String, i32>::default()) as Arc<dyn Any + Send + Sync>,
];
let result = HashMap::<String, i32>::extract_from_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<dyn Any + Send + Sync>];
let result = HashMap::<String, i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_result_extract_downcast_failure() {
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Result::<i32, String>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_option_extract_downcast_failure() {
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Option::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_vec_extract_downcast_failure() {
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Vec::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_arc_extract_wrong_dependency_count() {
let deps = vec![];
let result = Arc::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
}
#[tokio::test]
async fn test_arc_extract_downcast_failure() {
let deps = vec![Arc::new("wrong".to_string()) as Arc<dyn Any + Send + Sync>];
let result = Arc::<i32>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_tuple_wrong_dependency_count() {
let deps = vec![
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
Arc::new(0) as Arc<dyn Any + Send + Sync>,
];
let result = <(i32, i32)>::extract_from_deps(deps).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Expected 2 dependencies, got 3"));
}
+4 -6
View File
@@ -11,7 +11,7 @@
//! system. The public API is fully type-safe with no runtime type errors. Internal execution //! 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. //! 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 //! - **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. //! type-specific extraction logic for seamless integration.
//! - **Runtime-agnostic**: Works with any async runtime (Tokio, async-std, smol, Embassy, etc.) //! - **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 //! - **Type-state pattern**: The API guides you with compile-time errors if you wire dependencies
@@ -442,15 +442,13 @@
//! //!
//! ## Custom Types //! ## 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: //! the `#[task]` macro generates the necessary extraction logic:
//! //!
//! ```no_run //! ```no_run
//! # use dagx::{task, DagRunner, Task}; //! # use dagx::{task, DagRunner, Task};
//! # use futures::FutureExt; //! # use futures::FutureExt;
//! //!
//! // Just derive Clone - that's all you need!
//! #[derive(Clone)]
//! struct User { //! struct User {
//! name: String, //! name: String,
//! age: u32, //! age: u32,
@@ -645,7 +643,7 @@
//! } //! }
//! //!
//! // Downstream tasks receive &Vec<String> as normal //! // Downstream tasks receive &Vec<String> as normal
//! // Behind the scenes: Arc<Vec<String>> is cloned cheaply, then inner Vec extracted //! // Behind the scenes: Arc<Vec<String>> is cloned cheaply, then a reference to the inner Vec is extracted
//! struct ProcessData; //! struct ProcessData;
//! #[task] //! #[task]
//! impl ProcessData { //! impl ProcessData {
@@ -791,7 +789,7 @@ mod types;
pub use builder::TaskBuilder; pub use builder::TaskBuilder;
pub use error::{DagError, DagResult}; pub use error::{DagError, DagResult};
pub use runner::DagRunner; pub use runner::DagRunner;
pub use task::Task; pub use task::{Task, TaskInput};
pub use types::{Pending, TaskHandle}; pub use types::{Pending, TaskHandle};
// Re-export the procedural macro // Re-export the procedural macro
+7 -16
View File
@@ -12,8 +12,7 @@ use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use crate::error::DagError; use crate::error::DagError;
use crate::task::Task; use crate::task::{Task, TaskInput};
use crate::types::NodeId;
/// Type-erased async execution result (Arc-wrapped for efficient fanout) /// Type-erased async execution result (Arc-wrapped for efficient fanout)
type ExecuteFuture = type ExecuteFuture =
@@ -52,37 +51,29 @@ pub(crate) trait ExecutableNode: Send {
/// ///
/// The output is also returned from execute_with_deps for central storage. /// The output is also returned from execute_with_deps for central storage.
pub(crate) struct TypedNode<T: Task> { pub(crate) struct TypedNode<T: Task> {
pub(crate) id: NodeId,
pub(crate) task: T, pub(crate) task: T,
} }
impl<T: Task + 'static> TypedNode<T> { impl<T: Task + 'static> TypedNode<T> {
pub(crate) fn new(id: NodeId, task: T) -> Self { pub(crate) fn new(task: T) -> Self {
Self { id, task } Self { task }
} }
} }
impl<T: Task + 'static> ExecutableNode for TypedNode<T> impl<T: Task + 'static> ExecutableNode for TypedNode<T>
where where
T::Input: 'static + Clone, T::Input: 'static,
T::Output: 'static + Clone, T::Output: 'static,
{ {
fn execute_with_deps( fn execute_with_deps(
self: Box<Self>, self: Box<Self>,
dependencies: Vec<Arc<dyn Any + Send + Sync>>, dependencies: Vec<Arc<dyn Any + Send + Sync>>,
) -> ExecuteFuture { ) -> ExecuteFuture {
Box::pin(async move { 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 // generated by the #[task] macro. This allows ANY type to work without
// requiring ExtractInput trait implementations. // requiring ExtractInput trait implementations.
let output = match self.task.extract_and_run(dependencies).await { let output = self.task.run(TaskInput::new(dependencies.iter())).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 });
}
};
// Arc Wrapping Strategy: // Arc Wrapping Strategy:
// Wrap output in Arc ONCE for efficient fan-out sharing. // Wrap output in Arc ONCE for efficient fan-out sharing.
+4 -23
View File
@@ -3,7 +3,6 @@
use std::sync::Arc; use std::sync::Arc;
use crate::node::{ExecutableNode, TypedNode}; use crate::node::{ExecutableNode, TypedNode};
use crate::types::NodeId;
// Simple test task // Simple test task
struct TestTask { 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] #[tokio::test]
async fn test_execute_with_deps_success() { async fn test_execute_with_deps_success() {
// Test successful execution with channels // Test successful execution with channels
let dependency_node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 21 })); let dependency_node = Box::new(TypedNode::new(TestTask { value: 21 }));
let dependent_node = Box::new(TypedNode::new(NodeId(1), TaskWithDependency)); let dependent_node = Box::new(TypedNode::new(TaskWithDependency));
// Execute dependency node with sender // Execute dependency node with sender
let dep_dependencies = vec![]; let dep_dependencies = vec![];
@@ -71,7 +52,7 @@ async fn test_execute_with_deps_success() {
#[tokio::test] #[tokio::test]
async fn test_execute_with_deps_sink_node() { async fn test_execute_with_deps_sink_node() {
// Test that sink nodes (no dependents) return their output // 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 // No senders means this is a sink node
let dependencies = vec![]; let dependencies = vec![];
@@ -88,7 +69,7 @@ async fn test_execute_with_deps_sink_node() {
#[tokio::test] #[tokio::test]
async fn test_execute_with_deps_non_sink_node() { async fn test_execute_with_deps_non_sink_node() {
// Test that non-sink nodes send output via channels // 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) // Execute with senders (non-sink node)
let node = Box::new(node); let node = Box::new(node);
+12 -14
View File
@@ -199,8 +199,8 @@ impl DagRunner {
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending> pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
where where
Tk: Task + Sync + 'static, Tk: Task + Sync + 'static,
Tk::Input: 'static + Clone, Tk::Input: 'static,
Tk::Output: 'static + Clone, Tk::Output: 'static,
{ {
let id = self.alloc_id(); let id = self.alloc_id();
@@ -211,7 +211,7 @@ impl DagRunner {
"adding task to DAG" "adding task to DAG"
); );
let node = TypedNode::new(id, task); let node = TypedNode::new(task);
self.nodes.lock().push(Some(Box::new(node))); self.nodes.lock().push(Some(Box::new(node)));
self.edges.lock().insert(id, Vec::new()); self.edges.lock().insert(id, Vec::new());
self.dependents.lock().insert(id, Vec::new()); self.dependents.lock().insert(id, Vec::new());
@@ -519,6 +519,7 @@ impl DagRunner {
/// # Behavior /// # Behavior
/// ///
/// All task outputs are stored after execution and can be retrieved via get(). /// 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 /// # Errors
/// ///
@@ -557,26 +558,26 @@ impl DagRunner {
/// assert_eq!(dag.get(task).unwrap(), 42); /// assert_eq!(dag.get(task).unwrap(), 42);
/// # }; /// # };
/// ``` /// ```
pub fn get<T: 'static + Clone + Send + Sync, H>(&self, handle: H) -> DagResult<T> pub fn get<T: 'static + Send + Sync, H>(&self, handle: H) -> DagResult<T>
where where
H: Into<TaskHandle<T>>, H: Into<TaskHandle<T>>,
{ {
let handle: TaskHandle<T> = handle.into(); let handle: TaskHandle<T> = 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, task_id: handle.id.0,
})?; })?;
// Downcast Arc<dyn Any> to Arc<T>, then clone the inner value // Downcast Arc<dyn Any> to Arc<T>
// The Arc itself is stored for efficient sharing, but get() clones the data let output = arc_output
Arc::clone(arc_output)
.downcast::<T>() .downcast::<T>()
.map(|arc| (*arc).clone())
.map_err(|_| DagError::TypeMismatch { .map_err(|_| DagError::TypeMismatch {
expected: std::any::type_name::<T>(), expected: std::any::type_name::<T>(),
found: "unknown", found: "unknown",
}) })?;
Ok(Arc::into_inner(output).unwrap())
} }
#[inline] #[inline]
@@ -673,6 +674,3 @@ impl DagRunner {
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[cfg(test)]
mod coverage_tests;
-237
View File
@@ -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<String> = 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::<String>());
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<u8>
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<u8>
let wrong_handle: TaskHandle<Vec<u8>> = 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::<Vec<u8>>());
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<HashMap<String, i32>> = 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<i32> and Vec<String>
use crate::task_fn;
let dag = DagRunner::new();
// Task outputs Vec<i32>
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<String>
let wrong_handle: TaskHandle<Vec<String>> = 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::<Vec<String>>());
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<i32>
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<String>
let wrong_handle: TaskHandle<Option<String>> = 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::<Option<String>>());
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<String> = 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<bool> = 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<i32> = 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());
}
-21
View File
@@ -185,27 +185,6 @@ fn test_add_task_increments_id() {
assert_eq!(dag.nodes.lock().len(), 3); 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] #[tokio::test]
async fn test_invalid_node_id_in_get() { async fn test_invalid_node_id_in_get() {
init_tracing(); init_tracing();
+84 -56
View File
@@ -3,16 +3,18 @@
//! This module defines the core Task trait that all DAG nodes must implement, //! This module defines the core Task trait that all DAG nodes must implement,
//! along with convenience functions for creating tasks from closures. //! 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. /// A unit of async work with typed inputs and outputs.
/// ///
/// Use the [`task`](crate::task) macro to implement this trait. The macro automatically /// 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 /// 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 /// **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 /// # Task Patterns
/// ///
@@ -96,18 +98,82 @@ pub trait Task: Send {
type Input: Send; type Input: Send;
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send; fn run(self, input: TaskInput<Self::Input>) -> impl Future<Output = Self::Output> + Send;
}
/// Internal method: Extract input from list of type-erased dependencies and execute the task. pub struct TaskInput<'inputs, Input: Send> {
/// inputs: Iter<'inputs, Arc<dyn Any + Send + Sync + 'static>>,
/// This method is implemented automatically by the #[task] macro with inline phantom: PhantomData<Input>,
/// extraction logic based on the task's parameter signature. This allows ANY }
/// type to work without requiring trait implementations.
#[doc(hidden)] impl<'inputs, Input: Send> TaskInput<'inputs, Input> {
fn extract_and_run( pub(crate) fn new(inputs: Iter<'inputs, Arc<dyn Any + Send + Sync + 'static>>) -> Self {
self, Self {
dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>, inputs,
) -> impl Future<Output = Result<Self::Output, String>> + Send; 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<I, O, F>
where
for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
I: ExtractInput + Send + Sync + 'static,
O: Send,
{
f: F,
_phantom: std::marker::PhantomData<fn(I) -> O>,
}
impl<I, O, F> Task for TaskFn<I, O, F>
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. /// 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 /// **INTERNAL USE ONLY**: This function is for testing purposes only and should
/// not be used in production code. Use the `#[task]` macro instead. /// not be used in production code. Use the `#[task]` macro instead.
#[doc(hidden)] #[doc(hidden)]
pub fn task_fn<I, O, Fut, F>(f: F) -> impl Task<Input = I, Output = O> pub fn task_fn<I, O, F>(f: F) -> TaskFn<I, O, F>
where where
F: FnMut(I) -> Fut + Send + 'static, for<'input> F: FnMut(I::Retv<'input>) -> O + Send,
Fut: Future<Output = O> + Send + 'static, I: ExtractInput + Send + Sync + 'static,
I: Send + crate::extract::ExtractInput + 'static, I::Input: Send + Sync + 'static,
O: Send + Sync + 'static, O: Send + Sync + 'static,
{ {
struct TaskFn<I, O, Fut, F>
where
F: FnMut(I) -> Fut + Send,
Fut: Future<Output = O> + Send,
I: Send,
O: Send,
{
f: F,
_phantom: std::marker::PhantomData<fn(I) -> O>,
}
impl<I, O, Fut, F> Task for TaskFn<I, O, Fut, F>
where
F: FnMut(I) -> Fut + Send,
Fut: Future<Output = O> + 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<Arc<dyn std::any::Any + Send + Sync>>,
) -> Result<Self::Output, String> {
// For task_fn, we use ExtractInput since we don't have macro-generated extraction
let input = I::extract_from_deps(dependencies)
.await
.map_err(|e| format!("Failed to extract input: {}", e))?;
Ok(self.run(input).await)
}
}
TaskFn { TaskFn {
f, f,
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
+83 -45
View File
@@ -1,6 +1,7 @@
//! Unit tests for task module //! 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::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; 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 // Test line 111 in task.rs - Task implementation for TaskFn
// Create a simple task function // Create a simple task function
let task = task_fn(|input: i32| async move { input * 2 }); let task = task_fn::<i32, _, _>(|input: &i32| input * 2);
// Test the Task trait implementation // Test the Task trait implementation
let result = task.run(42).await; let result = task
.run(TaskInput::new(
[Arc::new(42) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 84); assert_eq!(result, 84);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_unit_input() { async fn test_task_fn_with_unit_input() {
// 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<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 100); assert_eq!(result, 100);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_tuple_input() { async fn test_task_fn_with_tuple_input() {
// 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<dyn Any + Send + Sync + 'static>,
Arc::new(20) as Arc<dyn Any + Send + Sync + 'static>,
]
.iter(),
))
.await;
assert_eq!(result, 30); assert_eq!(result, 30);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_string_input() { async fn test_task_fn_with_string_input() {
// Test task_fn with String input // Test task_fn with String input
let task = task_fn(|input: String| { let task = task_fn::<String, _, _>(|input: &String| input.len());
let len = input.len();
async move { len }
});
let test_string = String::from("hello"); 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<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 5); assert_eq!(result, 5);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_complex_output() { async fn test_task_fn_with_complex_output() {
// Test task_fn with complex output type // 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::<i32, _, _>(|&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<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, vec![5, 10, 15]); 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() { async fn test_task_fn_stateful_closure() {
// Test that task_fn can capture state // Test that task_fn can capture state
let multiplier = 3; let multiplier = 3;
let task = task_fn(move |input: i32| async move { input * multiplier }); let task = task_fn::<i32, _, _>(move |input: &i32| input * multiplier);
let result = task.run(7).await; let result = task
.run(TaskInput::new(
[Arc::new(7) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 21); assert_eq!(result, 21);
} }
@@ -74,34 +100,30 @@ struct CustomTask {
} }
impl Task for CustomTask { impl Task for CustomTask {
type Input = i32; type Input = (i32,);
type Output = i32; type Output = i32;
#[allow(refining_impl_trait)] #[allow(refining_impl_trait)]
fn run(mut self, input: Self::Input) -> Pin<Box<dyn Future<Output = Self::Output> + Send>> { fn run(
mut self,
input: TaskInput<'_, Self::Input>,
) -> Pin<Box<dyn Future<Output = Self::Output> + Send + '_>> {
Box::pin(async move { Box::pin(async move {
self.state += input; self.state += input.next().0;
self.state self.state
}) })
} }
#[allow(refining_impl_trait)]
fn extract_and_run(
self,
_dependencies: Vec<Arc<dyn std::any::Any + Send + Sync>>,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, String>> + Send>> {
Box::pin(async move {
// This is a test helper - just return an error
Err("extract_and_run not implemented for CustomTask (test helper)".to_string())
})
}
} }
#[tokio::test] #[tokio::test]
async fn test_custom_task_implementation() { async fn test_custom_task_implementation() {
let task = CustomTask { state: 10 }; let task = CustomTask { state: 10 };
let result1 = task.run(5).await; let result1 = task
.run(TaskInput::new(
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result1, 15); assert_eq!(result1, 15);
// Can't call run again since task was consumed // Can't call run again since task was consumed
@@ -131,15 +153,21 @@ fn test_task_trait_bounds() {
#[tokio::test] #[tokio::test]
async fn test_task_fn_single_call() { async fn test_task_fn_single_call() {
// Test that task_fn works (can only call once due to ownership) // 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::<i32, _, _>(|x: &i32| x + 1);
assert_eq!(task.run(1).await, 2); assert_eq!(
task.run(TaskInput::new(
[Arc::new(1) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
2
);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_result_type() { async fn test_task_fn_with_result_type() {
// Test task_fn returning Result // Test task_fn returning Result
let task = task_fn(|x: i32| async move { let task = task_fn::<i32, _, _>(|&x: &i32| {
if x > 0 { if x > 0 {
Ok(x * 2) Ok(x * 2)
} else { } 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<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Ok(10)
);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_with_option_type() { async fn test_task_fn_with_option_type() {
// Test task_fn returning Option // Test task_fn returning Option
let task = task_fn(|x: i32| async move { let task = task_fn::<i32, _, _>(|&x: &i32| if x > 0 { Some(x * 2) } else { None });
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<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Some(10)
);
} }
#[tokio::test] #[tokio::test]
async fn test_task_fn_explicit_trait_call() { async fn test_task_fn_explicit_trait_call() {
// Test explicit Task trait method call to cover line 121 // Test explicit Task trait method call to cover line 121
let task = task_fn(|x: i32| async move { x * 3 }); let task = task_fn::<i32, _, _>(|x: &i32| x * 3);
// Explicitly call the Task trait's run method // 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<dyn Any + Send + Sync + 'static>].iter()),
)
.await;
assert_eq!(result, 30); assert_eq!(result, 30);
} }
+2 -5
View File
@@ -3,7 +3,7 @@
//! This module defines the fundamental types used throughout the DAG system, //! This module defines the fundamental types used throughout the DAG system,
//! including node identifiers, task handles, and type-state markers. //! including node identifiers, task handles, and type-state markers.
use crate::builder::{IsUnitType, TaskBuilder}; use crate::builder::TaskBuilder;
use crate::task::Task; use crate::task::Task;
/// Opaque node identifier /// Opaque node identifier
@@ -63,10 +63,7 @@ pub struct Pending;
// TaskBuilder can be converted to a TaskHandle ONLY if Input is () (unit type) // 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() // This enforces at compile time that tasks with non-unit inputs MUST call .depends_on()
impl<'a, Tk: Task, Deps> From<TaskBuilder<'a, Tk, Deps>> for TaskHandle<Tk::Output> impl<'a, Tk: Task<Input = ()>, Deps> From<TaskBuilder<'a, Tk, Deps>> for TaskHandle<Tk::Output> {
where
Tk::Input: IsUnitType,
{
fn from(node: TaskBuilder<'a, Tk, Deps>) -> Self { fn from(node: TaskBuilder<'a, Tk, Deps>) -> Self {
TaskHandle { TaskHandle {
id: node.id, id: node.id,
+7 -15
View File
@@ -29,7 +29,7 @@ async fn test_unit_type_taskbuilder_to_handle_conversion() {
// Explicitly convert to TaskHandle using From implementation // Explicitly convert to TaskHandle using From implementation
// This triggers the From<TaskBuilder> implementation at line 69-74, // This triggers the From<TaskBuilder> implementation at line 69-74,
// specifically covering line 71 // specifically covering line 71
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<_> = builder.into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -76,7 +76,7 @@ async fn test_unit_type_conversion_with_multiple_tasks() {
// Convert each builder to handle explicitly // Convert each builder to handle explicitly
let first_handle: TaskHandle<String> = first_builder.into(); let first_handle: TaskHandle<String> = first_builder.into();
let second_handle: TaskHandle<i32> = second_builder.into(); let second_handle: TaskHandle<_> = second_builder.into();
let third_handle: TaskHandle<bool> = third_builder.into(); let third_handle: TaskHandle<bool> = third_builder.into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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)); let builder = dag.add_task(StatefulTask::new(21));
// Explicit conversion - triggers line 71 // Explicit conversion - triggers line 71
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<_> = builder.into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -143,7 +143,6 @@ async fn test_taskbuilder_conversion_assignment() {
// Conversion through type annotation and assignment // Conversion through type annotation and assignment
// This also uses From<TaskBuilder> implementation // This also uses From<TaskBuilder> implementation
let handle: TaskHandle<Vec<i32>> = builder.into(); let handle: TaskHandle<Vec<i32>> = builder.into();
let id = handle.id;
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -151,13 +150,6 @@ async fn test_taskbuilder_conversion_assignment() {
let result = dag.get(handle).unwrap(); let result = dag.get(handle).unwrap();
assert_eq!(result, vec![1, 2, 3]); assert_eq!(result, vec![1, 2, 3]);
// Verify the ID was preserved correctly through conversion
let another_handle = TaskHandle::<Vec<i32>> {
id,
_phantom: std::marker::PhantomData,
};
assert_eq!(dag.get(another_handle).unwrap(), vec![1, 2, 3]);
} }
#[tokio::test] #[tokio::test]
@@ -184,7 +176,7 @@ async fn test_mixed_unit_and_dependent_tasks() {
// Source is unit-type, can be converted // Source is unit-type, can be converted
let source_builder = dag.add_task(Source); let source_builder = dag.add_task(Source);
let source_handle: TaskHandle<i32> = source_builder.into(); // Covers line 71 let source_handle: TaskHandle<_> = source_builder.into(); // Covers line 71
// Doubler needs dependencies, so we use it differently // Doubler needs dependencies, so we use it differently
let doubler = dag.add_task(Doubler).depends_on(source_handle); let doubler = dag.add_task(Doubler).depends_on(source_handle);
@@ -260,9 +252,9 @@ fn test_multiple_conversions_preserve_ids() {
let id3 = builder3.id; let id3 = builder3.id;
// Convert all builders // Convert all builders
let handle1: TaskHandle<i32> = builder1.into(); let handle1: TaskHandle<_> = builder1.into();
let handle2: TaskHandle<i32> = builder2.into(); let handle2: TaskHandle<_> = builder2.into();
let handle3: TaskHandle<i32> = builder3.into(); let handle3: TaskHandle<_> = builder3.into();
// Verify IDs are preserved and sequential // Verify IDs are preserved and sequential
assert_eq!(handle1.id, id1); assert_eq!(handle1.id, id1);
+2 -2
View File
@@ -40,7 +40,7 @@ fn test_node_id_operations() {
#[test] #[test]
fn test_task_handle_clone() { fn test_task_handle_clone() {
let handle: TaskHandle<i32> = TaskHandle { let handle: TaskHandle<(i32,)> = TaskHandle {
id: NodeId(42), id: NodeId(42),
_phantom: PhantomData, _phantom: PhantomData,
}; };
@@ -129,7 +129,7 @@ fn test_task_builder_from_conversion() {
let builder_id = builder.id; let builder_id = builder.id;
// This conversion uses the From implementation // This conversion uses the From implementation
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<_> = builder.into();
assert_eq!(handle.id, builder_id); assert_eq!(handle.id, builder_id);
} }
+33 -35
View File
@@ -11,12 +11,12 @@ async fn test_1000_level_deep_chain() -> DagResult<()> {
// Test a linear chain of 1000 tasks // Test a linear chain of 1000 tasks
let dag = DagRunner::new(); 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 let mut current = first.into(); // Convert to TaskHandle
for _i in 1..1000 { for _i in 1..1000 {
current = dag current = dag
.add_task(task_fn(move |prev: i32| async move { prev + 1 })) .add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
.depends_on(current); .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 // Test a deep chain where each node has a side branch
let dag = DagRunner::new(); 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<i32> = first.into(); let mut main_chain: dagx::TaskHandle<i32> = first.into();
let mut branches = Vec::new(); let mut branches = Vec::new();
for i in 1..500 { for i in 1..500 {
// Main chain continues // Main chain continues
main_chain = dag main_chain = dag
.add_task(task_fn(move |prev: i32| async move { prev + 1 })) .add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
.depends_on(main_chain); .depends_on(main_chain);
// Create a branch at every 10th node // Create a branch at every 10th node
if i % 10 == 0 { if i % 10 == 0 {
let branch = dag let branch = dag
.add_task(task_fn(move |main_val: i32| async move { main_val * 2 })) .add_task(task_fn::<i32, _, _>(move |main_val: &i32| main_val * 2))
.depends_on(main_chain); .depends_on(main_chain);
branches.push(branch); branches.push(branch);
} }
@@ -71,12 +71,12 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
let mut chain_ends = Vec::new(); let mut chain_ends = Vec::new();
for chain_id in 0..8 { 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<i32> = first.into(); let mut current: dagx::TaskHandle<i32> = first.into();
for _ in 0..500 { for _ in 0..500 {
current = dag current = dag
.add_task(task_fn(|prev: i32| async move { prev + 1 })) .add_task(task_fn::<i32, _, _>(|&prev: &i32| prev + 1))
.depends_on(current); .depends_on(current);
} }
@@ -85,8 +85,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
// Add a final task that depends on all chains // Add a final task that depends on all chains
let final_task = dag let final_task = dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32, i32), _, _>(
|inputs: (i32, i32, i32, i32, i32, i32, i32, i32)| async move { |inputs: (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
inputs.0 inputs.0
+ inputs.1 + inputs.1
+ inputs.2 + inputs.2
@@ -126,28 +126,24 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let order = Arc::new(AtomicUsize::new(0)); 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(); let order = order.clone();
move |_: ()| { move |_: ()| {
let order = order.clone(); let order = order.clone();
async move { assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0);
assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0); 0
0
}
} }
})); }));
let mut current: dagx::TaskHandle<i32> = first.into(); let mut current: dagx::TaskHandle<i32> = first.into();
for expected in 1..100 { for expected in 1..100 {
current = dag current = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let order = order.clone(); let order = order.clone();
move |prev: i32| { move |&prev: &i32| {
let order = order.clone(); let order = order.clone();
async move { assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected);
assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected); prev + 1
prev + 1
}
} }
})) }))
.depends_on(current); .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<i32> { fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
if depth == 0 { 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 task.into() // Convert TaskBuilder to TaskHandle
} else { } else {
let left = build_tree(dag, depth - 1, value * 2); let left = build_tree(dag, depth - 1, value * 2);
let right = build_tree(dag, depth - 1, value * 2 + 1); let right = build_tree(dag, depth - 1, value * 2 + 1);
dag.add_task(task_fn(move |(l, r): (i32, i32)| async move { l + r })) dag.add_task(task_fn::<(i32, i32), _, _>(move |(l, r): (&i32, &i32)| {
.depends_on((&left, &right)) l + r
}))
.depends_on((&left, &right))
} }
} }
@@ -196,15 +194,15 @@ async fn test_fibonacci_chain() -> DagResult<()> {
// Test a Fibonacci-like dependency pattern // Test a Fibonacci-like dependency pattern
let dag = DagRunner::new(); let dag = DagRunner::new();
let f0_builder = dag.add_task(task_fn(|_: ()| async { 0i64 })); let f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
let f1_builder = dag.add_task(task_fn(|_: ()| async { 1i64 })); let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64));
let mut prev2: dagx::TaskHandle<i64> = f0_builder.into(); let mut prev2: dagx::TaskHandle<i64> = f0_builder.into();
let mut prev1: dagx::TaskHandle<i64> = f1_builder.into(); let mut prev1: dagx::TaskHandle<i64> = f1_builder.into();
for _ in 2..50 { for _ in 2..50 {
let next = dag 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)); .depends_on((&prev2, &prev1));
prev2 = prev1; prev2 = prev1;
@@ -225,12 +223,12 @@ async fn test_deep_chain_with_accumulator() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Use a simpler accumulation pattern - just sum // 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<i64> = first.into(); let mut current: dagx::TaskHandle<i64> = first.into();
for i in 1..=200 { for i in 1..=200 {
current = dag current = dag
.add_task(task_fn(move |sum: i64| async move { sum + i as i64 })) .add_task(task_fn::<i64, _, _>(move |sum: &i64| sum + i as i64))
.depends_on(current); .depends_on(current);
} }
@@ -247,8 +245,8 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
// Test a zigzag pattern: two chains that cross-reference each other // Test a zigzag pattern: two chains that cross-reference each other
let dag = DagRunner::new(); let dag = DagRunner::new();
let a_first = dag.add_task(task_fn(|_: ()| async { 1 })); let a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b_first = dag.add_task(task_fn(|_: ()| async { 2 })); let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let mut chain_a: dagx::TaskHandle<i32> = a_first.into(); let mut chain_a: dagx::TaskHandle<i32> = a_first.into();
let mut chain_b: dagx::TaskHandle<i32> = b_first.into(); let mut chain_b: dagx::TaskHandle<i32> = b_first.into();
@@ -257,19 +255,19 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
if i % 2 == 0 { if i % 2 == 0 {
// A depends on B // A depends on B
chain_a = dag chain_a = dag
.add_task(task_fn(move |b_val: i32| async move { b_val + 1 })) .add_task(task_fn::<i32, _, _>(move |b_val: &i32| b_val + 1))
.depends_on(chain_b); .depends_on(chain_b);
} else { } else {
// B depends on A // B depends on A
chain_b = dag chain_b = dag
.add_task(task_fn(move |a_val: i32| async move { a_val + 2 })) .add_task(task_fn::<i32, _, _>(move |a_val: &i32| a_val + 2))
.depends_on(chain_a); .depends_on(chain_a);
} }
} }
// Final task depends on both chains // Final task depends on both chains
let final_task = dag 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)); .depends_on((&chain_a, &chain_b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Ultimate deep chain test
let dag = DagRunner::new(); 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<u64> = first.into(); let mut current: dagx::TaskHandle<u64> = first.into();
for _ in 1..10_000 { for _ in 1..10_000 {
current = dag current = dag
.add_task(task_fn(|prev: u64| async move { prev + 1 })) .add_task(task_fn::<u64, _, _>(|prev: &u64| prev + 1))
.depends_on(current); .depends_on(current);
} }
+51 -56
View File
@@ -16,12 +16,9 @@ async fn test_10000_independent_tasks() -> DagResult<()> {
let mut tasks: Vec<_> = (0..10_000) let mut tasks: Vec<_> = (0..10_000)
.map(|i| { .map(|i| {
let counter = counter.clone(); let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone(); counter.fetch_add(1, Ordering::Relaxed);
async move { i
counter.fetch_add(1, Ordering::Relaxed);
i
}
})) }))
}) })
.collect(); .collect();
@@ -48,13 +45,15 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
// Bottom layer: 2048 nodes - convert to TaskHandles // Bottom layer: 2048 nodes - convert to TaskHandles
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048) let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
.map(|i| { .map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move { let task = dag.add_task(task_fn::<(), _, _>(
if i < 1024 { move |_: ()| {
1 if i < 1024 {
} else { 1
0 } else {
} 0
})); }
},
));
task.into() task.into()
}) })
.collect(); .collect();
@@ -74,20 +73,20 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
let task = match deps.len() { let task = match deps.len() {
1 => dag 1 => dag
.add_task(task_fn(|a: i32| async move { a })) .add_task(task_fn::<i32, _, _>(|&a: &i32| a))
.depends_on(deps[0]), .depends_on(deps[0]),
2 => dag 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])), .depends_on((deps[0], deps[1])),
3 => dag 3 => dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (i32, i32, i32)| async move { a + b + c }, |(a, b, c): (&i32, &i32, &i32)| a + b + c,
)) ))
.depends_on((deps[0], deps[1], deps[2])), .depends_on((deps[0], deps[1], deps[2])),
4 => dag 4 => dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move { .add_task(task_fn::<(i32, i32, i32, i32), _, _>(
a + b + c + d |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
})) ))
.depends_on((deps[0], deps[1], deps[2], deps[3])), .depends_on((deps[0], deps[1], deps[2], deps[3])),
_ => unreachable!(), _ => unreachable!(),
}; };
@@ -98,7 +97,7 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
// Final layer combines the last 2 // Final layer combines the last 2
let root = dag 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((&current_layer[0], &current_layer[1])); .depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Bottom layer: 2048 nodes - convert to TaskHandles
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048) let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
.map(|i| { .map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move { let task = dag.add_task(task_fn::<(), _, _>(
if i < 1024 { move |_: ()| {
1 if i < 1024 {
} else { 1
0 } else {
} 0
})); }
},
));
task.into() task.into()
}) })
.collect(); .collect();
@@ -145,20 +146,20 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
let task = match deps.len() { let task = match deps.len() {
1 => dag 1 => dag
.add_task(task_fn(|a: i32| async move { a })) .add_task(task_fn::<i32, _, _>(|&a: &i32| a))
.depends_on(deps[0]), .depends_on(deps[0]),
2 => dag 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])), .depends_on((deps[0], deps[1])),
3 => dag 3 => dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (i32, i32, i32)| async move { a + b + c }, |(a, b, c): (&i32, &i32, &i32)| a + b + c,
)) ))
.depends_on((deps[0], deps[1], deps[2])), .depends_on((deps[0], deps[1], deps[2])),
4 => dag 4 => dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move { .add_task(task_fn::<(i32, i32, i32, i32), _, _>(
a + b + c + d |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
})) ))
.depends_on((deps[0], deps[1], deps[2], deps[3])), .depends_on((deps[0], deps[1], deps[2], deps[3])),
_ => unreachable!(), _ => unreachable!(),
}; };
@@ -169,7 +170,7 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
// Final layer combines the last 2 // Final layer combines the last 2
let root = dag 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((&current_layer[0], &current_layer[1])); .depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| fut).await?; dag.run(|fut| fut).await?;
@@ -189,7 +190,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
// Create 1000 source tasks // Create 1000 source tasks
let sources: Vec<_> = (0..1000) let sources: Vec<_> = (0..1000)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { i % 10 })) dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10))
.into() .into()
}) })
.collect(); .collect();
@@ -201,9 +202,9 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
let idx2 = (i * 13) % 1000; let idx2 = (i * 13) % 1000;
let idx3 = (i * 17) % 1000; let idx3 = (i * 17) % 1000;
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move { dag.add_task(task_fn::<(i32, i32, i32), _, _>(
a + b + c + i as i32 move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32,
})) ))
.depends_on((&sources[idx1], &sources[idx2], &sources[idx3])) .depends_on((&sources[idx1], &sources[idx2], &sources[idx3]))
}) })
.collect(); .collect();
@@ -231,28 +232,24 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
let mut chain_ends = Vec::new(); let mut chain_ends = Vec::new();
for chain_id in 0..100 { for chain_id in 0..100 {
let first = dag.add_task(task_fn({ let first = dag.add_task(task_fn::<(), _, _>({
let counter = counter.clone(); let counter = counter.clone();
move |_: ()| { move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::Relaxed);
counter.fetch_add(1, Ordering::Relaxed); chain_id * 100
chain_id * 100
}
} }
})); }));
let mut current: dagx::TaskHandle<i32> = first.into(); let mut current: dagx::TaskHandle<i32> = first.into();
for _i in 1..100 { for _i in 1..100 {
current = dag current = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let counter = counter.clone(); let counter = counter.clone();
move |prev: i32| { move |&prev: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::Relaxed);
counter.fetch_add(1, Ordering::Relaxed); prev + 1
prev + 1
}
} }
})) }))
.depends_on(current); .depends_on(current);
@@ -291,12 +288,10 @@ async fn test_100000_nodes_stress() -> DagResult<()> {
.map(|i| { .map(|i| {
let completed = completed.clone(); let completed = completed.clone();
let task_id = batch * batch_size + i; let task_id = batch * batch_size + i;
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let completed = completed.clone(); let completed = completed.clone();
async move { completed.fetch_add(1, Ordering::Relaxed);
completed.fetch_add(1, Ordering::Relaxed); task_id
task_id
}
})) }))
}) })
.collect(); .collect();
+66 -97
View File
@@ -19,21 +19,19 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Task producing ZST // 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) // 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 // Task with PhantomData
let _t3 = dag.add_task(task_fn(|_: ()| async { let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper::<i32> {
PhantomWrapper::<i32> { _phantom: PhantomData,
_phantom: PhantomData,
}
})); }));
// Task that depends on t2 (uses primitive type) // Task that depends on t2 (uses primitive type)
let t4 = dag let t4 = dag
.add_task(task_fn(|val: i32| async move { .add_task(task_fn::<i32, _, _>(|&val: &i32| {
assert_eq!(val, 42); assert_eq!(val, 42);
"success" "success"
})) }))
@@ -59,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
let large = LargeStruct { let large = LargeStruct {
data: [42; 8192], data: [42; 8192],
more_data: vec![1; 1000], more_data: vec![1; 1000],
@@ -69,7 +67,7 @@ async fn test_large_value_types() -> DagResult<()> {
})); }));
let t2 = dag let t2 = dag
.add_task(task_fn(|val: usize| async move { .add_task(task_fn::<usize, _, _>(|&val: &usize| {
val // Just pass through to verify val // Just pass through to verify
})) }))
.depends_on(t1); .depends_on(t1);
@@ -86,12 +84,16 @@ async fn test_unit_type_chains() -> DagResult<()> {
// Test chains of unit type operations // Test chains of unit type operations
let dag = DagRunner::new(); let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async {})).into(); let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| {})).into();
let t2 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t1); let t2 = dag
let t3 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t2); .add_task(task_fn::<(), _, _>(|_: ()| {}))
.depends_on((t1,));
let t3 = dag
.add_task(task_fn::<(), _, _>(|_: ()| {}))
.depends_on((t2,));
let t4 = dag let t4 = dag
.add_task(task_fn(|_: ()| async { "done" })) .add_task(task_fn::<(), _, _>(|_: ()| "done"))
.depends_on(t3); .depends_on((t3,));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -103,82 +105,43 @@ async fn test_unit_type_chains() -> DagResult<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn test_nested_option_result_types() -> DagResult<()> {
// Test deeply nested generic types
type ComplexType = Result<Option<Result<Vec<Option<i32>>, 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::<i32>(),
_ => 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] #[tokio::test]
async fn test_string_types_variety() -> DagResult<()> { async fn test_string_types_variety() -> DagResult<()> {
// Test various string types // Test various string types
let dag = DagRunner::new(); let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { "static str" })); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| "static str"));
let t2 = dag.add_task(task_fn(|_: ()| async { String::from("owned string") })); let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| String::from("owned string")));
let t3 = dag.add_task(task_fn(|_: ()| async { let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
std::borrow::Cow::Borrowed("cow str") std::borrow::Cow::Borrowed("cow str")
})); }));
let t4 = dag.add_task(task_fn(|_: ()| async { Box::new("boxed str") })); let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| Box::new("boxed str")));
let t5 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str 2") })); let t5 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
let t6 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str") })); std::sync::Arc::new("arc str 2")
}));
let t6 = dag.add_task(task_fn::<(), _, _>(|_: ()| std::sync::Arc::new("arc str")));
let combined = dag let combined = dag
.add_task(task_fn( .add_task(task_fn::<
|(s1, s2, s3, s4, s5, s6): ( (
&str, &str,
String, String,
std::borrow::Cow<str>, std::borrow::Cow<str>,
Box<&str>, Box<&str>,
std::sync::Arc<&str>, std::sync::Arc<&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<str>,
&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)); .depends_on((t1, t2, t3, t4, t5, t6));
@@ -199,13 +162,17 @@ async fn test_generic_type_constraints() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { Wrapper(42) })); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(42)));
let t2 = dag.add_task(task_fn(|_: ()| async { Wrapper("hello") })); let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper("hello")));
let t3 = dag.add_task(task_fn(|_: ()| async { Wrapper(vec![1, 2, 3]) })); let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(vec![1, 2, 3])));
let combined = dag let combined = dag
.add_task(task_fn( .add_task(task_fn::<
|(w1, w2, w3): (Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>)| async move { (Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>),
_,
_,
>(
|(w1, w2, w3): (&Wrapper<i32>, &Wrapper<&str>, &Wrapper<Vec<i32>>)| {
format!("{:?} {:?} {:?}", w1, w2, w3) format!("{:?} {:?} {:?}", w1, w2, w3)
}, },
)) ))
@@ -226,7 +193,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
let dag = DagRunner::new(); 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)); let arc = Arc::new(Mutex::new(100));
// Extract value instead of passing Arc // Extract value instead of passing Arc
let guard = arc.lock().unwrap(); let guard = arc.lock().unwrap();
@@ -234,7 +201,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
})); }));
let t2 = dag let t2 = dag
.add_task(task_fn(|val: i32| async move { val * 2 })) .add_task(task_fn::<i32, _, _>(|&val: &i32| val * 2))
.depends_on(t1); .depends_on(t1);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Test with fixed-size arrays
let dag = DagRunner::new(); let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { [1, 2, 3, 4, 5] })); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| [1, 2, 3, 4, 5]));
let t2 = dag.add_task(task_fn(|_: ()| async { [6, 7, 8, 9, 10] })); let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| [6, 7, 8, 9, 10]));
let t3 = dag let t3 = dag
.add_task(task_fn(|(a1, a2): ([i32; 5], [i32; 5])| async move { .add_task(task_fn::<([i32; 5], [i32; 5]), _, _>(
let mut sum = 0; |(a1, a2): (&[i32; 5], &[i32; 5])| {
for i in 0..5 { let mut sum = 0;
sum += a1[i] + a2[i]; for i in 0..5 {
} sum += a1[i] + a2[i];
sum }
})) sum
},
))
.depends_on((t1, t2)); .depends_on((t1, t2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { MyEnum::Unit })); let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Unit));
let t2 = dag.add_task(task_fn(|_: ()| async { let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
MyEnum::Tuple(42, "test".to_string()) MyEnum::Tuple(42, "test".to_string())
})); }));
let t3 = dag.add_task(task_fn(|_: ()| async { MyEnum::Struct { x: 10, y: 20 } })); let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 }));
let t4 = dag.add_task(task_fn(|_: ()| async { MyEnum::LargeVariant([0; 1024]) })); let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024])));
let combined = dag let combined = dag
.add_task(task_fn( .add_task(task_fn::<(MyEnum, MyEnum, MyEnum, MyEnum), _, _>(
|(e1, e2, e3, e4): (MyEnum, MyEnum, MyEnum, MyEnum)| async move { |(e1, e2, e3, e4): (&MyEnum, &MyEnum, &MyEnum, &MyEnum)| {
matches!(e1, MyEnum::Unit) matches!(e1, MyEnum::Unit)
&& matches!(e2, MyEnum::Tuple(42, _)) && matches!(e2, MyEnum::Tuple(42, _))
&& matches!(e3, MyEnum::Struct { x: 10, y: 20 }) && matches!(e3, MyEnum::Struct { x: 10, y: 20 })
+19 -14
View File
@@ -5,6 +5,7 @@ use dagx::{task, DagResult, DagRunner};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::task::yield_now;
#[tokio::test] #[tokio::test]
async fn test_empty_dag_execution() -> DagResult<()> { 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 // Test the absolute minimum: one task with no dependencies
let dag = DagRunner::new(); 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?; 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 executed = Arc::new(AtomicUsize::new(0));
let exec_clone = executed.clone(); 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(); 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?; 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 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?; 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 dag = DagRunner::new();
let tasks: Vec<_> = (0..100) 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(); .collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
} else { } else {
// DAG with a task // 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?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
assert_eq!(dag.get(task)?, i); assert_eq!(dag.get(task)?, i);
} }
@@ -151,7 +150,7 @@ async fn test_dag_with_never_type_simulation() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// This task completes immediately but returns a Result containing "never" type // This task completes immediately but returns a Result containing "never" type
let task = dag.add_task(task_fn(|_: ()| async { Result::<i32, Never>::Ok(42) })); let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, Never>::Ok(42)));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Test a task that does the absolute minimum async work
let dag = DagRunner::new(); let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { struct YieldTask;
// Just yield once to the runtime
tokio::task::yield_now().await; #[task]
1337 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?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
+2 -2
View File
@@ -80,7 +80,7 @@ async fn test_custom_type_passthrough() {
let person_result = dag.get(process).unwrap(); let person_result = dag.get(process).unwrap();
let company_result = dag.get(transform).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.name, "Alice's Company");
assert_eq!(company_result.employees.len(), 1); 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_a = dag.get(path_a).unwrap();
let result_b = dag.get(path_b).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"); assert_eq!(result_b.name, "Alice's Company");
} }
+4 -4
View File
@@ -54,7 +54,7 @@ fn test_handle_is_immutable_reference() {
let builder = dag.add_task(NoInput); let builder = dag.add_task(NoInput);
// Convert to handle (only works because Input = ()) // Convert to handle (only works because Input = ())
let handle: TaskHandle<i32> = builder.into(); let handle: TaskHandle<_> = builder.into();
// TaskHandle is Copy/Clone - just an ID wrapper // TaskHandle is Copy/Clone - just an ID wrapper
let _copy1 = handle; let _copy1 = handle;
@@ -108,7 +108,7 @@ fn test_ordering_prevents_cycles() {
// So we can only create DAGs, never cycles! // So we can only create DAGs, never cycles!
// Let's create a valid DAG instead: A→B (B depends on A) // Let's create a valid DAG instead: A→B (B depends on A)
let a_handle: TaskHandle<i32> = 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); let _b_handle = b_builder.depends_on(a_handle);
// Now we could try: a_handle.depends_on(b_handle)? // Now we could try: a_handle.depends_on(b_handle)?
// But TaskHandle has no such method! // But TaskHandle has no such method!
@@ -138,8 +138,8 @@ fn test_unit_tasks_cannot_create_cycles() {
let task2 = dag.add_task(Task2); let task2 = dag.add_task(Task2);
// Both have unit input, so we can convert to handles // Both have unit input, so we can convert to handles
let _handle1: TaskHandle<i32> = task1.into(); let _handle1: TaskHandle<_> = task1.into();
let _handle2: TaskHandle<i32> = task2.into(); let _handle2: TaskHandle<_> = task2.into();
// Now we have two handles, but... // Now we have two handles, but...
// TaskHandle has no methods to add dependencies! // TaskHandle has no methods to add dependencies!
+45 -48
View File
@@ -1,7 +1,7 @@
//! Tests for dependency resolution order and transitive dependencies //! Tests for dependency resolution order and transitive dependencies
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle}; use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::{Arc, Mutex}; 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())); let execution_log = Arc::new(Mutex::new(Vec::new()));
struct ATask {
log: Arc<Mutex<Vec<(&'static str, i32)>>>,
}
#[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 // Create a chain: A -> B -> C -> D -> E
let a = { let a = dag.add_task(ATask {
let log = execution_log.clone(); 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 b = { let b = {
let log = execution_log.clone(); let log = execution_log.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let log = log.clone(); let log = log.clone();
async move { log.lock().unwrap().push(("B_start", x));
log.lock().unwrap().push(("B_start", x)); assert_eq!(x, 1, "B should receive 1 from A");
assert_eq!(x, 1, "B should receive 1 from A"); log.lock().unwrap().push(("B_end", x + 1));
log.lock().unwrap().push(("B_end", x + 1)); x + 1
x + 1
}
})) }))
.depends_on(a) .depends_on(a)
}; };
let c = { let c = {
let log = execution_log.clone(); let log = execution_log.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let log = log.clone(); let log = log.clone();
async move { log.lock().unwrap().push(("C_start", x));
log.lock().unwrap().push(("C_start", x)); assert_eq!(x, 2, "C should receive 2 from B");
assert_eq!(x, 2, "C should receive 2 from B"); log.lock().unwrap().push(("C_end", x + 1));
log.lock().unwrap().push(("C_end", x + 1)); x + 1
x + 1
}
})) }))
.depends_on(b) .depends_on(b)
}; };
let d = { let d = {
let log = execution_log.clone(); let log = execution_log.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let log = log.clone(); let log = log.clone();
async move { log.lock().unwrap().push(("D_start", x));
log.lock().unwrap().push(("D_start", x)); assert_eq!(x, 3, "D should receive 3 from C");
assert_eq!(x, 3, "D should receive 3 from C"); log.lock().unwrap().push(("D_end", x + 1));
log.lock().unwrap().push(("D_end", x + 1)); x + 1
x + 1
}
})) }))
.depends_on(c) .depends_on(c)
}; };
let e = { let e = {
let log = execution_log.clone(); let log = execution_log.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let log = log.clone(); let log = log.clone();
async move { log.lock().unwrap().push(("E_start", x));
log.lock().unwrap().push(("E_start", x)); assert_eq!(x, 4, "E should receive 4 from D");
assert_eq!(x, 4, "E should receive 4 from D"); log.lock().unwrap().push(("E_end", x + 1));
log.lock().unwrap().push(("E_end", x + 1)); x + 1
x + 1
}
})) }))
.depends_on(d) .depends_on(d)
}; };
@@ -108,12 +105,12 @@ async fn test_transitive_dependencies() -> DagResult<()> {
// A -> B -> C (creates transitive A -> C relationship) // A -> B -> C (creates transitive A -> C relationship)
let dag = DagRunner::new(); 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 let b = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(b); .depends_on(b);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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<()> { async fn test_deep_chain_50_levels() -> DagResult<()> {
let dag = DagRunner::new(); 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()]; let mut handles = vec![first.into()];
for _i in 1..=50 { for _i in 1..=50 {
let next = dag let next = dag
.add_task(task_fn(move |x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x + 1))
.depends_on(handles.last().unwrap()); .depends_on(handles.last().unwrap());
handles.push(next); handles.push(next);
} }
@@ -149,12 +146,12 @@ async fn test_deep_chain_50_levels() -> DagResult<()> {
async fn test_deep_chain_200_levels() -> DagResult<()> { async fn test_deep_chain_200_levels() -> DagResult<()> {
let dag = DagRunner::new(); 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(); let mut current = first.into();
for _ in 0..200 { for _ in 0..200 {
current = dag current = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(current); .depends_on(current);
} }
+19 -17
View File
@@ -8,9 +8,9 @@ use futures::FutureExt;
async fn test_single_dependency() -> DagResult<()> { async fn test_single_dependency() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let source = dag.add_task(task_fn(|_: ()| async { 42 })); let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let dependent = dag let dependent = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source); .depends_on(source);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 dag = DagRunner::new();
let source: TaskHandle<_> = dag 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(); .into();
let sum = dag let sum = dag
.add_task(task_fn(|v: Vec<i32>| async move { v.iter().sum::<i32>() })) .add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
v.iter().sum::<i32>()
}))
.depends_on(source); .depends_on(source);
let product = dag let product = dag
.add_task(task_fn( .add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
|v: Vec<i32>| async move { v.iter().product::<i32>() }, v.iter().product::<i32>()
)) }))
.depends_on(source); .depends_on(source);
let final_result = dag 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)); .depends_on((&sum, &product));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Test that dependencies with different types work correctly
let dag = DagRunner::new(); let dag = DagRunner::new();
let int_source = dag.add_task(task_fn(|_: ()| async { 42 })); let int_source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let string_source = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let string_source = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
let bool_source = dag.add_task(task_fn(|_: ()| async { true })); let bool_source = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
let combined = dag let combined = dag
.add_task(task_fn(|(i, s, b): (i32, String, bool)| async move { .add_task(task_fn::<(i32, String, bool), _, _>(
format!("{}: {} ({})", s, i, b) |(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b),
})) ))
.depends_on((int_source, string_source, bool_source)); .depends_on((int_source, string_source, bool_source));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Test that a single node can be a dependency for multiple downstream nodes
let dag = DagRunner::new(); 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 // Create 10 tasks that all depend on the shared node
let dependents: Vec<_> = (0..10) let dependents: Vec<_> = (0..10)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x * (i + 1) })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * (i + 1)))
.depends_on(shared) .depends_on(shared)
}) })
.collect(); .collect();
@@ -103,7 +105,7 @@ async fn test_multiple_source_nodes() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let sources: Vec<_> = (0..20) 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(); .collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
+27 -29
View File
@@ -8,10 +8,10 @@ use futures::FutureExt;
async fn test_two_dependencies() -> DagResult<()> { async fn test_two_dependencies() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 10 })); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let b = dag.add_task(task_fn(|_: ()| async { 20 })); let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag 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)); .depends_on((a, b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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<()> { async fn test_three_dependencies() -> DagResult<()> {
let dag = DagRunner::new(); 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(|_: ()| async { 2 })); let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let c = dag.add_task(task_fn(|_: ()| async { 3 })); let c = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
let sum = dag let sum = dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32), _, _>(
|(x, y, z): (i32, i32, i32)| async move { x + y + z }, |(x, y, z): (&i32, &i32, &i32)| x + y + z,
)) ))
.depends_on((a, b, c)); .depends_on((a, b, c));
@@ -44,13 +44,13 @@ async fn test_four_dependencies() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let deps: Vec<_> = (1..=4) 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(); .collect();
let sum = dag let sum = dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move { .add_task(task_fn::<(i32, i32, i32, i32), _, _>(
a + b + c + d |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
})) ))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3])); .depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 dag = DagRunner::new();
let deps: Vec<_> = (1..=5) 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(); .collect();
let sum = dag let sum = dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32, i32, i32), _, _>(
|(a, b, c, d, e): (i32, i32, i32, i32, i32)| async move { a + b + c + d + e }, |(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])); .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 dag = DagRunner::new();
let deps: Vec<_> = (1..=6) 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(); .collect();
let sum = dag.add_task(task_fn( let sum = dag
|(a, b, c, d, e, f): (i32, i32, i32, i32, i32, i32)| async move { .add_task(task_fn::<(i32, i32, i32, i32, i32, i32), _, _>(
a + b + c + d + e + f |(a, b, c, d, e, f): (&i32, &i32, &i32, &i32, &i32, &i32)| a + b + c + d + e + f,
} ))
)).depends_on(( .depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]));
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]
));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 dag = DagRunner::new();
let deps: Vec<_> = (1..=7) 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(); .collect();
let sum = dag let sum = dag
.add_task(task_fn( .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)| async move { |(a, b, c, d, e, f, g): (&i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
a + b + c + d + e + f + g a + b + c + d + e + f + g
}, },
)) ))
@@ -130,12 +128,12 @@ async fn test_eight_dependencies() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let deps: Vec<_> = (1..=8) 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(); .collect();
let sum = dag let sum = dag
.add_task(task_fn( .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)| async move { |(a, b, c, d, e, f, g, h): (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
a + b + c + d + e + f + g + h a + b + c + d + e + f + g + h
}, },
)) ))
+16 -16
View File
@@ -8,7 +8,7 @@ use futures::FutureExt;
async fn test_result_not_found_error() { async fn test_result_not_found_error() {
let dag = DagRunner::new(); 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 // Try to get result before running
let result = dag.get(t1); let result = dag.get(t1);
@@ -27,11 +27,11 @@ async fn test_nested_result_error_handling() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Task that returns a Result // Task that returns a Result
let task1 = dag.add_task(task_fn(|_: ()| async { let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, String>::Err("Internal error".to_string()) Result::<i32, String>::Err("Internal error".to_string())
})); }));
let task2 = dag.add_task(task_fn(|_: ()| async { Result::<i32, String>::Ok(42) })); let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, String>::Ok(42)));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -46,7 +46,7 @@ async fn test_nested_result_error_handling() {
async fn test_option_none_not_error() { async fn test_option_none_not_error() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { None::<i32> })); let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::<i32>));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
@@ -66,11 +66,11 @@ async fn test_custom_error_types() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task1 = dag.add_task(task_fn(|_: ()| async { let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string())) Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string()))
})); }));
let task2 = dag.add_task(task_fn(|_: ()| async { let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, CustomError>::Err(CustomError::ProcessingError { Result::<i32, CustomError>::Err(CustomError::ProcessingError {
code: 500, code: 500,
details: "Processing failed".to_string(), details: "Processing failed".to_string(),
@@ -82,13 +82,13 @@ async fn test_custom_error_types() {
.unwrap(); .unwrap();
// Check custom errors are preserved // 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"), Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
_ => panic!("Wrong error type"), _ => panic!("Wrong error type"),
} }
match dag.get(task2).unwrap() { match dag.get(task2).unwrap().as_ref() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500), Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500),
_ => panic!("Wrong error type"), _ => panic!("Wrong error type"),
} }
} }
@@ -97,13 +97,13 @@ async fn test_custom_error_types() {
async fn test_error_in_tuple_dependency() { async fn test_error_in_tuple_dependency() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let ok_task = dag.add_task(task_fn(|_: ()| async { 10 })); let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let err_task = dag.add_task(task_fn(|_: ()| async { let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error in dependency"); panic!("Error in dependency");
})); }));
let dependent = dag 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)); .depends_on((ok_task, err_task));
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; 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() { async fn test_string_panic_vs_structured_panic() {
let dag = DagRunner::new(); 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"); 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"); 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 // Test error chains with simple string-based errors
let dag = DagRunner::new(); 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 // Simulate an error chain with nested Results
let _inner: Result<i32, String> = Err("Inner error".to_string()); let _inner: Result<i32, String> = Err("Inner error".to_string());
let outer: Result<Result<i32, String>, String> = Err("Outer error".to_string()); let outer: Result<Result<i32, String>, String> = Err("Outer error".to_string());
@@ -152,7 +152,7 @@ async fn test_anyhow_like_error_chain() {
let result = dag.get(task).unwrap(); let result = dag.get(task).unwrap();
assert!(result.is_err()); assert!(result.is_err());
if let Err(e) = result { if let Err(e) = result.as_ref() {
assert_eq!(e, "Outer error"); assert_eq!(e, "Outer error");
} }
} }
+92 -86
View File
@@ -1,26 +1,28 @@
//! Tests for error propagation through the DAG //! Tests for error propagation through the DAG
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagRunner, TaskHandle}; use dagx::{task, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test] #[tokio::test]
async fn test_linear_error_propagation() { async fn test_linear_error_propagation() {
let dag = DagRunner::new(); 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 let t2 = dag
.add_task(task_fn(|x: i32| async move { .add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
panic!("Error at t2 with input {}", x); panic!("Error at t2 with input {}", x);
})) }))
.depends_on(t1); .depends_on(t1);
let t3 = dag let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t2); .depends_on(t2);
let t4 = dag let t4 = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(t3); .depends_on(t3);
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -46,20 +48,20 @@ async fn test_diamond_error_propagation() {
// \ / // \ /
// sink // 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 let left = dag
.add_task(task_fn(|x: i32| async move { x / 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
.depends_on(source); .depends_on(source);
let right = dag let right = dag
.add_task(task_fn(|x: i32| async move { .add_task(task_fn::<i32, _, _>(|&x: &i32| {
panic!("Right path fails with {}", x); panic!("Right path fails with {}", x);
})) }))
.depends_on(source); .depends_on(source);
let sink = dag 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)); .depends_on((&left, &right));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; 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 // Branch with error
let error_source: TaskHandle<_> = dag let error_source: TaskHandle<_> = dag
.add_task(task_fn({ .add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone(); let tracker = execution_tracker.clone();
move |_: ()| { move |_: ()| -> i32 {
let tracker = tracker.clone(); let tracker = tracker.clone();
async move { tracker.fetch_add(1, Ordering::SeqCst);
tracker.fetch_add(1, Ordering::SeqCst); panic!("Error source");
panic!("Error source");
}
} }
})) }))
.into(); .into();
let error_dependent = dag let error_dependent = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let tracker = execution_tracker.clone(); let tracker = execution_tracker.clone();
move |_: i32| { move |_: &i32| {
let tracker = tracker.clone(); let tracker = tracker.clone();
async move { tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute 42
42
}
} }
})) }))
.depends_on(error_source); .depends_on(error_source);
// Independent branch // Independent branch
let independent = dag.add_task(task_fn({ let independent = dag.add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone(); let tracker = execution_tracker.clone();
move |_: ()| { move |_: ()| {
let tracker = tracker.clone(); let tracker = tracker.clone();
async move { tracker.fetch_add(10, Ordering::SeqCst);
tracker.fetch_add(10, Ordering::SeqCst); 10
10
}
} }
})); }));
@@ -133,22 +129,24 @@ async fn test_multiple_error_sources_convergence() {
// Multiple error sources // Multiple error sources
let error1 = dag let error1 = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 1"); panic!("Error source 1");
})) }))
.into(); .into();
let error2 = dag let error2 = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 2"); panic!("Error source 2");
})) }))
.into(); .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 // Convergence point depends on all three
let convergence = dag 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)); .depends_on((&error1, &error2, &success));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; 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 dag = DagRunner::new();
let source: TaskHandle<_> = dag let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
panic!("Source fails"); panic!("Source fails");
})) }))
.into(); .into();
@@ -177,7 +175,7 @@ async fn test_error_in_wide_fanout() {
// Create wide fanout from failing source // Create wide fanout from failing source
let dependents: Vec<_> = (0..20) let dependents: Vec<_> = (0..20)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x + i })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source) .depends_on(source)
}) })
.collect(); .collect();
@@ -199,24 +197,26 @@ async fn test_selective_error_propagation() {
// Source that produces a Result // Source that produces a Result
let source: TaskHandle<_> = dag let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, String>::Err("Source error".to_string()) Result::<i32, String>::Err("Source error".to_string())
})) }))
.into(); .into();
// Handler that processes the Result // Handler that processes the Result
let handler = dag let handler = dag
.add_task(task_fn(|result: Result<i32, String>| async move { .add_task(task_fn::<Result<_, _>, _, _>(
match result { |result: &Result<i32, String>| {
Ok(val) => val * 2, match result {
Err(_) => -1, // Default value on error Ok(val) => val * 2,
} Err(_) => -1, // Default value on error
})) }
},
))
.depends_on(source); .depends_on(source);
// Further processing // Further processing
let final_task = dag let final_task = dag
.add_task(task_fn(|val: i32| async move { .add_task(task_fn::<i32, _, _>(|&val: &i32| {
if val < 0 { if val < 0 {
"Handled error case".to_string() "Handled error case".to_string()
} else { } else {
@@ -244,41 +244,49 @@ async fn test_error_propagation_timing() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new())); let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
let t1 = dag.add_task(task_fn({ struct T1Task {
let order = propagation_order.clone(); order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
move |_: ()| { }
let order = order.clone();
async move { #[task]
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; impl T1Task {
order.lock().push("t1_complete"); async fn run(&self) -> i32 {
1 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<parking_lot::Mutex<Vec<&'static str>>>,
}
#[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 let t2 = dag
.add_task(task_fn({ .add_task(T2Task {
let order = propagation_order.clone(); 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);
}
}
}))
.depends_on(t1); .depends_on(t1);
let _t3 = dag let _t3 = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let order = propagation_order.clone(); let order = propagation_order.clone();
move |x: i32| { move |x: &i32| {
let order = order.clone(); let order = order.clone();
async move { order.lock().push("t3_should_not_run");
order.lock().push("t3_should_not_run"); x + 1
x + 1
}
} }
})) }))
.depends_on(t2); .depends_on(t2);
@@ -307,53 +315,51 @@ async fn test_partial_branch_failure_propagation() {
// \ | / // \ | /
// sink // sink
let root: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into(); let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
// Branch A // Branch A
let a1 = dag let a1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(root); .depends_on(root);
let a2 = dag let a2 = dag
.add_task(task_fn(|x: i32| async move { x + 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(a1); .depends_on(a1);
let a3 = dag let a3 = dag
.add_task(task_fn(|x: i32| async move { x + 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
.depends_on(a2); .depends_on(a2);
// Branch B (with error) - use Result instead of panic // Branch B (with error) - use Result instead of panic
let b1 = dag let b1 = dag
.add_task(task_fn(|x: i32| async move { Ok(x * 2) })) .add_task(task_fn::<i32, _, _>(|&x: &i32| Ok::<i32, &str>(x * 2)))
.depends_on(root); .depends_on(root);
let b2 = dag let b2 = dag
.add_task(task_fn(|_x: Result<i32, &str>| async move { .add_task(task_fn::<Result<_, _>, _, _>(|_x: &Result<i32, &str>| {
Err("Branch B fails at b2") Err::<i32, &str>("Branch B fails at b2")
})) }))
.depends_on(b1); .depends_on(b1);
let b3 = dag let b3 = dag
.add_task(task_fn( .add_task(task_fn::<Result<_, _>, _, _>(|x: &Result<i32, &str>| {
|x: Result<i32, &str>| async move { x.map(|v| v * 3) }, x.map(|v| v * 3)
)) }))
.depends_on(b2); .depends_on(b2);
// Branch C // Branch C
let c1 = dag let c1 = dag
.add_task(task_fn(|x: i32| async move { x - 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
.depends_on(root); .depends_on(root);
let c2 = dag let c2 = dag
.add_task(task_fn(|x: i32| async move { x - 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
.depends_on(c1); .depends_on(c1);
let c3 = dag let c3 = dag
.add_task(task_fn(|x: i32| async move { x - 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 3))
.depends_on(c2); .depends_on(c2);
// Sink depends on all branches - handle Result from B // Sink depends on all branches - handle Result from B
let sink = dag let sink = dag
.add_task(task_fn( .add_task(task_fn::<(i32, Result<_, _>, _), _, _>(
|(a, b, c): (i32, Result<i32, &str>, i32)| async move { |(a, b, c): (&i32, &Result<i32, &str>, &i32)| match b {
match b { Ok(b_val) => Ok(a + b_val + c),
Ok(b_val) => Ok(a + b_val + c), Err(e) => Err(*e),
Err(e) => Err(e),
}
}, },
)) ))
.depends_on((&a3, &b3, &c3)); .depends_on((&a3, &b3, &c3));
+46 -54
View File
@@ -11,17 +11,17 @@ async fn test_fallback_on_error() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Primary task that fails // Primary task that fails
let primary = dag.add_task(task_fn(|_: ()| async { let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Primary task failed"); panic!("Primary task failed");
#[allow(unreachable_code)] #[allow(unreachable_code)]
42 42
})); }));
// Fallback task // 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 // Selector that uses fallback if primary fails
let _selector = dag.add_task(task_fn(|_: ()| async {})); let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {}));
// Run DAG // Run DAG
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; 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)); let attempt_counter = Arc::new(AtomicUsize::new(0));
// Task that would fail on first attempts (simulated) // 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(); let counter = attempt_counter.clone();
move |_: ()| { move |_: ()| {
let counter = counter.clone(); 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 // In real retry, this would be retried
// Here we simulate by not panicking // Here we simulate by not panicking
if attempt == 0 { if attempt == 0 {
// Would normally panic here // Would normally panic here
return Err::<i32, &str>("Simulated failure"); return Err::<i32, &str>("Simulated failure");
}
Ok(42)
} }
Ok(42)
} }
})); }));
@@ -79,26 +77,24 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
.map(|i| { .map(|i| {
let failures = failure_count.clone(); let failures = failure_count.clone();
let circuit = circuit_open.clone(); let circuit = circuit_open.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let failures = failures.clone(); let failures = failures.clone();
let circuit = circuit.clone(); let circuit = circuit.clone();
async move { // Check if circuit is open
// Check if circuit is open if circuit.load(Ordering::SeqCst) {
if circuit.load(Ordering::SeqCst) { return Err("Circuit breaker open");
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)
} }
// 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(); .collect();
@@ -120,7 +116,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
// Some tasks that might fail // Some tasks that might fail
let _tasks: Vec<_> = (0..10) let _tasks: Vec<_> = (0..10)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
if i % 3 == 0 { if i % 3 == 0 {
panic!("Task {} failed", i); panic!("Task {} failed", i);
} }
@@ -130,7 +126,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
.collect(); .collect();
// Aggregator that handles failures gracefully // 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 // 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 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) let mut tasks: Vec<_> = (0..10)
.map(|i| { .map(|i| {
let errors = errors.clone(); let errors = errors.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let errors = errors.clone(); let errors = errors.clone();
async move { if i % 2 == 0 {
if i % 2 == 0 { errors.lock().push(format!("Error from task {}", i));
errors.lock().push(format!("Error from task {}", i)); return Err(format!("Task {} error", i));
return Err(format!("Task {} error", i));
}
Ok(i)
} }
Ok(i)
})) }))
}) })
.collect(); .collect();
@@ -187,16 +181,16 @@ async fn test_graceful_degradation() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Critical task // 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 // Optional enhancement that fails - returns Result
let _enhancement = dag.add_task(task_fn(|_: ()| async { let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Err::<Vec<i32>, &str>("Enhancement failed") Err::<Vec<i32>, &str>("Enhancement failed")
})); }));
// Final task that works with what's available // Final task that works with what's available
let final_task = dag let final_task = dag
.add_task(task_fn(|base: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|base: &Vec<i32>| {
// Would combine with enhancement if available // Would combine with enhancement if available
// Here just uses base // Here just uses base
base.iter().sum::<i32>() base.iter().sum::<i32>()
@@ -216,22 +210,22 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Group A: contains an error - use Result // 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 let a2 = dag
.add_task(task_fn(|_x: i32| async move { .add_task(task_fn::<i32, _, _>(|_x: &i32| {
Err::<i32, &str>("Group A error") Err::<i32, &str>("Group A error")
})) }))
.depends_on(a1); .depends_on(a1);
// Group B: should be isolated from Group A's error // 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 let b2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(b1); .depends_on(b1);
// Group C: depends on B but not A // Group C: depends on B but not A
let c1 = dag let c1 = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(b2); .depends_on(b2);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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)); let compensation_triggered = Arc::new(AtomicBool::new(false));
// Action that fails // Action that fails
let action = dag.add_task(task_fn(|_: ()| async { let action = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Perform some action that fails // Perform some action that fails
panic!("Action failed"); panic!("Action failed");
})); }));
// Compensation task (runs independently) // Compensation task (runs independently)
let compensation = dag.add_task(task_fn({ let compensation = dag.add_task(task_fn::<(), _, _>({
let triggered = compensation_triggered.clone(); let triggered = compensation_triggered.clone();
move |_: ()| { move |_: ()| {
let triggered = triggered.clone(); let triggered = triggered.clone();
async move { triggered.store(true, Ordering::SeqCst);
triggered.store(true, Ordering::SeqCst); "Compensation executed"
"Compensation executed"
}
} }
})); }));
+49 -57
View File
@@ -15,7 +15,7 @@ async fn test_empty_dag() {
#[tokio::test] #[tokio::test]
async fn test_single_task() { async fn test_single_task() {
let dag = DagRunner::new(); 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)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await .await
.unwrap(); .unwrap();
@@ -26,18 +26,18 @@ async fn test_single_task() {
async fn test_deep_chain() { async fn test_deep_chain() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 })); let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b = dag let b = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(b); .depends_on(b);
let d = dag let d = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(c); .depends_on(c);
let e = dag let e = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(d); .depends_on(d);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -54,7 +54,7 @@ async fn test_wide_parallel() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let tasks: Vec<_> = (0..10) 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(); .collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -70,15 +70,15 @@ async fn test_wide_parallel() {
async fn test_diamond_dependency() { async fn test_diamond_dependency() {
let dag = DagRunner::new(); 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 let b = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(a); .depends_on(a);
let d = dag 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)); .depends_on((&b, &c));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -100,13 +100,11 @@ struct User {
async fn test_different_output_types() { async fn test_different_output_types() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() })); let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] })); let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
let struct_task = dag.add_task(task_fn(|_: ()| async { let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User {
User { name: "Alice".to_string(),
name: "Alice".to_string(), age: 30,
age: 30,
}
})); }));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -124,22 +122,22 @@ async fn test_different_output_types() {
async fn test_multiple_sinks() { async fn test_multiple_sinks() {
let dag = DagRunner::new(); 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 // Branch 1
let branch1 = dag let branch1 = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source); .depends_on(source);
let sink1 = dag let sink1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(branch1); .depends_on(branch1);
// Branch 2 // Branch 2
let branch2 = dag let branch2 = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(source); .depends_on(source);
let sink2 = dag let sink2 = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(branch2); .depends_on(branch2);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -156,12 +154,12 @@ async fn test_single_task_inline_path() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Single task in its own layer // 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 let t2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(t1); .depends_on(t1);
let t3 = dag let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(t2); .depends_on(t2);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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) // Test multi-task layer spawn path (line 411-456 in runner.rs)
let dag = DagRunner::new(); 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) // Create multiple tasks in the same layer (all depend on source)
let t1 = dag let t1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source); .depends_on(source);
let t2 = dag let t2 = dag
.add_task(task_fn(|x: i32| async move { x + 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(source); .depends_on(source);
let t3 = dag let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
.depends_on(source); .depends_on(source);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -205,14 +203,14 @@ async fn test_producer_consumer_channels() {
// Producer // Producer
let producer: TaskHandle<_> = dag 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(); .into();
// Consumer // Consumer
let consumer = dag let consumer = dag
.add_task(task_fn( .add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|data: Vec<i32>| async move { data.iter().sum::<i32>() }, data.iter().sum::<i32>()
)) }))
.depends_on(producer); .depends_on(producer);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 // Test fanout with multiple consumers from one producer
let dag = DagRunner::new(); 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 // Multiple consumers
let c1 = dag let c1 = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(producer); .depends_on(producer);
let c2 = dag let c2 = dag
.add_task(task_fn(|x: i32| async move { x + 10 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
.depends_on(producer); .depends_on(producer);
let c3 = dag let c3 = dag
.add_task(task_fn(|x: i32| async move { x - 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
.depends_on(producer); .depends_on(producer);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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(); let dag = DagRunner::new();
// Create diamond pattern to ensure dependents are tracked correctly // 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 let left = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source); .depends_on(source);
let right = dag let right = dag
.add_task(task_fn(|x: i32| async move { x + 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(source); .depends_on(source);
let sink = dag 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)); .depends_on((&left, &right));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -286,33 +284,27 @@ async fn test_layer_execution_order() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let order1 = execution_order.clone(); 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(); let order = order1.clone();
async move { order.lock().push(1);
order.lock().push(1); 1
1
}
})); }));
let order2 = execution_order.clone(); let order2 = execution_order.clone();
let t2 = dag let t2 = dag
.add_task(task_fn(move |x: i32| { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order2.clone(); let order = order2.clone();
async move { order.lock().push(2);
order.lock().push(2); x + 1
x + 1
}
})) }))
.depends_on(t1); .depends_on(t1);
let order3 = execution_order.clone(); let order3 = execution_order.clone();
let t3 = dag let t3 = dag
.add_task(task_fn(move |x: i32| { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order3.clone(); let order = order3.clone();
async move { order.lock().push(3);
order.lock().push(3); x + 1
x + 1
}
})) }))
.depends_on(t2); .depends_on(t2);
-82
View File
@@ -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");
}
+6 -6
View File
@@ -170,7 +170,7 @@ async fn integration_test_wide_and_deep() -> DagResult<()> {
for source in &sources { for source in &sources {
let step1 = dag.add_task(Multiply::new(2)).depends_on(source); let step1 = dag.add_task(Multiply::new(2)).depends_on(source);
let step2 = dag let step2 = dag
.add_task(task_fn(|x: i32| async move { x + 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(step1); .depends_on(step1);
let step3 = dag.add_task(Multiply::new(3)).depends_on(step2); let step3 = dag.add_task(Multiply::new(3)).depends_on(step2);
final_nodes.push(step3); 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 // Create 5 independent tasks that each sleep for 100ms
let tasks: Vec<_> = (0..num_parallel_tasks) let tasks: Vec<_> = (0..num_parallel_tasks)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
sleep(sleep_duration).await; sleep(sleep_duration).await;
i i
})) }))
@@ -314,7 +314,7 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
// Layer 1: 4 parallel tasks (should take ~50ms) // Layer 1: 4 parallel tasks (should take ~50ms)
let parallel_tasks: Vec<_> = (0..4) let parallel_tasks: Vec<_> = (0..4)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
sleep(sleep_duration).await; sleep(sleep_duration).await;
i * 10 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 // Layer 2: Sequential chain of 3 tasks depending on the first parallel task
// (should add ~150ms) // (should add ~150ms)
let seq1 = dag let seq1 = dag
.add_task(task_fn(move |x: i32| async move { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await; sleep(sleep_duration).await;
x + 1 x + 1
})) }))
.depends_on(parallel_tasks[0]); .depends_on(parallel_tasks[0]);
let seq2 = dag let seq2 = dag
.add_task(task_fn(move |x: i32| async move { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await; sleep(sleep_duration).await;
x + 1 x + 1
})) }))
.depends_on(seq1); .depends_on(seq1);
let seq3 = dag let seq3 = dag
.add_task(task_fn(move |x: i32| async move { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await; sleep(sleep_duration).await;
x + 1 x + 1
})) }))
-1
View File
@@ -1,5 +1,4 @@
// Test module for execution tests // Test module for execution tests
mod basic; mod basic;
mod examples;
mod results; mod results;
+9 -9
View File
@@ -9,8 +9,8 @@ use futures::FutureExt;
async fn test_result_type() { async fn test_result_type() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let success_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) })); let success_task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
let error_task = dag.add_task(task_fn(|_: ()| async { let error_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Err::<i32, String>("error".to_string()) Err::<i32, String>("error".to_string())
})); }));
@@ -25,7 +25,7 @@ async fn test_result_type() {
#[tokio::test] #[tokio::test]
async fn test_dag_result_type_works() { async fn test_dag_result_type_works() {
let dag = DagRunner::new(); 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 // Test that DagResult<()> works with ? operator pattern
async fn run_dag(dag: &DagRunner) -> DagResult<()> { async fn run_dag(dag: &DagRunner) -> DagResult<()> {
@@ -41,7 +41,7 @@ async fn test_dag_result_type_works() {
#[tokio::test] #[tokio::test]
async fn test_task_not_executed_error() { async fn test_task_not_executed_error() {
let dag = DagRunner::new(); 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 // Try to get result before running - should error
let result = dag.get(task); let result = dag.get(task);
@@ -58,7 +58,7 @@ async fn test_task_not_executed_error() {
async fn test_error_messages_are_helpful() { async fn test_error_messages_are_helpful() {
// Test ResultNotFound error message // Test ResultNotFound error message
let dag = DagRunner::new(); 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); let result = dag.get(task);
assert!(result.is_err()); assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err()); let err_msg = format!("{}", result.unwrap_err());
@@ -70,7 +70,7 @@ async fn test_error_messages_are_helpful() {
#[tokio::test] #[tokio::test]
async fn test_successful_execution_returns_ok() { async fn test_successful_execution_returns_ok() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 })); let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
// Should return Ok(()) // Should return Ok(())
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await; 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() { async fn test_stateful_tasks() {
let dag = DagRunner::new(); 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); let counter = dag.add_task(Counter::new(10)).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 dag = DagRunner::new();
let multiplier = 7; let multiplier = 7;
let base = dag.add_task(task_fn(|_: ()| async { 5 })); let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let scaled = dag let scaled = dag
.add_task(task_fn(move |x: i32| async move { x * multiplier })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
.depends_on(base); .depends_on(base);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+34 -29
View File
@@ -1,27 +1,30 @@
//! Concurrent access tests //! Concurrent access tests
use crate::common::task_fn; use crate::common::task_fn;
use dagx::DagRunner; use dagx::{task, DagRunner};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::task::JoinSet; 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] #[tokio::test]
async fn test_concurrent_run_calls() { async fn test_concurrent_run_calls() {
// Test that multiple run() calls are handled correctly // Test that multiple run() calls are handled correctly
let dag = Arc::new(DagRunner::new()); let dag = Arc::new(DagRunner::new());
// Add some tasks // Add some tasks
let tasks: Vec<_> = (0..10) let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect();
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(Duration::from_millis(5)).await;
i
}))
})
.collect();
// Try to run the DAG multiple times concurrently // Try to run the DAG multiple times concurrently
let mut handles = JoinSet::new(); let mut handles = JoinSet::new();
@@ -59,7 +62,7 @@ async fn test_concurrent_execution_and_building() {
// Start with some initial tasks // Start with some initial tasks
let initial: Vec<_> = (0..10) 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(); .collect();
// Execute the initial DAG // Execute the initial DAG
@@ -81,7 +84,7 @@ async fn test_concurrent_execution_and_building() {
for i in 10..20 { for i in 10..20 {
let dag = Arc::clone(&dag); let dag = Arc::clone(&dag);
handles.spawn(async move { handles.spawn(async move {
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
i i
}); });
} }
@@ -112,14 +115,14 @@ async fn test_dag_builder_thread_safety() {
handles.spawn(async move { handles.spawn(async move {
// Each thread creates a small pipeline // 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 let transform1 = dag
.add_task(task_fn(move |x: i32| async move { x * 10 })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 10))
.depends_on(source); .depends_on(source);
let transform2 = dag let transform2 = dag
.add_task(task_fn(move |x: i32| async move { x + thread_id })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x + thread_id))
.depends_on(transform1); .depends_on(transform1);
counter.fetch_add(1, Ordering::SeqCst); counter.fetch_add(1, Ordering::SeqCst);
@@ -155,7 +158,7 @@ async fn test_concurrent_get_operations() {
// Create and execute tasks // Create and execute tasks
let tasks: Vec<_> = (0..100) 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(); .collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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() { async fn test_no_race_in_dependency_tracking() {
// Test that concurrent dependency modifications don't cause races // Test that concurrent dependency modifications don't cause races
let dag = Arc::new(DagRunner::new()); 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(); let mut handles = JoinSet::new();
@@ -199,7 +202,7 @@ async fn test_no_race_in_dependency_tracking() {
let source_clone = source_handle; let source_clone = source_handle;
handles.spawn(async move { handles.spawn(async move {
dag.add_task(task_fn(move |x: i32| async move { x + i })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source_clone) .depends_on(source_clone)
}); });
} }
@@ -239,20 +242,22 @@ async fn test_high_concurrency_stress() {
match i % 3 { match i % 3 {
0 => { 0 => {
// Add a source task // Add a source task
dag.add_task(task_fn(move |_: ()| async move { i })); dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
} }
1 => { 1 => {
// Add a task with a simple dependency // Add a task with a simple dependency
let source = dag.add_task(task_fn(move |_: ()| async move { i })); let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
dag.add_task(task_fn(move |x: i32| async move { x * 2 })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 2))
.depends_on(source); .depends_on(source);
} }
2 => { 2 => {
// Add a more complex subgraph // Add a more complex subgraph
let a = dag.add_task(task_fn(move |_: ()| async move { i })); let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
let b = dag.add_task(task_fn(move |_: ()| async move { i + 1 })); let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1));
dag.add_task(task_fn(move |(x, y): (i32, i32)| async move { x + y })) dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
.depends_on((a, b)); x + y
}))
.depends_on((a, b));
} }
_ => unreachable!(), _ => unreachable!(),
} }
@@ -289,7 +294,7 @@ async fn test_lock_contention_measurement() {
handles.spawn(async move { handles.spawn(async move {
for j in 0..10 { 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); ops.fetch_add(1, Ordering::SeqCst);
// Small yield to increase contention likelihood // Small yield to increase contention likelihood
@@ -326,7 +331,7 @@ async fn test_atomic_dag_operations() {
let dag = Arc::new(DagRunner::new()); let dag = Arc::new(DagRunner::new());
// Create a base task // 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 // Try to create dependent tasks concurrently
let mut handles = JoinSet::new(); let mut handles = JoinSet::new();
@@ -340,7 +345,7 @@ async fn test_atomic_dag_operations() {
handles.spawn(async move { handles.spawn(async move {
let dep = dag let dep = dag
.add_task(task_fn(move |x: i32| async move { x + i })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(base_clone); .depends_on(base_clone);
success.fetch_add(1, Ordering::SeqCst); success.fetch_add(1, Ordering::SeqCst);
@@ -381,12 +386,12 @@ async fn test_no_deadlock_on_circular_waiting() {
handles.spawn(async move { handles.spawn(async move {
// Each thread creates a chain of tasks // 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()]; let mut chain = vec![first.into()];
for j in 0..5 { for j in 0..5 {
let next = dag let next = dag
.add_task(task_fn(move |x: i32| async move { x + j })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j))
.depends_on(chain.last().unwrap()); .depends_on(chain.last().unwrap());
chain.push(next); chain.push(next);
+146 -179
View File
@@ -1,7 +1,7 @@
//! Tests for execution correctness and parallelism //! Tests for execution correctness and parallelism
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle}; use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; 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())); let layer_timing = Arc::new(Mutex::new(Vec::new()));
struct Layer0Task {
timing: Arc<Mutex<Vec<(String, Instant)>>>,
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 // Layer 0: 3 independent sources
let sources: Vec<_> = (0..3) let sources: Vec<_> = (0..3)
.map(|i| { .map(|i| {
let timing = layer_timing.clone(); dag.add_task(Layer0Task {
dag.add_task(task_fn(move |_: ()| { timing: layer_timing.clone(),
let timing = timing.clone(); idx: i,
async move { })
let start = Instant::now();
timing.lock().unwrap().push((format!("L0_{}", i), start));
sleep(Duration::from_millis(20)).await;
i * 10
}
}))
}) })
.collect(); .collect();
struct Layer1Task {
timing: Arc<Mutex<Vec<(String, Instant)>>>,
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 // Layer 1: Tasks depending on layer 0
let layer1: Vec<_> = sources let layer1: Vec<_> = sources
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, source)| { .map(|(i, source)| {
let timing = layer_timing.clone(); dag.add_task(Layer1Task {
dag.add_task(task_fn(move |x: i32| { idx: i,
let timing = timing.clone(); timing: layer_timing.clone(),
async move { })
let start = Instant::now();
timing.lock().unwrap().push((format!("L1_{}", i), start));
sleep(Duration::from_millis(20)).await;
x + 1
}
}))
.depends_on(source) .depends_on(source)
}) })
.collect(); .collect();
@@ -52,14 +76,14 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
// Layer 2: Final aggregation // Layer 2: Final aggregation
let final_task = { let final_task = {
let timing = layer_timing.clone(); let timing = layer_timing.clone();
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| { dag.add_task(task_fn::<(i32, i32, i32), _, _>(
let timing = timing.clone(); move |(a, b, c): (&i32, &i32, &i32)| {
async move { let timing = timing.clone();
let start = Instant::now(); let start = Instant::now();
timing.lock().unwrap().push(("L2_final".to_string(), start)); timing.lock().unwrap().push(("L2_final".to_string(), start));
a + b + c a + b + c
} },
})) ))
.depends_on((&layer1[0], &layer1[1], &layer1[2])) .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())); let completion_order = Arc::new(Mutex::new(Vec::new()));
// Create a complex DAG with clear ordering requirements struct ATask {
let a = { order: Arc<Mutex<Vec<&'static str>>>,
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
}
}))
};
let b = { #[task]
let order = completion_order.clone(); impl ATask {
dag.add_task(task_fn(move |_: ()| { async fn run(&self) -> i32 {
let order = order.clone(); sleep(Duration::from_millis(10)).await;
async move { self.order.lock().unwrap().push("A");
sleep(Duration::from_millis(5)).await; 1
order.lock().unwrap().push("B"); }
2 }
}
})) // Create a complex DAG with clear ordering requirements
}; let a = dag.add_task(ATask {
order: completion_order.clone(),
});
struct BTask {
order: Arc<Mutex<Vec<&'static str>>>,
}
#[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 c = {
let order = completion_order.clone(); 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(); let order = order.clone();
async move { order.lock().unwrap().push("C");
order.lock().unwrap().push("C"); x + y
x + y
}
})) }))
.depends_on((a, b)) .depends_on((a, b))
}; };
let d = { let d = {
let order = completion_order.clone(); let order = completion_order.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order.clone(); let order = order.clone();
async move { order.lock().unwrap().push("D");
order.lock().unwrap().push("D"); x * 2
x * 2
}
})) }))
.depends_on(c) .depends_on(c)
}; };
@@ -215,54 +245,70 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
let execution_order = Arc::new(Mutex::new(Vec::new())); let execution_order = Arc::new(Mutex::new(Vec::new()));
// Create a chain: A -> B -> C -> D struct ATask {
let a: TaskHandle<_> = { order: Arc<Mutex<Vec<&'static str>>>,
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
}
}))
} }
.into();
let b = { #[task]
let order = execution_order.clone(); impl ATask {
dag.add_task(task_fn(move |x: i32| { async fn run(&self) -> i32 {
let order = order.clone(); sleep(Duration::from_millis(20)).await;
async move { self.order.lock().unwrap().push("A");
order.lock().unwrap().push("B"); 1
sleep(Duration::from_millis(10)).await; }
x + 1 }
}
}))
.depends_on(a)
};
let c = { // Create a chain: A -> B -> C -> D
let order = execution_order.clone(); let a: TaskHandle<_> = dag
dag.add_task(task_fn(move |x: i32| { .add_task(ATask {
let order = order.clone(); order: execution_order.clone(),
async move { })
order.lock().unwrap().push("C"); .into();
sleep(Duration::from_millis(10)).await;
x + 1 struct BTask {
} order: Arc<Mutex<Vec<&'static str>>>,
})) }
.depends_on(b)
}; #[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<Mutex<Vec<&'static str>>>,
}
#[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 d = {
let order = execution_order.clone(); let order = execution_order.clone();
dag.add_task(task_fn(move |x: i32| { dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order.clone(); let order = order.clone();
async move { order.lock().unwrap().push("D");
order.lock().unwrap().push("D"); x + 1
x + 1
}
})) }))
.depends_on(c) .depends_on(c)
}; };
@@ -282,82 +328,3 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
Ok(()) 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(())
}
-1
View File
@@ -2,7 +2,6 @@
pub mod concurrent; pub mod concurrent;
pub mod execution; pub mod execution;
pub mod proof;
pub mod spawning; pub mod spawning;
pub mod threads; pub mod threads;
pub mod timing; pub mod timing;
-63
View File
@@ -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(())
}
+3 -62
View File
@@ -5,9 +5,6 @@ use dagx::{DagResult, DagRunner};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test] #[tokio::test]
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> { async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
@@ -18,14 +15,12 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
// Create 10 independent tasks // Create 10 independent tasks
let tasks: Vec<_> = (0..10) let tasks: Vec<_> = (0..10)
.map(|i| { .map(|i| {
dag.add_task(task_fn({ dag.add_task(task_fn::<(), _, _>({
let value = spawn_count.clone(); let value = spawn_count.clone();
move |_: ()| { move |_: ()| {
let value = value.clone(); let value = value.clone();
async move { value.fetch_add(1, Ordering::SeqCst);
value.fetch_add(1, Ordering::SeqCst); i
i
}
} }
})) }))
}) })
@@ -49,57 +44,3 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
Ok(()) 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(())
}
+158 -310
View File
@@ -1,10 +1,12 @@
//! Tests for multi-threading and concurrent execution //! Tests for multi-threading and concurrent execution
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle}; use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread::ThreadId;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
@@ -17,27 +19,36 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new())); let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
struct LayerTask {
ids: Arc<Mutex<HashSet<ThreadId>>>,
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 // Create 50 tasks with actual work to encourage multi-threading
let tasks: Vec<_> = (0..50) let tasks: Vec<_> = (0..50)
.map(|i| { .map(|i| {
let ids = thread_ids.clone(); dag.add_task(LayerTask {
dag.add_task(task_fn(move |_: ()| { ids: thread_ids.clone(),
let ids = ids.clone(); idx: i,
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
}
}))
}) })
.collect(); .collect();
@@ -71,42 +82,50 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
let concurrent_count = Arc::new(AtomicUsize::new(0)); let concurrent_count = Arc::new(AtomicUsize::new(0));
let max_concurrent = Arc::new(AtomicUsize::new(0)); let max_concurrent = Arc::new(AtomicUsize::new(0));
struct LayerTask {
count: Arc<AtomicUsize>,
max: Arc<AtomicUsize>,
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 // Create 20 tasks that track concurrent execution
let tasks: Vec<_> = (0..20) let tasks: Vec<_> = (0..20)
.map(|i| { .map(|i| {
let count = concurrent_count.clone(); dag.add_task(LayerTask {
let max = max_concurrent.clone(); count: concurrent_count.clone(),
max: max_concurrent.clone(),
dag.add_task(task_fn(move |_: ()| { idx: i,
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
}
}))
}) })
.collect(); .collect();
@@ -141,44 +160,56 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
let concurrent_middle = Arc::new(AtomicUsize::new(0)); 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<AtomicUsize>,
}
#[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 // These should run in parallel
let parallel1 = { let parallel1 = dag
let counter = concurrent_middle.clone(); .add_task(ParallelTask1 {
dag.add_task(task_fn(move |x: i32| { counter: concurrent_middle.clone(),
let counter = counter.clone(); })
async move { .depends_on(source);
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 parallel2 = { struct ParallelTask2 {
let counter = concurrent_middle.clone(); counter: Arc<AtomicUsize>,
dag.add_task(task_fn(move |x: i32| { }
let counter = counter.clone();
async move { #[task]
counter.fetch_add(1, Ordering::SeqCst); impl ParallelTask2 {
sleep(Duration::from_millis(20)).await; async fn run(&self, x: &i32) -> i32 {
let count = counter.load(Ordering::SeqCst); self.counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_sub(1, Ordering::SeqCst); sleep(Duration::from_millis(20)).await;
// Both should be running let count = self.counter.load(Ordering::SeqCst);
assert!(count > 0, "Should see concurrent execution"); self.counter.fetch_sub(1, Ordering::SeqCst);
x * 3 // Both should be running
} assert!(count > 0, "Should see concurrent execution");
})) x * 3
.depends_on(source) }
}; }
let parallel2 = dag
.add_task(ParallelTask2 {
counter: concurrent_middle.clone(),
})
.depends_on(source);
let sink = dag 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((&parallel1, &parallel2)); .depends_on((&parallel1, &parallel2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -188,197 +219,6 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
Ok(()) 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] #[tokio::test]
async fn test_massive_parallel_fanout() -> DagResult<()> { async fn test_massive_parallel_fanout() -> DagResult<()> {
// Test extreme parallelism with 1000 parallel tasks // 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 max_concurrent = Arc::new(AtomicUsize::new(0));
let current = 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<AtomicUsize>,
max: Arc<AtomicUsize>,
curr: Arc<AtomicUsize>,
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 // Create 1000 tasks all depending on the same source
let tasks: Vec<_> = (0..1000) let tasks: Vec<_> = (0..1000)
.map(|i| { .map(|i| {
let comp = completed.clone(); dag.add_task(FanoutTask {
let max = max_concurrent.clone(); comp: completed.clone(),
let curr = current.clone(); max: max_concurrent.clone(),
curr: current.clone(),
dag.add_task(task_fn(move |x: i32| { idx: i,
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
}
}))
.depends_on(source) .depends_on(source)
}) })
.collect(); .collect();
+12 -87
View File
@@ -1,14 +1,20 @@
//! Tests for timing and speedup verification //! Tests for timing and speedup verification
use crate::common::task_fn; use dagx::{task, DagResult, DagRunner};
use dagx::{DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::time::sleep; use tokio::time::sleep;
struct SleepTask(usize, Duration);
#[task]
impl SleepTask {
async fn run(&self) -> usize {
sleep(self.1).await;
self.0
}
}
#[tokio::test] #[tokio::test]
async fn test_parallel_execution_speedup() -> DagResult<()> { async fn test_parallel_execution_speedup() -> DagResult<()> {
// Prove parallel execution by comparing timing // Prove parallel execution by comparing timing
@@ -19,12 +25,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
// Create independent tasks that each take 50ms // Create independent tasks that each take 50ms
let tasks: Vec<_> = (0..num_tasks) let tasks: Vec<_> = (0..num_tasks)
.map(|i| { .map(|i| dag.add_task(SleepTask(i, work_duration)))
dag.add_task(task_fn(move |_: ()| async move {
sleep(work_duration).await;
i
}))
})
.collect(); .collect();
let start = Instant::now(); let start = Instant::now();
@@ -55,79 +56,3 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
Ok(()) 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(())
}
+47 -43
View File
@@ -14,7 +14,7 @@ async fn test_butterfly_network() -> DagResult<()> {
// Layer 0: inputs // Layer 0: inputs
let inputs: Vec<_> = (0..8) let inputs: Vec<_> = (0..8)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { i as f64 })) dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64))
.into() .into()
}) })
.collect(); .collect();
@@ -26,25 +26,29 @@ async fn test_butterfly_network() -> DagResult<()> {
let idx2 = i * 2 + 1; let idx2 = i * 2 + 1;
// Sum operation // Sum operation
layer1.push( layer1.push(
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a + b) / 2.0 })) dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
.depends_on((&inputs[idx1], &inputs[idx2])), (a + b) / 2.0
}))
.depends_on((&inputs[idx1], &inputs[idx2])),
); );
// Difference operation // Difference operation
layer1.push( layer1.push(
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a - b) / 2.0 })) dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
.depends_on((&inputs[idx1], &inputs[idx2])), (a - b) / 2.0
}))
.depends_on((&inputs[idx1], &inputs[idx2])),
); );
} }
// Layer 2: crossover connections // Layer 2: crossover connections
let layer2: Vec<_> = vec![ 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])), .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])), .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])), .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])), .depends_on((&layer1[3], &layer1[7])),
]; ];
@@ -76,11 +80,13 @@ async fn test_recursive_butterfly() -> DagResult<()> {
// Butterfly connections // Butterfly connections
for i in 0..mid { for i in 0..mid {
let sum = dag 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])); .depends_on((&inputs[i], &inputs[i + mid]));
let diff = dag 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])); .depends_on((&inputs[i], &inputs[i + mid]));
outputs.push(sum); outputs.push(sum);
@@ -94,7 +100,7 @@ async fn test_recursive_butterfly() -> DagResult<()> {
let inputs: Vec<dagx::TaskHandle<i32>> = vec![10, 20, 30, 40] let inputs: Vec<dagx::TaskHandle<i32>> = vec![10, 20, 30, 40]
.into_iter() .into_iter()
.map(|val| { .map(|val| {
let task = dag.add_task(task_fn(move |_: ()| async move { val })); let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val));
task.into() task.into()
}) })
.collect(); .collect();
@@ -119,28 +125,36 @@ async fn test_benes_network() -> DagResult<()> {
// 4 inputs // 4 inputs
let inputs: Vec<_> = (0..4) let inputs: Vec<_> = (0..4)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { 1 << i })) dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i))
.into() .into()
}) // Powers of 2 }) // Powers of 2
.collect(); .collect();
// First stage: sort pairs // First stage: sort pairs
let stage1: Vec<_> = vec![ let stage1: Vec<_> = vec![
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) })) dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
.depends_on((&inputs[0], &inputs[1])), a.min(b)
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) })) }))
.depends_on((&inputs[0], &inputs[1])), .depends_on((&inputs[0], &inputs[1])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) })) dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
.depends_on((&inputs[2], &inputs[3])), a.max(b)
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) })) }))
.depends_on((&inputs[2], &inputs[3])), .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 // Second stage with crossover
let outputs: Vec<_> = vec![ 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])), .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])), .depends_on((&stage1[1], &stage1[3])),
]; ];
@@ -162,12 +176,10 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
let inputs: Vec<TaskHandle<_>> = (0..8) let inputs: Vec<TaskHandle<_>> = (0..8)
.map(|i| { .map(|i| {
let counter = counter.clone(); let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::Relaxed);
counter.fetch_add(1, Ordering::Relaxed); i
i
}
})) }))
.into() .into()
}) })
@@ -178,7 +190,7 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
.map(|i| { .map(|i| {
// Shuffle pattern: rotate bits // Shuffle pattern: rotate bits
let source = ((i << 1) | (i >> 2)) & 0x7; let source = ((i << 1) | (i >> 2)) & 0x7;
dag.add_task(task_fn(move |x: i32| async move { x })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x))
.depends_on(inputs[source]) .depends_on(inputs[source])
}) })
.collect(); .collect();
@@ -188,21 +200,13 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
.flat_map(|i| { .flat_map(|i| {
let idx = i * 2; let idx = i * 2;
vec![ vec![
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { dag.add_task(task_fn::<(i32, i32), _, _>(
if a > b { |(&a, &b): (&i32, &i32)| if a > b { b } else { a },
b ))
} else {
a
}
}))
.depends_on((&shuffled[idx], &shuffled[idx + 1])), .depends_on((&shuffled[idx], &shuffled[idx + 1])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { dag.add_task(task_fn::<(i32, i32), _, _>(
if a > b { |(&a, &b): (&i32, &i32)| if a > b { a } else { b },
a ))
} else {
b
}
}))
.depends_on((&shuffled[idx], &shuffled[idx + 1])), .depends_on((&shuffled[idx], &shuffled[idx + 1])),
] ]
}) })
+66 -73
View File
@@ -11,10 +11,10 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Input validation // Input validation
let input = dag.add_task(task_fn(|_: ()| async { 42 })); let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let validate = dag let validate = dag
.add_task(task_fn(|x: i32| async move { .add_task(task_fn::<i32, _, _>(|&x: &i32| {
if x > 0 && x < 100 { if x > 0 && x < 100 {
Ok(x) Ok(x)
} else { } else {
@@ -25,37 +25,37 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> {
// Process valid input // Process valid input
let process = dag let process = dag
.add_task(task_fn(|result: Result<i32, &str>| async move { .add_task(task_fn::<Result<_, _>, _, _>(
match result { |result: &Result<i32, &str>| match result {
Ok(x) => x * 2, Ok(x) => *x * 2,
Err(_) => -1, Err(_) => -1,
} },
})) ))
.depends_on(validate); .depends_on(validate);
// Parallel enhancement steps // Parallel enhancement steps
let enhance1 = dag let enhance1 = dag
.add_task(task_fn(|x: i32| async move { x + 10 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
.depends_on(process); .depends_on(process);
let enhance2 = dag let enhance2 = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(process); .depends_on(process);
let enhance3 = dag let enhance3 = dag
.add_task(task_fn(|x: i32| async move { x - 5 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
.depends_on(process); .depends_on(process);
// Combine enhancements // Combine enhancements
let combine = dag let combine = dag
.add_task(task_fn(|(e1, e2, e3): (i32, i32, i32)| async move { .add_task(task_fn::<(i32, i32, i32), _, _>(
vec![e1, e2, e3] |(&e1, &e2, &e3): (&i32, &i32, &i32)| vec![e1, e2, e3],
})) ))
.depends_on((&enhance1, &enhance2, &enhance3)); .depends_on((&enhance1, &enhance2, &enhance3));
// Final aggregation // Final aggregation
let final_result = dag let final_result = dag
.add_task(task_fn(|values: Vec<i32>| async move { .add_task(task_fn::<Vec<_>, _, _>(|values: &Vec<i32>| {
values.iter().sum::<i32>() values.iter().sum::<i32>()
})) }))
.depends_on(combine); .depends_on(combine);
@@ -76,7 +76,7 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
// Map phase: process 16 inputs in parallel // Map phase: process 16 inputs in parallel
let mappers: Vec<dagx::TaskHandle<Vec<(&str, usize)>>> = (0..16) let mappers: Vec<dagx::TaskHandle<Vec<(&str, usize)>>> = (0..16)
.map(|i| { .map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move { let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Simulate word count // Simulate word count
vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)] vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)]
})); }));
@@ -94,14 +94,23 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
match deps.len() { match deps.len() {
4 => dag 4 => dag
.add_task(task_fn( .add_task(task_fn::<
(
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
),
_,
_,
>(
move |inputs: ( move |inputs: (
Vec<(&str, usize)>, &Vec<(&str, usize)>,
Vec<(&str, usize)>, &Vec<(&str, usize)>,
Vec<(&str, usize)>, &Vec<(&str, usize)>,
Vec<(&str, usize)>, &Vec<(&str, usize)>,
)| async move { )| {
let mut combined = Vec::new(); let mut combined = Vec::<(&str, usize)>::new();
combined.extend(inputs.0); combined.extend(inputs.0);
combined.extend(inputs.1); combined.extend(inputs.1);
combined.extend(inputs.2); combined.extend(inputs.2);
@@ -119,11 +128,11 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
let reducers: Vec<_> = shuffled let reducers: Vec<_> = shuffled
.iter() .iter()
.map(|shuffled_data| { .map(|shuffled_data| {
dag.add_task(task_fn(|data: Vec<(&str, usize)>| async move { dag.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<(&str, usize)>| {
// Sum values by key // Sum values by key
let mut totals = std::collections::HashMap::new(); let mut totals = std::collections::HashMap::new();
for (key, value) in data { for (key, value) in data {
*totals.entry(key).or_insert(0) += value; *totals.entry(*key).or_insert(0) += value;
} }
totals.len() totals.len()
})) }))
@@ -133,8 +142,8 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
// Final aggregation // Final aggregation
let final_task = dag let final_task = dag
.add_task(task_fn( .add_task(task_fn::<(_, _, _, _), _, _>(
|(r1, r2, r3, r4): (usize, usize, usize, usize)| async move { r1 + r2 + r3 + r4 }, |(r1, r2, r3, r4): (&usize, &usize, &usize, &usize)| r1 + r2 + r3 + r4,
)) ))
.depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3])); .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)); let iteration_count = Arc::new(AtomicUsize::new(0));
// Initial value // Initial value
let init = dag.add_task(task_fn(|_: ()| async { 1 })); let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
// Stage 1: Transform // Stage 1: Transform
let stage1 = dag let stage1 = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let counter = iteration_count.clone(); let counter = iteration_count.clone();
move |x: i32| { move |x: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst); x * 2
x * 2
}
} }
})) }))
.depends_on(init); .depends_on(init);
// Stage 2: Filter // Stage 2: Filter
let stage2 = dag let stage2 = dag
.add_task(task_fn(|x: i32| async move { .add_task(task_fn::<i32, _, _>(
if x < 100 { |&x: &i32| if x < 100 { Some(x) } else { None },
Some(x) ))
} else {
None
}
}))
.depends_on(stage1); .depends_on(stage1);
// Stage 3: Accumulate // Stage 3: Accumulate
let stage3 = dag let stage3 = dag
.add_task(task_fn(|opt: Option<i32>| async move { .add_task(task_fn::<Option<_>, _, _>(|opt: &Option<i32>| {
opt.map(|x| x + 10).unwrap_or(0) opt.map(|x| x + 10).unwrap_or(0)
})) }))
.depends_on(stage2); .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 // 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 // by having a separate "feedback" task that would feed back in a real system
let _feedback = dag let _feedback = dag
.add_task(task_fn(|x: i32| async move { .add_task(task_fn::<i32, _, _>(|&x: &i32| {
// In a real feedback loop, this would feed back to stage1 // In a real feedback loop, this would feed back to stage1
x / 2 x / 2
})) }))
@@ -210,7 +213,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Scatter: distribute work to multiple workers // Scatter: distribute work to multiple workers
let source: TaskHandle<_> = dag let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { .add_task(task_fn::<(), _, _>(|_: ()| {
vec![10, 20, 30, 40, 50, 60, 70, 80] vec![10, 20, 30, 40, 50, 60, 70, 80]
})) }))
.into(); .into();
@@ -218,7 +221,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Workers process chunks // Workers process chunks
let workers: Vec<_> = (0..4) let workers: Vec<_> = (0..4)
.map(|worker_id| { .map(|worker_id| {
dag.add_task(task_fn(move |data: Vec<i32>| async move { dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<i32>| {
// Each worker processes a chunk // Each worker processes a chunk
let chunk_size = data.len() / 4; let chunk_size = data.len() / 4;
let start = worker_id * chunk_size; let start = worker_id * chunk_size;
@@ -236,8 +239,8 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Gather: collect results from all workers // Gather: collect results from all workers
let gather = dag let gather = dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|(w0, w1, w2, w3): (i32, i32, i32, i32)| async move { w0 + w1 + w2 + w3 }, |(w0, w1, w2, w3): (&i32, &i32, &i32, &i32)| w0 + w1 + w2 + w3,
)) ))
.depends_on((&workers[0], &workers[1], &workers[2], &workers[3])); .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)); let phase_counter = Arc::new(AtomicUsize::new(0));
// Fork phase // Fork phase
let parent = dag.add_task(task_fn({ let parent = dag.add_task(task_fn::<(), _, _>({
let counter = phase_counter.clone(); let counter = phase_counter.clone();
move |_: ()| { move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.store(1, Ordering::SeqCst);
counter.store(1, Ordering::SeqCst); 100
100
}
} }
})); }));
@@ -273,12 +274,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
for i in 0..3 { for i in 0..3 {
let counter = phase_counter.clone(); let counter = phase_counter.clone();
let task = dag let task = dag
.add_task(task_fn(move |x: i32| { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { assert_eq!(counter.load(Ordering::SeqCst), 1);
assert_eq!(counter.load(Ordering::SeqCst), 1); x + i
x + i
}
})) }))
.depends_on(prev1); .depends_on(prev1);
path1.push(task); path1.push(task);
@@ -290,12 +289,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
for i in 0..3 { for i in 0..3 {
let counter = phase_counter.clone(); let counter = phase_counter.clone();
let task = dag let task = dag
.add_task(task_fn(move |x: i32| { .add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { assert_eq!(counter.load(Ordering::SeqCst), 1);
assert_eq!(counter.load(Ordering::SeqCst), 1); x * 2 + i
x * 2 + i
}
})) }))
.depends_on(prev2); .depends_on(prev2);
path2.push(task); path2.push(task);
@@ -304,28 +301,24 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
// Barrier: wait for all paths // Barrier: wait for all paths
let barrier = dag let barrier = dag
.add_task(task_fn({ .add_task(task_fn::<(i32, i32), _, _>({
let counter = phase_counter.clone(); let counter = phase_counter.clone();
move |(p1, p2): (i32, i32)| { move |(p1, p2): (&i32, &i32)| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.store(2, Ordering::SeqCst);
counter.store(2, Ordering::SeqCst); p1 + p2
p1 + p2
}
} }
})) }))
.depends_on((&path1[2], &path2[2])); .depends_on((&path1[2], &path2[2]));
// Join phase // Join phase
let join = dag let join = dag
.add_task(task_fn({ .add_task(task_fn::<i32, _, _>({
let counter = phase_counter.clone(); let counter = phase_counter.clone();
move |x: i32| { move |&x: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { assert_eq!(counter.load(Ordering::SeqCst), 2);
assert_eq!(counter.load(Ordering::SeqCst), 2); x
x
}
} }
})) }))
.depends_on(barrier); .depends_on(barrier);
+21 -19
View File
@@ -14,10 +14,10 @@ async fn test_2d_grid_pattern() -> DagResult<()> {
// Initialize first row and column // Initialize first row and column
#[allow(clippy::needless_range_loop)] #[allow(clippy::needless_range_loop)]
for i in 0..4 { 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()); grid[0][i] = Some(task.into());
if i > 0 { 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()); 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 left = grid[i][j - 1].as_ref().unwrap();
let task = dag 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)); .depends_on((above, left));
grid[i][j] = Some(task); grid[i][j] = Some(task);
} }
@@ -54,7 +56,7 @@ async fn test_3d_grid_pattern() -> DagResult<()> {
vec![vec![vec![None; size]; size]; size]; vec![vec![vec![None; size]; size]; size];
// Initialize origin // 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()); grid[0][0][0] = Some(origin.into());
// Fill the 3D grid // Fill the 3D grid
@@ -76,18 +78,20 @@ async fn test_3d_grid_pattern() -> DagResult<()> {
let task_handle: dagx::TaskHandle<usize> = match deps.len() { let task_handle: dagx::TaskHandle<usize> = match deps.len() {
0 => { 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() t.into()
} }
1 => dag 1 => dag
.add_task(task_fn(move |prev: usize| async move { prev + 1 })) .add_task(task_fn::<usize, _, _>(move |prev: &usize| prev + 1))
.depends_on(deps[0]), .depends_on(deps[0]),
2 => dag 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])), .depends_on((&deps[0], &deps[1])),
3 => dag 3 => dag
.add_task(task_fn( .add_task(task_fn::<(usize, usize, usize), _, _>(
move |(a, b, c): (usize, usize, usize)| async move { a + b + c }, move |(a, b, c): (&usize, &usize, &usize)| a + b + c,
)) ))
.depends_on((&deps[0], &deps[1], &deps[2])), .depends_on((&deps[0], &deps[1], &deps[2])),
_ => unreachable!(), _ => unreachable!(),
@@ -120,7 +124,7 @@ async fn test_hexagonal_grid() -> DagResult<()> {
let mut hex_grid = std::collections::HashMap::new(); let mut hex_grid = std::collections::HashMap::new();
// Center hex // 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<i32> = center_builder.into(); let center: dagx::TaskHandle<i32> = center_builder.into();
hex_grid.insert((0, 0), center); hex_grid.insert((0, 0), center);
@@ -129,9 +133,7 @@ async fn test_hexagonal_grid() -> DagResult<()> {
for (i, &(dq, dr)) in directions.iter().enumerate() { for (i, &(dq, dr)) in directions.iter().enumerate() {
let neighbor: dagx::TaskHandle<i32> = dag let neighbor: dagx::TaskHandle<i32> = dag
.add_task(task_fn( .add_task(task_fn::<i32, _, _>(move |c: &i32| c + (i as i32 + 1) * 10))
move |c: i32| async move { c + (i as i32 + 1) * 10 },
))
.depends_on(center); .depends_on(center);
hex_grid.insert((dq, dr), neighbor); hex_grid.insert((dq, dr), neighbor);
} }
@@ -159,15 +161,15 @@ async fn test_hexagonal_grid() -> DagResult<()> {
.collect(); .collect();
let task: dagx::TaskHandle<i32> = if neighbors.len() == 2 { let task: dagx::TaskHandle<i32> = 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 (a + b) / 2 + q + r
})) }))
.depends_on((&neighbors[0], &neighbors[1])) .depends_on((&neighbors[0], &neighbors[1]))
} else if neighbors.len() == 1 { } else if neighbors.len() == 1 {
dag.add_task(task_fn(move |n: i32| async move { n + q + r })) dag.add_task(task_fn::<i32, _, _>(move |n: &i32| n + q + r))
.depends_on(neighbors[0]) .depends_on(neighbors[0])
} else { } 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() t.into()
}; };
@@ -193,7 +195,7 @@ async fn test_toroidal_grid() -> DagResult<()> {
#[allow(clippy::needless_range_loop)] #[allow(clippy::needless_range_loop)]
for i in 0..SIZE { for i in 0..SIZE {
for j 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()); 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 right = grid[i][(j + 1) % SIZE].as_ref().unwrap();
let task = dag let task = dag
.add_task(task_fn( .add_task(task_fn::<(_, _, _, _, _), _, _>(
move |(c, u, d, l, r): (i32, i32, i32, i32, i32)| async move { move |(c, u, d, l, r): (&i32, &i32, &i32, &i32, &i32)| {
// Average of self and 4 neighbors // Average of self and 4 neighbors
(c + u + d + l + r) / 5 (c + u + d + l + r) / 5
}, },
+36 -36
View File
@@ -10,7 +10,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> {
// Create 8 leaf nodes // Create 8 leaf nodes
let leaves: Vec<_> = (0..8) 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(); .collect();
// Build binary tree reduction // Build binary tree reduction
@@ -23,7 +23,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> {
for chunk in current_level.chunks(2) { for chunk in current_level.chunks(2) {
if chunk.len() == 2 { if chunk.len() == 2 {
let task = dag 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])); .depends_on((&chunk[0], &chunk[1]));
next_level.push(task); next_level.push(task);
} else { } else {
@@ -51,7 +51,7 @@ async fn test_n_ary_tree() -> DagResult<()> {
// Build a 3-ary tree (each node has 3 children) // Build a 3-ary tree (each node has 3 children)
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> { fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
if depth == 0 { 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(); return task.into();
} }
@@ -59,9 +59,9 @@ async fn test_n_ary_tree() -> DagResult<()> {
.map(|i| build_tree(dag, depth - 1, value * 3 + i)) .map(|i| build_tree(dag, depth - 1, value * 3 + i))
.collect(); .collect();
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move { dag.add_task(task_fn::<(i32, i32, i32), _, _>(
value + a + b + c move |(a, b, c): (&i32, &i32, &i32)| value + a + b + c,
})) ))
.depends_on((&children[0], &children[1], &children[2])) .depends_on((&children[0], &children[1], &children[2]))
} }
@@ -88,30 +88,30 @@ async fn test_unbalanced_tree() -> DagResult<()> {
// / \ // / \
// f g // f g
let f: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into(); let f: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
let g = dag.add_task(task_fn(|_: ()| async { 2 })); let g = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(f); .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<i32> = d_builder.into(); let d: dagx::TaskHandle<i32> = d_builder.into();
let e = dag let e = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(g); .depends_on(g);
let a = dag 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)); .depends_on((&c, &d));
let b = dag let b = dag
.add_task(task_fn(|e: i32| async move { e + 10 })) .add_task(task_fn::<i32, _, _>(|e: &i32| e + 10))
.depends_on(e); .depends_on(e);
let root = dag 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)); .depends_on((&a, &b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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 // Build a trie-like structure for paths
let root: TaskHandle<_> = dag let root: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { "root".to_string() })) .add_task(task_fn::<(), _, _>(|_: ()| "root".to_string()))
.into(); .into();
// First level // First level
let usr = dag let usr = dag
.add_task(task_fn(|r: String| async move { format!("{}/usr", r) })) .add_task(task_fn::<String, _, _>(|r: &String| format!("{}/usr", r)))
.depends_on(root); .depends_on(root);
let etc = dag let etc = dag
.add_task(task_fn(|r: String| async move { format!("{}/etc", r) })) .add_task(task_fn::<String, _, _>(|r: &String| format!("{}/etc", r)))
.depends_on(root); .depends_on(root);
let var = dag let var = dag
.add_task(task_fn(|r: String| async move { format!("{}/var", r) })) .add_task(task_fn::<String, _, _>(|r: &String| format!("{}/var", r)))
.depends_on(root); .depends_on(root);
// Second level // Second level
let usr_bin = dag let usr_bin = dag
.add_task(task_fn(|u: String| async move { format!("{}/bin", u) })) .add_task(task_fn::<String, _, _>(|u: &String| format!("{}/bin", u)))
.depends_on(usr); .depends_on(usr);
let usr_lib = dag let usr_lib = dag
.add_task(task_fn(|u: String| async move { format!("{}/lib", u) })) .add_task(task_fn::<String, _, _>(|u: &String| format!("{}/lib", u)))
.depends_on(usr); .depends_on(usr);
let etc_config = dag let etc_config = dag
.add_task(task_fn(|e: String| async move { format!("{}/config", e) })) .add_task(task_fn::<String, _, _>(|e: &String| {
format!("{}/config", e)
}))
.depends_on(etc); .depends_on(etc);
let var_log = dag let var_log = dag
.add_task(task_fn(|v: String| async move { format!("{}/log", v) })) .add_task(task_fn::<String, _, _>(|v: &String| format!("{}/log", v)))
.depends_on(var); .depends_on(var);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; 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, node_id: usize,
) -> dagx::TaskHandle<usize> { ) -> dagx::TaskHandle<usize> {
if depth == 0 { 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(); return task.into();
} }
@@ -199,20 +201,18 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> {
match k { match k {
2 => dag 2 => dag
.add_task(task_fn(move |(a, b): (usize, usize)| async move { .add_task(task_fn::<(usize, usize), _, _>(
node_id + a + b move |(a, b): (&usize, &usize)| node_id + a + b,
})) ))
.depends_on((&children[0], &children[1])), .depends_on((&children[0], &children[1])),
3 => dag 3 => dag
.add_task(task_fn( .add_task(task_fn::<(usize, usize, usize), _, _>(
move |(a, b, c): (usize, usize, usize)| async move { node_id + a + b + c }, move |(a, b, c): (&usize, &usize, &usize)| node_id + a + b + c,
)) ))
.depends_on((&children[0], &children[1], &children[2])), .depends_on((&children[0], &children[1], &children[2])),
4 => dag 4 => dag
.add_task(task_fn( .add_task(task_fn::<(usize, usize, usize, usize), _, _>(
move |(a, b, c, d): (usize, usize, usize, usize)| async move { move |(a, b, c, d): (&usize, &usize, &usize, &usize)| node_id + a + b + c + d,
node_id + a + b + c + d
},
)) ))
.depends_on((&children[0], &children[1], &children[2], &children[3])), .depends_on((&children[0], &children[1], &children[2], &children[3])),
_ => panic!("Unsupported k value"), _ => panic!("Unsupported k value"),
@@ -236,7 +236,7 @@ async fn test_merkle_tree_pattern() -> DagResult<()> {
// Simulate a Merkle tree for 8 data blocks // Simulate a Merkle tree for 8 data blocks
let data_blocks: Vec<_> = (0..8) let data_blocks: Vec<_> = (0..8)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Simulate hash of data block // Simulate hash of data block
format!("hash_{}", i) format!("hash_{}", i)
})) }))
@@ -253,9 +253,9 @@ async fn test_merkle_tree_pattern() -> DagResult<()> {
for pair in current_level.chunks(2) { for pair in current_level.chunks(2) {
if pair.len() == 2 { if pair.len() == 2 {
let hash_task = dag let hash_task = dag
.add_task(task_fn(|(left, right): (String, String)| async move { .add_task(task_fn::<(String, String), _, _>(
format!("hash({}_{})", left, right) |(left, right): (&String, &String)| format!("hash({}_{})", left, right),
})) ))
.depends_on((&pair[0], &pair[1])); .depends_on((&pair[0], &pair[1]));
next_level.push(hash_task); next_level.push(hash_task);
} else { } else {
-52
View File
@@ -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)
}
+101 -3
View File
@@ -1,4 +1,102 @@
//! Performance tests module //! Stress tests and benchmarks
pub mod memory; use crate::common::task_fn;
pub mod stress; 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::<i32, _, _>(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::<i32, _, _>(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<i32> = source.into();
let left = dag
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source_handle);
let right = dag
.add_task(task_fn::<i32, _, _>(|&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<i32> = hub.into();
for j in 0..5 {
let fan = dag
.add_task(task_fn::<i32, _, _>(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());
}
}
-102
View File
@@ -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<i32> = 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<i32> = 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());
}
}
+3 -3
View File
@@ -37,7 +37,7 @@ async fn test_parallel_execution_async_std() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let tasks: Vec<_> = (0..10) 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(); .collect();
dag.run(async_std::task::spawn).await.unwrap(); 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 b = dag.add_task(Value(20));
let sum = dag.add_task(Add).depends_on((a, b)); let sum = dag.add_task(Add).depends_on((a, b));
let double = dag let double = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(sum); .depends_on(sum);
dag.run(async_std::task::spawn).await.unwrap(); dag.run(async_std::task::spawn).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60); assert_eq!(dag.get(double).unwrap().clone(), 60);
} }
+37 -39
View File
@@ -32,10 +32,10 @@ fn test_async_executor_basic() {
let future = async { let future = async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 10 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let y = dag.add_task(task_fn(|_: ()| async { 20 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag 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)); .depends_on((x, y));
dag.run(|fut| executor.spawn(fut)).await.unwrap(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -54,18 +54,18 @@ fn test_async_executor_complex_dag() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Create a diamond pattern // 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 let left = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source); .depends_on(source);
let right = dag let right = dag
.add_task(task_fn(|x: i32| async move { x * 3 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
.depends_on(source); .depends_on(source);
let sink = dag 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)); .depends_on((&left, &right));
dag.run(|fut| executor.spawn(fut)).await.unwrap(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -88,12 +88,10 @@ fn test_async_executor_parallel_tasks() {
let tasks: Vec<_> = (0..10) let tasks: Vec<_> = (0..10)
.map(|i| { .map(|i| {
let c = counter.clone(); let c = counter.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c.clone(); let c = c.clone();
async move { c.fetch_add(1, Ordering::SeqCst);
c.fetch_add(1, Ordering::SeqCst); i
i
}
})) }))
}) })
.collect(); .collect();
@@ -120,7 +118,7 @@ fn test_async_executor_with_local_spawn() {
let future = async { let future = async {
let dag = DagRunner::new(); 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(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -138,14 +136,14 @@ fn test_async_executor_deep_chain() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Build a chain of 50 tasks // 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 let mut current = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(start); .depends_on(start);
for _ in 1..50 { for _ in 1..50 {
current = dag current = dag
.add_task(task_fn(|x: i32| async move { x + 1 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(current); .depends_on(current);
} }
@@ -167,9 +165,9 @@ fn test_pollster_basic() {
pollster::block_on(async { pollster::block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 100 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
let doubled = dag let doubled = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(x); .depends_on(x);
dag.run(|fut| async { pollster::block_on(fut) }) dag.run(|fut| async { pollster::block_on(fut) })
@@ -186,11 +184,11 @@ fn test_pollster_complex() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Fan-out pattern // 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) let tasks: Vec<_> = (0..5)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x * i })) dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * i))
.depends_on(source) .depends_on(source)
}) })
.collect(); .collect();
@@ -211,7 +209,7 @@ fn test_pollster_error_handling() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Task that returns Result // Task that returns Result
let task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) })); let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
dag.run(|fut| async { pollster::block_on(fut) }) dag.run(|fut| async { pollster::block_on(fut) })
.await .await
@@ -230,10 +228,10 @@ fn test_futures_executor_basic() {
futures_executor::block_on(async { futures_executor::block_on(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 15 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15));
let y = dag.add_task(task_fn(|_: ()| async { 25 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25));
let product = dag 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)); .depends_on((x, y));
let spawner = ThreadPool::new().unwrap(); let spawner = ThreadPool::new().unwrap();
@@ -256,7 +254,7 @@ fn test_futures_executor_threadpool() {
// Create multiple independent tasks // Create multiple independent tasks
let tasks: Vec<_> = (0..20) 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(); .collect();
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
@@ -281,18 +279,18 @@ fn test_futures_executor_diamond_pattern() {
futures_executor::block_on(async { futures_executor::block_on(async {
let dag = DagRunner::new(); 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 let b = dag
.add_task(task_fn(|x: i32| async move { x + 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(a); .depends_on(a);
let c = dag let c = dag
.add_task(task_fn(|x: i32| async move { x - 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
.depends_on(a); .depends_on(a);
let d = dag 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)); .depends_on((&b, &c));
let spawner = ThreadPool::new().unwrap(); let spawner = ThreadPool::new().unwrap();
@@ -318,7 +316,7 @@ fn test_all_runtimes_compatibility() {
let executor = async_executor::Executor::new(); let executor = async_executor::Executor::new();
executor.run(async { executor.run(async {
let dag = DagRunner::new(); 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(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
assert_eq!(dag.get(x).unwrap(), 123); assert_eq!(dag.get(x).unwrap(), 123);
}); });
@@ -328,7 +326,7 @@ fn test_all_runtimes_compatibility() {
{ {
pollster::block_on(async { pollster::block_on(async {
let dag = DagRunner::new(); 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) }) dag.run(|fut| async { pollster::block_on(fut) })
.await .await
.unwrap(); .unwrap();
@@ -340,7 +338,7 @@ fn test_all_runtimes_compatibility() {
{ {
futures_executor::block_on(async { futures_executor::block_on(async {
let dag = DagRunner::new(); 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(); let spawner = ThreadPool::new().unwrap();
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
.await .await
@@ -360,7 +358,7 @@ fn test_runtime_edge_cases() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// This tests that our API works even with !Send futures // 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 // Local state
42 42
@@ -386,7 +384,7 @@ fn test_async_executor_stress() {
// Create 100 tasks // Create 100 tasks
let tasks: Vec<_> = (0..100) let tasks: Vec<_> = (0..100)
.map(|i| { .map(|i| {
dag.add_task(task_fn(move |_: ()| async move { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Small delay to test executor scheduling // Small delay to test executor scheduling
i i
})) }))
@@ -406,7 +404,7 @@ fn test_pollster_nested_blocks() {
// Test nested blocking - edge case // Test nested blocking - edge case
pollster::block_on(async { pollster::block_on(async {
let dag1 = DagRunner::new(); 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) }) dag1.run(|fut| async { pollster::block_on(fut) })
.await .await
@@ -425,7 +423,7 @@ fn test_futures_executor_multi_pool() {
pool1.spawn_ok(async move { pool1.spawn_ok(async move {
let dag = DagRunner::new(); 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()) dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
.await .await
@@ -459,8 +457,8 @@ fn test_sync_tasks_on_async_executor() {
executor.run(async { executor.run(async {
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 5 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn(|_: ()| async { 7 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 7));
let sum = dag.add_task(SyncAdd).depends_on((x, y)); let sum = dag.add_task(SyncAdd).depends_on((x, y));
dag.run(|fut| executor.spawn(fut)).await.unwrap(); dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -483,7 +481,7 @@ fn test_sync_tasks_on_pollster() {
pollster::block_on(async { pollster::block_on(async {
let dag = DagRunner::new(); 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); let doubled = dag.add_task(SyncDouble).depends_on(x);
dag.run(|fut| async { pollster::block_on(fut) }) dag.run(|fut| async { pollster::block_on(fut) })
+3 -3
View File
@@ -40,7 +40,7 @@ fn test_parallel_execution_smol() {
let dag = DagRunner::new(); let dag = DagRunner::new();
let tasks: Vec<_> = (0..10) 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(); .collect();
dag.run(smol::spawn).await.unwrap(); dag.run(smol::spawn).await.unwrap();
@@ -60,11 +60,11 @@ fn test_complex_dependencies_smol() {
let b = dag.add_task(Value(20)); let b = dag.add_task(Value(20));
let sum = dag.add_task(Add).depends_on((a, b)); let sum = dag.add_task(Add).depends_on((a, b));
let double = dag let double = dag
.add_task(task_fn(|x: i32| async move { x * 2 })) .add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(sum); .depends_on(sum);
dag.run(smol::spawn).await.unwrap(); dag.run(smol::spawn).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60); assert_eq!(dag.get(double).unwrap().clone(), 60);
}); });
} }
+13 -19
View File
@@ -41,8 +41,8 @@ async fn test_all_three_task_patterns() {
let dag = DagRunner::new(); let dag = DagRunner::new();
// Create initial values // Create initial values
let x = dag.add_task(task_fn(|_: ()| async { 5 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn(|_: ()| async { 3 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
// Pattern 1: Stateless addition // Pattern 1: Stateless addition
let sum = dag.add_task(StatelessAdd).depends_on((x, y)); // 5 + 3 = 8 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 c2 = counter.clone();
let c3 = 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(); let c = c1.clone();
async move { c.fetch_add(1, Ordering::SeqCst);
c.fetch_add(1, Ordering::SeqCst); 1
1
}
})); }));
let t2 = dag.add_task(task_fn(move |_: ()| { let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c2.clone(); let c = c2.clone();
async move { c.fetch_add(1, Ordering::SeqCst);
c.fetch_add(1, Ordering::SeqCst); 2
2
}
})); }));
let t3 = dag.add_task(task_fn(move |_: ()| { let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c3.clone(); let c = c3.clone();
async move { c.fetch_add(1, Ordering::SeqCst);
c.fetch_add(1, Ordering::SeqCst); 3
3
}
})); }));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 dag = DagRunner::new();
let multiplier = 7; let multiplier = 7;
let base = dag.add_task(task_fn(|_: ()| async { 5 })); let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let scaled = dag let scaled = dag
.add_task(task_fn(move |x: i32| async move { x * multiplier })) .add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
.depends_on(base); .depends_on(base);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+3 -3
View File
@@ -54,7 +54,7 @@ async fn test_readonly_state_with_input() {
// Test read-only state task with input using &self // Test read-only state task with input using &self
let dag = DagRunner::new(); 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); let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 // Test that read-only state can be used multiple times
let dag = DagRunner::new(); let dag = DagRunner::new();
let input1 = dag.add_task(task_fn(|_: ()| async { 10 })); let input1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let input2 = dag.add_task(task_fn(|_: ()| async { 20 })); let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let config = ReadOnlyConfig { let config = ReadOnlyConfig {
multiplier: 3, multiplier: 3,
+12 -12
View File
@@ -82,8 +82,8 @@ async fn test_stateless_basic() {
// Test basic stateless task with two inputs // Test basic stateless task with two inputs
let dag = DagRunner::new(); let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 10 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let y = dag.add_task(task_fn(|_: ()| async { 20 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag.add_task(StatelessAdd).depends_on((x, y)); let sum = dag.add_task(StatelessAdd).depends_on((x, y));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 // Test stateless task with single input
let dag = DagRunner::new(); 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); let doubled = dag.add_task(StatelessDouble).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)) 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 // Test stateless task with multiple inputs of different types
let dag = DagRunner::new(); let dag = DagRunner::new();
let num = dag.add_task(task_fn(|_: ()| async { 42 })); let num = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let text = dag.add_task(task_fn(|_: ()| async { "test".to_string() })); let text = dag.add_task(task_fn::<(), _, _>(|_: ()| "test".to_string()));
let flag = dag.add_task(task_fn(|_: ()| async { true })); let flag = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
let result = dag.add_task(StatelessFormat).depends_on((num, text, flag)); 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 dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 5 })); let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn(|_: ()| async { 3 })); let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
// Stateless multiplication // Stateless multiplication
let product = dag.add_task(StatelessMultiply).depends_on((x, y)); let product = dag.add_task(StatelessMultiply).depends_on((x, y));
@@ -182,7 +182,7 @@ async fn test_stateless_chain() {
// Test chaining stateless tasks // Test chaining stateless tasks
let dag = DagRunner::new(); 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 step1 = dag.add_task(StatelessInc).depends_on(start);
let step2 = dag.add_task(StatelessInc).depends_on(step1); let step2 = dag.add_task(StatelessInc).depends_on(step1);
let step3 = dag.add_task(StatelessInc).depends_on(step2); let step3 = dag.add_task(StatelessInc).depends_on(step2);
@@ -203,7 +203,7 @@ async fn test_stateless_parallel() {
let tasks: Vec<_> = (1..=5) let tasks: Vec<_> = (1..=5)
.map(|i| { .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) dag.add_task(StatelessSquare).depends_on(source)
}) })
.collect(); .collect();
@@ -224,8 +224,8 @@ async fn test_stateless_with_string_types() {
// Test stateless tasks with complex types // Test stateless tasks with complex types
let dag = DagRunner::new(); let dag = DagRunner::new();
let hello = dag.add_task(task_fn(|_: ()| async { "Hello, ".to_string() })); let hello = dag.add_task(task_fn::<(), _, _>(|_: ()| "Hello, ".to_string()));
let world = dag.add_task(task_fn(|_: ()| async { "World!".to_string() })); let world = dag.add_task(task_fn::<(), _, _>(|_: ()| "World!".to_string()));
let greeting = dag.add_task(StatelessConcat).depends_on((hello, world)); let greeting = dag.add_task(StatelessConcat).depends_on((hello, world));
+80 -63
View File
@@ -1,10 +1,11 @@
//! Tests for concurrent access patterns and race conditions //! Tests for concurrent access patterns and race conditions
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle}; use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[tokio::test] #[tokio::test]
@@ -12,13 +13,20 @@ async fn test_concurrent_result_retrieval() -> DagResult<()> {
// Test concurrent get() calls after execution // Test concurrent get() calls after execution
let dag = Arc::new(DagRunner::new()); 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 // Add tasks with minimal delays
let tasks: Vec<dagx::TaskHandle<usize>> = (0..10) let tasks: Vec<dagx::TaskHandle<usize>> = (0..10)
.map(|i| { .map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move { let task = dag.add_task(DelayTask(i, Duration::from_millis(1)));
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
i as usize
}));
task.into() task.into()
}) })
.collect(); .collect();
@@ -54,7 +62,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
// Start with a few initial tasks // Start with a few initial tasks
let initial: Vec<_> = (0..5) 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(); .collect();
// Start building more tasks in background // 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 build_handle = tokio::spawn(async move {
let mut tasks: Vec<dagx::TaskHandle<i32>> = Vec::new(); let mut tasks: Vec<dagx::TaskHandle<i32>> = Vec::new();
for i in 5..50 { 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()); tasks.push(task.into());
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; 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 // Create a source task
let source: TaskHandle<_> = dag let source: TaskHandle<_> = dag
.add_task(task_fn({ .add_task(task_fn::<(), _, _>({
let counter = resolution_counter.clone(); let counter = resolution_counter.clone();
move |_: ()| { move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst); 42
42
}
} }
})) }))
.into(); .into();
@@ -124,12 +130,10 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> {
let dependents: Vec<_> = (0..100) let dependents: Vec<_> = (0..100)
.map(|i| { .map(|i| {
let counter = resolution_counter.clone(); let counter = resolution_counter.clone();
dag.add_task(task_fn(move |val: i32| { dag.add_task(task_fn::<i32, _, _>(move |val: &i32| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst); val + i
val + i
}
})) }))
.depends_on(source) .depends_on(source)
}) })
@@ -154,38 +158,56 @@ async fn test_read_write_race_patterns() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let shared_state = Arc::new(RwLock::new(Vec::new())); let shared_state = Arc::new(RwLock::new(Vec::new()));
struct WriterTask {
state: Arc<RwLock<Vec<i32>>>,
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 // Writers
let writers: Vec<_> = (0..10) let writers: Vec<_> = (0..10)
.map(|i| { .map(|i| {
let state = shared_state.clone(); dag.add_task(WriterTask {
dag.add_task(task_fn(move |_: ()| { state: shared_state.clone(),
let state = state.clone(); idx: i,
async move { })
let mut guard = state.write().await;
guard.push(i);
tokio::task::yield_now().await; // Force potential race
guard.push(i * 10);
i
}
}))
.into() .into()
}) })
.collect(); .collect();
struct ReaderTask {
state: Arc<RwLock<Vec<i32>>>,
}
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) // Readers (depend on writers to ensure ordering)
let readers: Vec<_> = writers let readers: Vec<_> = writers
.iter() .iter()
.map(|writer| { .map(|writer| {
let state = shared_state.clone(); dag.add_task(ReaderTask {
dag.add_task(task_fn(move |writer_id: i32| { state: shared_state.clone(),
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)
}
}))
.depends_on(writer) .depends_on(writer)
}) })
.collect(); .collect();
@@ -194,7 +216,7 @@ async fn test_read_write_race_patterns() -> DagResult<()> {
// Verify readers saw their writer's data // Verify readers saw their writer's data
for reader in &readers { 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); assert!(saw_writer, "Reader didn't see writer {}", writer_id);
} }
@@ -217,30 +239,27 @@ async fn test_atomic_counter_races() -> DagResult<()> {
.map(|_| { .map(|_| {
let counter = counter.clone(); let counter = counter.clone();
let max_seen = max_seen.clone(); let max_seen = max_seen.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
let max_seen = max_seen.clone(); let max_seen = max_seen.clone();
async move { let mut local_max = 0;
let mut local_max = 0; for _ in 0..100 {
for _ in 0..100 { let val = counter.fetch_add(1, Ordering::SeqCst);
let val = counter.fetch_add(1, Ordering::SeqCst); local_max = local_max.max(val);
local_max = local_max.max(val);
// Update global max // Update global max
let mut current_max = max_seen.load(Ordering::SeqCst); let mut current_max = max_seen.load(Ordering::SeqCst);
while val > current_max { while val > current_max {
match max_seen.compare_exchange_weak( match max_seen.compare_exchange_weak(
current_max, current_max,
val, val,
Ordering::SeqCst, Ordering::SeqCst,
Ordering::SeqCst, Ordering::SeqCst,
) { ) {
Ok(_) => break, Ok(_) => break,
Err(x) => current_max = x, Err(x) => current_max = x,
}
} }
} }
local_max
} }
})) }))
}) })
@@ -272,12 +291,10 @@ async fn test_multiple_dag_runners_interaction() -> DagResult<()> {
let tasks: Vec<_> = (0..20) let tasks: Vec<_> = (0..20)
.map(|task_id| { .map(|task_id| {
let counter = counter.clone(); let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| { dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone(); let counter = counter.clone();
async move { counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst); (dag_id, task_id)
(dag_id, task_id)
}
})) }))
}) })
.collect(); .collect();
+232 -194
View File
@@ -1,7 +1,7 @@
//! Tests for forced execution interleaving patterns //! Tests for forced execution interleaving patterns
use crate::common::task_fn; use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle}; use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt; use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -14,56 +14,47 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
let barrier = Arc::new(Barrier::new(3)); let barrier = Arc::new(Barrier::new(3));
let order = Arc::new(parking_lot::Mutex::new(Vec::new())); let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
struct InterleaveTask {
barrier: Arc<Barrier>,
order: Arc<parking_lot::Mutex<Vec<String>>>,
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 // Three parallel tasks that synchronize at barrier
let t1 = dag.add_task(task_fn({ let t1 = dag.add_task(InterleaveTask {
let barrier = barrier.clone(); barrier: barrier.clone(),
let order = order.clone(); order: order.clone(),
move |_: ()| { prefix: "t1",
let barrier = barrier.clone(); value: 1,
let order = order.clone(); });
async move { let t2 = dag.add_task(InterleaveTask {
order.lock().push("t1_start"); barrier: barrier.clone(),
barrier.wait().await; order: order.clone(),
order.lock().push("t1_end"); prefix: "t2",
1 value: 2,
} });
} let t3 = dag.add_task(InterleaveTask {
})); barrier: barrier.clone(),
order: order.clone(),
let t2 = dag.add_task(task_fn({ prefix: "t2",
let barrier = barrier.clone(); value: 3,
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
}
}
}));
// Collector // Collector
let collector = dag let collector = dag
.add_task(task_fn( .add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (i32, i32, i32)| async move { a + b + c }, |(a, b, c): (&i32, &i32, &i32)| a + b + c,
)) ))
.depends_on((t1, t2, t3)); .depends_on((t1, t2, t3));
@@ -93,76 +84,105 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
let events: Arc<parking_lot::Mutex<Vec<String>>> = let events: Arc<parking_lot::Mutex<Vec<String>>> =
Arc::new(parking_lot::Mutex::new(Vec::new())); Arc::new(parking_lot::Mutex::new(Vec::new()));
// Two chains that alternate execution struct FirstA {
let first_a = dag.add_task(task_fn({ turn: Arc<AtomicUsize>,
let turn = turn.clone(); events: Arc<parking_lot::Mutex<Vec<String>>>,
let events = events.clone(); }
move |_: ()| {
let turn = turn.clone(); #[task]
let events = events.clone(); impl FirstA {
async move { async fn run(&self) -> i32 {
while turn.load(Ordering::SeqCst) != 0 { while self.turn.load(Ordering::SeqCst) != 0 {
tokio::task::yield_now().await; tokio::task::yield_now().await;
}
events.lock().push("A0".to_string());
turn.store(1, Ordering::SeqCst);
0
} }
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<i32> = first_a.into(); let mut chain_a: dagx::TaskHandle<i32> = first_a.into();
let first_b = dag.add_task(task_fn({ struct FirstB {
let turn = turn.clone(); turn: Arc<AtomicUsize>,
let events = events.clone(); events: Arc<parking_lot::Mutex<Vec<String>>>,
move |_: ()| { }
let turn = turn.clone();
let events = events.clone(); #[task]
async move { impl FirstB {
while turn.load(Ordering::SeqCst) != 1 { async fn run(&self) -> i32 {
tokio::task::yield_now().await; while self.turn.load(Ordering::SeqCst) != 1 {
} tokio::task::yield_now().await;
events.lock().push("B0".to_string());
turn.store(2, Ordering::SeqCst);
0
} }
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<i32> = first_b.into(); let mut chain_b: dagx::TaskHandle<i32> = first_b.into();
struct ChainA {
turn: Arc<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<String>>>,
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<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<String>>>,
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 { for i in 1..5 {
let turn_clone = turn.clone();
let events_clone = events.clone();
chain_a = dag chain_a = dag
.add_task(task_fn(move |x: i32| { .add_task(ChainA {
let turn_clone = turn_clone.clone(); events: events.clone(),
let events_clone = events_clone.clone(); turn: turn.clone(),
async move { idx: i,
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
}
}))
.depends_on(chain_a); .depends_on(chain_a);
let turn_clone = turn.clone();
let events_clone = events.clone();
chain_b = dag chain_b = dag
.add_task(task_fn(move |x: i32| { .add_task(ChainB {
let turn_clone = turn_clone.clone(); turn: turn.clone(),
let events_clone = events_clone.clone(); events: events.clone(),
async move { idx: i,
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
}
}))
.depends_on(chain_b); .depends_on(chain_b);
} }
@@ -184,55 +204,62 @@ async fn test_layered_interleaving() -> DagResult<()> {
let dag = DagRunner::new(); let dag = DagRunner::new();
let layer_active = Arc::new(AtomicUsize::new(0)); let layer_active = Arc::new(AtomicUsize::new(0));
struct Layer1Task {
active: Arc<AtomicUsize>,
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 // Layer 1: 4 tasks
let layer1: Vec<_> = (0..4) let layer1: Vec<_> = (0..4)
.map(|i| { .map(|i| {
let active = layer_active.clone(); dag.add_task(Layer1Task {
dag.add_task(task_fn(move |_: ()| { active: layer_active.clone(),
let active = active.clone(); idx: i,
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
}
}))
.into() .into()
}) })
.collect(); .collect();
struct Layer23Task {
active: Arc<AtomicUsize>,
}
#[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 // Layer 2: 2 tasks, each depends on 2 from layer1
let layer2: Vec<_> = (0..2) let layer2: Vec<_> = (0..2)
.map(|i| { .map(|i| {
let active = layer_active.clone();
let idx = i * 2; let idx = i * 2;
dag.add_task(task_fn(move |(a, b): (i32, i32)| { dag.add_task(Layer23Task {
let active = active.clone(); active: layer_active.clone(),
async move { })
active.fetch_add(1, Ordering::SeqCst);
tokio::task::yield_now().await;
active.fetch_sub(1, Ordering::SeqCst);
a + b
}
}))
.depends_on((&layer1[idx], &layer1[idx + 1])) .depends_on((&layer1[idx], &layer1[idx + 1]))
}) })
.collect(); .collect();
// Layer 3: 1 task depends on both layer2 // Layer 3: 1 task depends on both layer2
let layer3 = dag let layer3 = dag
.add_task(task_fn({ .add_task(Layer23Task {
let active = layer_active.clone(); 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
}
}
}))
.depends_on((&layer2[0], &layer2[1])); .depends_on((&layer2[0], &layer2[1]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -252,82 +279,93 @@ async fn test_cross_branch_synchronization() -> DagResult<()> {
// Branch A // Branch A
let a1: TaskHandle<_> = dag let a1: TaskHandle<_> = dag
.add_task(task_fn({ .add_task(task_fn::<(), _, _>({
let sync = sync_point.clone(); let sync = sync_point.clone();
let events = events.clone(); let events = events.clone();
move |_: ()| { move |_: ()| {
let sync = sync.clone(); events.lock().push("A1");
let events = events.clone(); sync.fetch_add(1, Ordering::SeqCst);
async move { 1
events.lock().push("A1");
sync.fetch_add(1, Ordering::SeqCst);
1
}
} }
})) }))
.into(); .into();
let a2 = dag struct A2Task {
.add_task(task_fn({ sync: Arc<AtomicUsize>,
let sync = sync_point.clone(); events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
let events = events.clone(); }
move |x: i32| {
let sync = sync.clone(); #[task]
let events = events.clone(); impl A2Task {
async move { async fn run(&self, x: &i32) -> i32 {
// Wait for B1 // Wait for B1
while sync.load(Ordering::SeqCst) < 2 { while self.sync.load(Ordering::SeqCst) < 2 {
tokio::task::yield_now().await; tokio::task::yield_now().await;
}
events.lock().push("A2");
sync.fetch_add(1, Ordering::SeqCst);
x + 1
}
} }
})) 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); .depends_on(a1);
// Branch B struct B1Task {
let b1 = dag.add_task(task_fn({ sync: Arc<AtomicUsize>,
let sync = sync_point.clone(); events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
let events = events.clone(); }
move |_: ()| {
let sync = sync.clone(); #[task]
let events = events.clone(); impl B1Task {
async move { async fn run(&self) -> i32 {
// Wait for A1 // Wait for A1
while sync.load(Ordering::SeqCst) < 1 { while self.sync.load(Ordering::SeqCst) < 1 {
tokio::task::yield_now().await; tokio::task::yield_now().await;
}
events.lock().push("B1");
sync.fetch_add(1, Ordering::SeqCst);
10
} }
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<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[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 let b2 = dag
.add_task(task_fn({ .add_task(B2Task {
let sync = sync_point.clone(); events: events.clone(),
let events = events.clone(); sync: sync_point.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
}
}
}))
.depends_on(b1); .depends_on(b1);
// Merge // Merge
let merge = dag 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)); .depends_on((&a2, &b2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?; dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
-272
View File
@@ -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<i32>
let vec_i32 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] }));
// Task returning Vec<String>
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<i32>
let opt_task = dag.add_task(task_fn(|_: ()| async { Some(42) }));
// Task returning Result<i32, String>
let result_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(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<i32>| 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::<i32> }));
let transform2 = dag
.add_task(task_fn(
|opt: Option<i32>| 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);
}
-72
View File
@@ -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<T> 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);
}
+71 -3
View File
@@ -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; use dagx::*;
mod handles;
#[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<T> 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::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source);
let triple = dag
.add_task(task_fn::<i32, _, _>(|&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);
}