Update tests, examples, benches

This commit is contained in:
TechnoPorg committed 2026-02-11 22:01:50 +01:00
1 parent 0137296fb4
commit 86787b3a0d
71 files changed
+1522 -3278

No files matched your search

+21 -21
View File
@@ -12,7 +12,7 @@ pub fn bench_dag_execution(c: &mut Criterion) {
rt.block_on(async {
let dag = DagRunner::new();
for i in 0..10 {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -26,7 +26,7 @@ pub fn bench_dag_execution(c: &mut Criterion) {
rt.block_on(async {
let dag = DagRunner::new();
for i in 0..100 {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -51,61 +51,61 @@ pub fn bench_dag_execution(c: &mut Criterion) {
let t0 = dag.add_task(Value(0));
let t1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| { x + 1 }))
.depends_on(t0);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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 {
let dag = DagRunner::new();
for i in 0..size {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
+3 -3
View File
@@ -18,14 +18,14 @@ pub fn bench_deep_chain(c: &mut Criterion) {
let dag = DagRunner::new();
let first: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 0 })).into();
let first: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| { 0 })).into();
let mut prev = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| { x + 1 }))
.depends_on(first);
for i in 2..100 {
prev = dag
.add_task(task_fn(move |x: i32| async move { x + i }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| { x + i }))
.depends_on(prev);
}
+4 -4
View File
@@ -18,15 +18,15 @@ pub fn bench_diamond_pattern(c: &mut Criterion) {
let dag = DagRunner::new();
let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
let b = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(a);
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);
let d = dag
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
.depends_on((&b, &c));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+15 -12
View File
@@ -18,34 +18,34 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
let dag = DagRunner::new();
// Extract from 3 sources
let source1 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
let source2 = dag.add_task(task_fn(|_: ()| async { vec![6, 7, 8, 9, 10] }));
let source3 = dag.add_task(task_fn(|_: ()| async { vec![11, 12, 13, 14, 15] }));
let source1 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
let source2 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![6, 7, 8, 9, 10]));
let source3 = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![11, 12, 13, 14, 15]));
// Transform each source
let transform1 = dag
.add_task(task_fn(|data: Vec<i32>| async move {
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>()
}))
.depends_on(source1);
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<_>>()
}))
.depends_on(source2);
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<_>>()
}))
.depends_on(source3);
// Merge all transformed data
let merge = dag
.add_task(task_fn(
|(d1, d2, d3): (Vec<i32>, Vec<i32>, Vec<i32>)| async move {
let mut result = d1;
.add_task(task_fn::<(Vec<_>, Vec<_>, Vec<_>), _, _>(
|(d1, d2, d3): (&Vec<i32>, &Vec<i32>, &Vec<i32>)| {
let mut result = d1.clone();
result.extend(d2);
result.extend(d3);
result
@@ -55,14 +55,17 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// Validate
let validate = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().filter(|&x| x > 0).collect::<Vec<_>>()
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.iter()
.copied()
.filter(|&x| x > 0)
.collect::<Vec<i32>>()
}))
.depends_on(merge);
// Load (compute sum)
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>()
}))
.depends_on(validate);
+1 -1
View File
@@ -19,7 +19,7 @@ pub fn bench_large_scale(c: &mut Criterion) {
let dag = DagRunner::new();
for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i * 2 }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i * 2 }));
}
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+5 -5
View File
@@ -17,18 +17,18 @@ pub fn bench_linear_pipeline(c: &mut Criterion) {
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 }));
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| { 1 }));
let b = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| { x + 1 }))
.depends_on(a);
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);
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);
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);
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};
let dag = DagRunner::new();
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| { 42 })).into();
// Create 100 tasks that all depend on source
for i in 0..100 {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| { x + i }))
.depends_on(source);
}
+3 -3
View File
@@ -14,7 +14,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
b.iter(|| {
let dag = DagRunner::new();
for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
});
});
@@ -25,7 +25,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
rt.block_on(async {
let dag = DagRunner::new();
for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -39,7 +39,7 @@ pub fn bench_10k_breakdown(c: &mut Criterion) {
// Pre-build the DAG outside the measurement
let dag = DagRunner::new();
for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| { i }));
}
b.iter(|| {
+4 -4
View File
@@ -14,16 +14,16 @@ pub fn bench_diamond(c: &mut Criterion) {
for i in 0..50 {
let source: TaskHandle<_> =
dag.add_task(task_fn(move |_: ()| async move { i })).into();
dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source);
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);
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));
}
+7 -6
View File
@@ -12,10 +12,10 @@ pub fn bench_fanout(c: &mut Criterion) {
b.iter(|| {
rt.block_on(async {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
for i in 0..100 {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source);
}
@@ -32,14 +32,14 @@ pub fn bench_fanout(c: &mut Criterion) {
rt.block_on(async {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
// Create 1000 strings - framework wraps in Arc automatically
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
}))
.into();
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
data.iter().filter(|s| s.contains(&i.to_string())).count()
}))
@@ -63,11 +63,12 @@ pub fn bench_fanout(c: &mut Criterion) {
// Source produces a simple Copy type (usize)
// Framework wraps in Arc<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
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);
}
+5 -3
View File
@@ -21,7 +21,7 @@ pub fn bench_config_broadcast(c: &mut Criterion) {
// Configuration source - framework wraps HashMap in Arc automatically
let config: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
HashMap::from([
("max_connections".to_string(), "100".to_string()),
("timeout_ms".to_string(), "5000".to_string()),
@@ -37,7 +37,8 @@ pub fn bench_config_broadcast(c: &mut Criterion) {
// Workers receive and process based on shared config
for worker_id in 0..workers {
dag.add_task(task_fn(move |cfg: HashMap<String, String>| async move {
dag.add_task(task_fn::<HashMap<_, _>, _, _>(
move |cfg: &HashMap<String, String>| {
// Simulate work based on configuration
let batch_size: usize = cfg
.get("batch_size")
@@ -50,7 +51,8 @@ pub fn bench_config_broadcast(c: &mut Criterion) {
// Simulate some computation
black_box(worker_id * batch_size + timeout / 100)
}))
},
))
.depends_on(config);
}
+9 -7
View File
@@ -21,7 +21,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
let dag = DagRunner::new();
// Extract: Load JSON-like data (framework wraps in Arc automatically)
let extract: TaskHandle<_> = dag.add_task(task_fn(move |_: ()| async move {
let extract: TaskHandle<_> = dag
.add_task(task_fn::<(), _, _>(move |_: ()| {
(0..count)
.map(|i| {
format!(
@@ -32,13 +33,14 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
)
})
.collect::<Vec<_>>()
})).into();
}))
.into();
// Transform: Three parallel transformations
// T1: Parse and validate
let validate = dag
.add_task(task_fn(move |data: Vec<String>| async move {
.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter()
.filter(|s| {
s.contains("\"id\"")
@@ -51,7 +53,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// T2: Extract scores
let scores = dag
.add_task(task_fn(move |data: Vec<String>| async move {
.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter()
.filter_map(|s| {
s.split("\"score\":").nth(1).and_then(|part| {
@@ -64,7 +66,7 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
// T3: Extract names
let names = dag
.add_task(task_fn(move |data: Vec<String>| async move {
.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<String>| {
data.iter()
.filter_map(|s| {
s.split("\"name\":\"")
@@ -77,8 +79,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) {
.depends_on(extract);
// Load: Aggregate all transformations
dag.add_task(task_fn(
move |(valid, scores, names): (usize, Vec<i32>, Vec<String>)| async move {
dag.add_task(task_fn::<(usize, Vec<_>, Vec<_>), _, _>(
move |(valid, scores, names): (&usize, &Vec<i32>, &Vec<String>)| {
let avg_score =
scores.iter().sum::<i32>() / scores.len().max(1) as i32;
format!(
+2 -2
View File
@@ -20,7 +20,7 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag
.add_task(task_fn(move |_: ()| async move {
.add_task(task_fn::<(), _, _>(move |_: ()| {
// Create MB of data - framework wraps in Arc automatically
vec![0u8; mb * 1024 * 1024]
}))
@@ -28,7 +28,7 @@ pub fn bench_memory_efficiency(c: &mut Criterion) {
// 20 consumers share the data via automatic Arc wrapping
for i in 0..20 {
dag.add_task(task_fn(move |data: Vec<u8>| async move {
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<u8>| {
// Process a small portion (data extracted from Arc)
let sample_sum: u32 = data
.iter()
+9 -7
View File
@@ -14,7 +14,7 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
// Stage 1: Load data (framework wraps in Arc automatically)
let data: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
(0..1000)
.map(|i| format!("Record-{:04}", i))
.collect::<Vec<_>>()
@@ -23,31 +23,33 @@ pub fn bench_mixed_patterns(c: &mut Criterion) {
// Stage 2: Fan-out for parallel analysis
let analysis1 = dag
.add_task(task_fn(|d: Vec<String>| async move {
.add_task(task_fn::<Vec<_>, _, _>(|d: &Vec<String>| {
d.iter().filter(|s| s.contains("00")).count()
}))
.depends_on(data);
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>()
}))
.depends_on(data);
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);
// Stage 3: Fan-in to aggregate
let summary = dag
.add_task(task_fn(|(a1, a2, a3): (usize, usize, usize)| async move {
.add_task(task_fn::<(usize, usize, usize), _, _>(
|(a1, a2, a3): (&usize, &usize, &usize)| {
format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3)
}))
},
))
.depends_on((&analysis1, &analysis2, &analysis3));
// Stage 4: Fan-out final report to multiple destinations
for i in 0..3 {
dag.add_task(task_fn(move |report: String| async move {
dag.add_task(task_fn::<String, _, _>(move |report: &String| {
format!("Destination {}: {}", i, report)
}))
.depends_on(summary);
+5 -5
View File
@@ -52,7 +52,7 @@
//! This proves dagx runs tasks on multiple CPU cores simultaneously.
//! ```
use dagx::{task, DagResult, DagRunner, Task};
use dagx::{task, DagResult, DagRunner, Task, TaskInput};
use futures::FutureExt;
use std::time::{Duration, Instant};
use tokio::time::sleep;
@@ -152,10 +152,10 @@ async fn main() -> DagResult<()> {
let task3 = ComputeSum::new(501, 751, WORK_DELAY);
let task4 = ComputeSum::new(751, 1001, WORK_DELAY);
let r1 = task1.run(()).await;
let r2 = task2.run(()).await;
let r3 = task3.run(()).await;
let r4 = task4.run(()).await;
let r1 = task1.run(TaskInput::empty()).await;
let r2 = task2.run(TaskInput::empty()).await;
let r3 = task3.run(TaskInput::empty()).await;
let r4 = task4.run(TaskInput::empty()).await;
let result = r1 + r2 + r3 + r4;
let elapsed = start.elapsed();
+1 -1
View File
@@ -446,7 +446,7 @@ async fn main() {
.unwrap();
// Check results
match dag.get(payment).unwrap() {
match dag.get(payment).unwrap().as_ref() {
Ok(_) => println!("\nPayment validation succeeded"),
Err(trace) => {
println!("\n[{}] Payment validation failed:", correlation_id);
+3 -3
View File
@@ -227,7 +227,7 @@ async fn main() {
.await
.unwrap();
match dag.get(logged).unwrap() {
match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e),
}
@@ -253,7 +253,7 @@ async fn main() {
.await
.unwrap();
match dag.get(logged).unwrap() {
match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e),
}
@@ -279,7 +279,7 @@ async fn main() {
.await
.unwrap();
match dag.get(logged).unwrap() {
match dag.get(logged).unwrap().as_ref() {
Ok(result) => println!("Final result: {}\n", result),
Err(e) => eprintln!("Final error: {}\n", e),
}
-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 ===");
}
+6 -19
View File
@@ -1,6 +1,5 @@
//! Unit tests for builder module
use crate::builder::IsUnitType;
use crate::runner::DagRunner;
use crate::types::TaskHandle;
@@ -52,24 +51,12 @@ fn test_task_builder_from_conversion() {
let builder = dag.add_task(TestTask { value: 42 });
// Convert builder to TaskHandle
let handle: TaskHandle<i32> = builder.into();
let handle: TaskHandle<_> = builder.into();
// Check that the ID is preserved
assert_eq!(handle.id.0, 0); // First task should have ID 0
}
#[test]
fn test_is_unit_type_trait() {
// Test that IsUnitType is correctly implemented for ()
fn assert_is_unit_type<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]
fn test_task_builder_depends_on_returns_handle() {
init_tracing();
@@ -79,7 +66,7 @@ fn test_task_builder_depends_on_returns_handle() {
let dependent = dag.add_task(TestTaskWithInput).depends_on(source);
// depends_on should return a TaskHandle
let _handle: TaskHandle<i32> = dependent;
let _handle: TaskHandle<_> = dependent;
}
#[test]
@@ -92,9 +79,9 @@ fn test_task_builder_chain() {
let t3 = dag.add_task(TestTaskWithInput).depends_on(t2);
// All should return TaskHandles
let _: TaskHandle<i32> = t1;
let _: TaskHandle<i32> = t2;
let _: TaskHandle<i32> = t3;
let _: TaskHandle<_> = t1;
let _: TaskHandle<_> = t2;
let _: TaskHandle<_> = t3;
}
#[test]
@@ -118,7 +105,7 @@ fn test_multiple_dependencies() {
let sum = dag.add_task(AddTask).depends_on((a, b));
// Should return a TaskHandle
let _: TaskHandle<i32> = sum;
let _: TaskHandle<_> = sum;
}
#[test]
+1 -1
View File
@@ -207,7 +207,7 @@ fn test_deps_handle_ref_explicit() {
// Create a reference and explicitly call to_node_ids
let handle_ref = &handle;
let node_ids = DepsTuple::<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[0], NodeId(77));
-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 -23
View File
@@ -3,7 +3,6 @@
use std::sync::Arc;
use crate::node::{ExecutableNode, TypedNode};
use crate::types::NodeId;
// Simple test task
struct TestTask {
@@ -26,29 +25,11 @@ impl TaskWithDependency {
}
}
#[tokio::test]
async fn test_execute_with_deps_missing_input() {
// Test when task expects input but doesn't receive it
let node = Box::new(TypedNode::new(NodeId(0), TaskWithDependency));
// TaskWithDependency expects an i32 input, but we provide no dependencies
let dependencies = vec![];
let result = node.execute_with_deps(dependencies).await;
assert!(result.is_err());
if let Err(crate::error::DagError::InvalidDependency { task_id }) = result {
assert_eq!(task_id, 0);
} else {
panic!("Expected InvalidDependency error, got: {:?}", result);
}
}
#[tokio::test]
async fn test_execute_with_deps_success() {
// Test successful execution with channels
let dependency_node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 21 }));
let dependent_node = Box::new(TypedNode::new(NodeId(1), TaskWithDependency));
let dependency_node = Box::new(TypedNode::new(TestTask { value: 21 }));
let dependent_node = Box::new(TypedNode::new(TaskWithDependency));
// Execute dependency node with sender
let dep_dependencies = vec![];
@@ -71,7 +52,7 @@ async fn test_execute_with_deps_success() {
#[tokio::test]
async fn test_execute_with_deps_sink_node() {
// Test that sink nodes (no dependents) return their output
let node = Box::new(TypedNode::new(NodeId(0), TestTask { value: 42 }));
let node = Box::new(TypedNode::new(TestTask { value: 42 }));
// No senders means this is a sink node
let dependencies = vec![];
@@ -88,7 +69,7 @@ async fn test_execute_with_deps_sink_node() {
#[tokio::test]
async fn test_execute_with_deps_non_sink_node() {
// Test that non-sink nodes send output via channels
let node = TypedNode::new(NodeId(0), TestTask { value: 42 });
let node = TypedNode::new(TestTask { value: 42 });
// Execute with senders (non-sink node)
let node = Box::new(node);
-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);
}
#[tokio::test]
async fn test_multiple_get_calls() {
init_tracing();
// Test that get() can be called multiple times for the same handle
let dag = DagRunner::new();
let handle: TaskHandle<_> = dag.add_task(TestTask { value: 100 }).into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
.unwrap();
// Multiple get calls should all return the same value
let result1 = dag.get(handle).unwrap();
let result2 = dag.get(handle).unwrap();
let result3 = dag.get(handle).unwrap();
assert_eq!(result1, 100);
assert_eq!(result2, 100);
assert_eq!(result3, 100);
}
#[tokio::test]
async fn test_invalid_node_id_in_get() {
init_tracing();
+84 -43
View File
@@ -1,6 +1,7 @@
//! Unit tests for task module
use crate::task::{task_fn, Task};
use crate::task::{task_fn, Task, TaskInput};
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
@@ -11,50 +12,74 @@ async fn test_task_fn_creation_and_execution() {
// Test line 111 in task.rs - Task implementation for TaskFn
// Create a simple task function
let task = task_fn(|input: i32| async move { input * 2 });
let task = task_fn::<i32, _, _>(|input: &i32| input * 2);
// Test the Task trait implementation
let result = task.run(42).await;
let result = task
.run(TaskInput::new(
vec![Arc::new(42) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 84);
}
#[tokio::test]
async fn test_task_fn_with_unit_input() {
// Test task_fn with unit input
let task = task_fn(|_: ()| async { 100 });
let task = task_fn::<(), _, _>(|_: ()| 100);
let result = task.run(()).await;
let result = task
.run(TaskInput::new(
vec![Arc::new(()) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 100);
}
#[tokio::test]
async fn test_task_fn_with_tuple_input() {
// Test task_fn with tuple input
let task = task_fn(|(a, b): (i32, i32)| async move { a + b });
let task = task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b);
let result = task.run((10, 20)).await;
let result = task
.run(TaskInput::new(
vec![
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);
}
#[tokio::test]
async fn 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| {
let len = input.len();
async move { len }
len
});
let test_string = String::from("hello");
let result = task.run(test_string).await;
let result = task
.run(TaskInput::new(
vec![Arc::new(test_string) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 5);
}
#[tokio::test]
async fn test_task_fn_with_complex_output() {
// Test task_fn with complex output type
let task = task_fn(|input: i32| async move { vec![input, input * 2, input * 3] });
let task = task_fn::<i32, _, _>(|&input: &i32| vec![input, input * 2, input * 3]);
let result = task.run(5).await;
let result = task
.run(TaskInput::new(
vec![Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, vec![5, 10, 15]);
}
@@ -62,9 +87,13 @@ async fn test_task_fn_with_complex_output() {
async fn test_task_fn_stateful_closure() {
// Test that task_fn can capture state
let multiplier = 3;
let task = task_fn(move |input: i32| async move { input * multiplier });
let task = task_fn::<i32, _, _>(move |input: &i32| input * multiplier);
let result = task.run(7).await;
let result = task
.run(TaskInput::new(
vec![Arc::new(7) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 21);
}
@@ -74,34 +103,30 @@ struct CustomTask {
}
impl Task for CustomTask {
type Input = i32;
type Input = (i32,);
type Output = i32;
#[allow(refining_impl_trait)]
fn run(mut self, input: Self::Input) -> Pin<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 {
self.state += input;
self.state += input.next().0;
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]
async fn test_custom_task_implementation() {
let task = CustomTask { state: 10 };
let result1 = task.run(5).await;
let result1 = task
.run(TaskInput::new(
vec![Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result1, 15);
// Can't call run again since task was consumed
@@ -131,15 +156,21 @@ fn test_task_trait_bounds() {
#[tokio::test]
async fn test_task_fn_single_call() {
// Test that task_fn works (can only call once due to ownership)
let task = task_fn(|x: i32| async move { x + 1 });
let task = task_fn::<i32, _, _>(|x: &i32| x + 1);
assert_eq!(task.run(1).await, 2);
assert_eq!(
task.run(TaskInput::new(
vec![Arc::new(1) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
2
);
}
#[tokio::test]
async fn test_task_fn_with_result_type() {
// Test task_fn returning Result
let task = task_fn(|x: i32| async move {
let task = task_fn::<i32, _, _>(|&x: &i32| {
if x > 0 {
Ok(x * 2)
} else {
@@ -147,30 +178,40 @@ async fn test_task_fn_with_result_type() {
}
});
assert_eq!(task.run(5).await, Ok(10));
assert_eq!(
task.run(TaskInput::new(
vec![Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Ok(10)
);
}
#[tokio::test]
async fn test_task_fn_with_option_type() {
// Test task_fn returning Option
let task = task_fn(|x: i32| async move {
if x > 0 {
Some(x * 2)
} else {
None
}
});
let task = task_fn::<i32, _, _>(|&x: &i32| if x > 0 { Some(x * 2) } else { None });
assert_eq!(task.run(5).await, Some(10));
assert_eq!(
task.run(TaskInput::new(
vec![Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Some(10)
);
}
#[tokio::test]
async fn test_task_fn_explicit_trait_call() {
// Test explicit Task trait method call to cover line 121
let task = task_fn(|x: i32| async move { x * 3 });
let task = task_fn::<i32, _, _>(|x: &i32| x * 3);
// Explicitly call the Task trait's run method
let result = Task::run(task, 10).await;
let result = Task::run(
task,
TaskInput::new(vec![Arc::new(10) as Arc<dyn Any + Send + Sync + 'static>].iter()),
)
.await;
assert_eq!(result, 30);
}
+7 -15
View File
@@ -29,7 +29,7 @@ async fn test_unit_type_taskbuilder_to_handle_conversion() {
// Explicitly convert to TaskHandle using From implementation
// This triggers the From<TaskBuilder> implementation at line 69-74,
// specifically covering line 71
let handle: TaskHandle<i32> = builder.into();
let handle: TaskHandle<_> = builder.into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -76,7 +76,7 @@ async fn test_unit_type_conversion_with_multiple_tasks() {
// Convert each builder to handle explicitly
let first_handle: TaskHandle<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();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -116,7 +116,7 @@ async fn test_unit_struct_with_state() {
let builder = dag.add_task(StatefulTask::new(21));
// Explicit conversion - triggers line 71
let handle: TaskHandle<i32> = builder.into();
let handle: TaskHandle<_> = builder.into();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -143,7 +143,6 @@ async fn test_taskbuilder_conversion_assignment() {
// Conversion through type annotation and assignment
// This also uses From<TaskBuilder> implementation
let handle: TaskHandle<Vec<i32>> = builder.into();
let id = handle.id;
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -151,13 +150,6 @@ async fn test_taskbuilder_conversion_assignment() {
let result = dag.get(handle).unwrap();
assert_eq!(result, vec![1, 2, 3]);
// Verify the ID was preserved correctly through conversion
let another_handle = TaskHandle::<Vec<i32>> {
id,
_phantom: std::marker::PhantomData,
};
assert_eq!(dag.get(another_handle).unwrap(), vec![1, 2, 3]);
}
#[tokio::test]
@@ -184,7 +176,7 @@ async fn test_mixed_unit_and_dependent_tasks() {
// Source is unit-type, can be converted
let source_builder = dag.add_task(Source);
let source_handle: TaskHandle<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
let doubler = dag.add_task(Doubler).depends_on(source_handle);
@@ -260,9 +252,9 @@ fn test_multiple_conversions_preserve_ids() {
let id3 = builder3.id;
// Convert all builders
let handle1: TaskHandle<i32> = builder1.into();
let handle2: TaskHandle<i32> = builder2.into();
let handle3: TaskHandle<i32> = builder3.into();
let handle1: TaskHandle<_> = builder1.into();
let handle2: TaskHandle<_> = builder2.into();
let handle3: TaskHandle<_> = builder3.into();
// Verify IDs are preserved and sequential
assert_eq!(handle1.id, id1);
+2 -2
View File
@@ -40,7 +40,7 @@ fn test_node_id_operations() {
#[test]
fn test_task_handle_clone() {
let handle: TaskHandle<i32> = TaskHandle {
let handle: TaskHandle<(i32,)> = TaskHandle {
id: NodeId(42),
_phantom: PhantomData,
};
@@ -129,7 +129,7 @@ fn test_task_builder_from_conversion() {
let builder_id = builder.id;
// This conversion uses the From implementation
let handle: TaskHandle<i32> = builder.into();
let handle: TaskHandle<_> = builder.into();
assert_eq!(handle.id, builder_id);
}
+28 -30
View File
@@ -11,12 +11,12 @@ async fn test_1000_level_deep_chain() -> DagResult<()> {
// Test a linear chain of 1000 tasks
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 0 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let mut current = first.into(); // Convert to TaskHandle
for _i in 1..1000 {
current = dag
.add_task(task_fn(move |prev: i32| async move { prev + 1 }))
.add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
.depends_on(current);
}
@@ -31,20 +31,20 @@ async fn test_deep_chain_with_branches() -> DagResult<()> {
// Test a deep chain where each node has a side branch
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 0 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let mut main_chain: dagx::TaskHandle<i32> = first.into();
let mut branches = Vec::new();
for i in 1..500 {
// Main chain continues
main_chain = dag
.add_task(task_fn(move |prev: i32| async move { prev + 1 }))
.add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
.depends_on(main_chain);
// Create a branch at every 10th node
if i % 10 == 0 {
let branch = dag
.add_task(task_fn(move |main_val: i32| async move { main_val * 2 }))
.add_task(task_fn::<i32, _, _>(move |main_val: &i32| main_val * 2))
.depends_on(main_chain);
branches.push(branch);
}
@@ -71,12 +71,12 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
let mut chain_ends = Vec::new();
for chain_id in 0..8 {
let first = dag.add_task(task_fn(move |_: ()| async move { chain_id * 1000 }));
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| chain_id * 1000));
let mut current: dagx::TaskHandle<i32> = first.into();
for _ in 0..500 {
current = dag
.add_task(task_fn(|prev: i32| async move { prev + 1 }))
.add_task(task_fn::<i32, _, _>(|&prev: &i32| prev + 1))
.depends_on(current);
}
@@ -85,8 +85,8 @@ async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
// Add a final task that depends on all chains
let final_task = dag
.add_task(task_fn(
|inputs: (i32, i32, i32, i32, i32, i32, i32, i32)| async move {
.add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32, i32), _, _>(
|inputs: (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
inputs.0
+ inputs.1
+ inputs.2
@@ -126,29 +126,25 @@ async fn test_deep_chain_execution_order() -> DagResult<()> {
let dag = DagRunner::new();
let order = Arc::new(AtomicUsize::new(0));
let first = dag.add_task(task_fn({
let first = dag.add_task(task_fn::<(), _, _>({
let order = order.clone();
move |_: ()| {
let order = order.clone();
async move {
assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0);
0
}
}
}));
let mut current: dagx::TaskHandle<i32> = first.into();
for expected in 1..100 {
current = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let order = order.clone();
move |prev: i32| {
move |&prev: &i32| {
let order = order.clone();
async move {
assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected);
prev + 1
}
}
}))
.depends_on(current);
}
@@ -168,13 +164,15 @@ async fn test_deep_binary_tree() -> DagResult<()> {
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
if depth == 0 {
let task = dag.add_task(task_fn(move |_: ()| async move { value }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
task.into() // Convert TaskBuilder to TaskHandle
} else {
let left = build_tree(dag, depth - 1, value * 2);
let right = build_tree(dag, depth - 1, value * 2 + 1);
dag.add_task(task_fn(move |(l, r): (i32, i32)| async move { l + r }))
dag.add_task(task_fn::<(i32, i32), _, _>(move |(l, r): (&i32, &i32)| {
l + r
}))
.depends_on((&left, &right))
}
}
@@ -196,15 +194,15 @@ async fn test_fibonacci_chain() -> DagResult<()> {
// Test a Fibonacci-like dependency pattern
let dag = DagRunner::new();
let f0_builder = dag.add_task(task_fn(|_: ()| async { 0i64 }));
let f1_builder = dag.add_task(task_fn(|_: ()| async { 1i64 }));
let f0_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
let f1_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 1i64));
let mut prev2: dagx::TaskHandle<i64> = f0_builder.into();
let mut prev1: dagx::TaskHandle<i64> = f1_builder.into();
for _ in 2..50 {
let next = dag
.add_task(task_fn(|(a, b): (i64, i64)| async move { a + b }))
.add_task(task_fn::<(i64, i64), _, _>(|(a, b): (&i64, &i64)| a + b))
.depends_on((&prev2, &prev1));
prev2 = prev1;
@@ -225,12 +223,12 @@ async fn test_deep_chain_with_accumulator() -> DagResult<()> {
let dag = DagRunner::new();
// Use a simpler accumulation pattern - just sum
let first = dag.add_task(task_fn(|_: ()| async { 0i64 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64));
let mut current: dagx::TaskHandle<i64> = first.into();
for i in 1..=200 {
current = dag
.add_task(task_fn(move |sum: i64| async move { sum + i as i64 }))
.add_task(task_fn::<i64, _, _>(move |sum: &i64| sum + i as i64))
.depends_on(current);
}
@@ -247,8 +245,8 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
// Test a zigzag pattern: two chains that cross-reference each other
let dag = DagRunner::new();
let a_first = dag.add_task(task_fn(|_: ()| async { 1 }));
let b_first = dag.add_task(task_fn(|_: ()| async { 2 }));
let a_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b_first = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let mut chain_a: dagx::TaskHandle<i32> = a_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 {
// A depends on B
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);
} else {
// B depends on A
chain_b = dag
.add_task(task_fn(move |a_val: i32| async move { a_val + 2 }))
.add_task(task_fn::<i32, _, _>(move |a_val: &i32| a_val + 2))
.depends_on(chain_a);
}
}
// Final task depends on both chains
let final_task = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&chain_a, &chain_b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -286,12 +284,12 @@ async fn test_10000_level_chain_stress() -> DagResult<()> {
// Ultimate deep chain test
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 0u64 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0u64));
let mut current: dagx::TaskHandle<u64> = first.into();
for _ in 1..10_000 {
current = dag
.add_task(task_fn(|prev: u64| async move { prev + 1 }))
.add_task(task_fn::<u64, _, _>(|prev: &u64| prev + 1))
.depends_on(current);
}
+33 -38
View File
@@ -16,12 +16,9 @@ async fn test_10000_independent_tasks() -> DagResult<()> {
let mut tasks: Vec<_> = (0..10_000)
.map(|i| {
let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| {
let counter = counter.clone();
async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
counter.fetch_add(1, Ordering::Relaxed);
i
}
}))
})
.collect();
@@ -48,13 +45,15 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
// Bottom layer: 2048 nodes - convert to TaskHandles
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
let task = dag.add_task(task_fn::<(), _, _>(
move |_: ()| {
if i < 1024 {
1
} else {
0
}
}));
},
));
task.into()
})
.collect();
@@ -74,20 +73,20 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
let task = match deps.len() {
1 => dag
.add_task(task_fn(|a: i32| async move { a }))
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
.depends_on(deps[0]),
2 => dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((deps[0], deps[1])),
3 => dag
.add_task(task_fn(
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
))
.depends_on((deps[0], deps[1], deps[2])),
4 => dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move {
a + b + c + d
}))
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
))
.depends_on((deps[0], deps[1], deps[2], deps[3])),
_ => unreachable!(),
};
@@ -98,7 +97,7 @@ async fn test_5000_node_pyramid() -> DagResult<()> {
// Final layer combines the last 2
let root = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -119,13 +118,15 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
// Bottom layer: 2048 nodes - convert to TaskHandles
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
let task = dag.add_task(task_fn::<(), _, _>(
move |_: ()| {
if i < 1024 {
1
} else {
0
}
}));
},
));
task.into()
})
.collect();
@@ -145,20 +146,20 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
let task = match deps.len() {
1 => dag
.add_task(task_fn(|a: i32| async move { a }))
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
.depends_on(deps[0]),
2 => dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((deps[0], deps[1])),
3 => dag
.add_task(task_fn(
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
))
.depends_on((deps[0], deps[1], deps[2])),
4 => dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move {
a + b + c + d
}))
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
))
.depends_on((deps[0], deps[1], deps[2], deps[3])),
_ => unreachable!(),
};
@@ -169,7 +170,7 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
// Final layer combines the last 2
let root = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&current_layer[0], &current_layer[1]));
dag.run(|fut| fut).await?;
@@ -189,7 +190,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
// Create 1000 source tasks
let sources: Vec<_> = (0..1000)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move { i % 10 }))
dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10))
.into()
})
.collect();
@@ -201,9 +202,9 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
let idx2 = (i * 13) % 1000;
let idx3 = (i * 17) % 1000;
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move {
a + b + c + i as i32
}))
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32,
))
.depends_on((&sources[idx1], &sources[idx2], &sources[idx3]))
})
.collect();
@@ -231,29 +232,25 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
let mut chain_ends = Vec::new();
for chain_id in 0..100 {
let first = dag.add_task(task_fn({
let first = dag.add_task(task_fn::<(), _, _>({
let counter = counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::Relaxed);
chain_id * 100
}
}
}));
let mut current: dagx::TaskHandle<i32> = first.into();
for _i in 1..100 {
current = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let counter = counter.clone();
move |prev: i32| {
move |&prev: &i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::Relaxed);
prev + 1
}
}
}))
.depends_on(current);
}
@@ -291,12 +288,10 @@ async fn test_100000_nodes_stress() -> DagResult<()> {
.map(|i| {
let completed = completed.clone();
let task_id = batch * batch_size + i;
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let completed = completed.clone();
async move {
completed.fetch_add(1, Ordering::Relaxed);
task_id
}
}))
})
.collect();
+60 -91
View File
@@ -19,21 +19,19 @@ async fn test_zero_sized_types_throughout() -> DagResult<()> {
let dag = DagRunner::new();
// Task producing ZST
let t1 = dag.add_task(task_fn(|_: ()| async { Empty }));
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Empty));
// Task that also produces ZST (independent since we can't extract custom types)
let t2: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let t2: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Task with PhantomData
let _t3 = dag.add_task(task_fn(|_: ()| async {
PhantomWrapper::<i32> {
let _t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| PhantomWrapper::<i32> {
_phantom: PhantomData,
}
}));
// Task that depends on t2 (uses primitive type)
let t4 = dag
.add_task(task_fn(|val: i32| async move {
.add_task(task_fn::<i32, _, _>(|&val: &i32| {
assert_eq!(val, 42);
"success"
}))
@@ -59,7 +57,7 @@ async fn test_large_value_types() -> DagResult<()> {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async {
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
let large = LargeStruct {
data: [42; 8192],
more_data: vec![1; 1000],
@@ -69,7 +67,7 @@ async fn test_large_value_types() -> DagResult<()> {
}));
let t2 = dag
.add_task(task_fn(|val: usize| async move {
.add_task(task_fn::<usize, _, _>(|&val: &usize| {
val // Just pass through to verify
}))
.depends_on(t1);
@@ -86,12 +84,16 @@ async fn test_unit_type_chains() -> DagResult<()> {
// Test chains of unit type operations
let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async {})).into();
let t2 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t1);
let t3 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t2);
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| {})).into();
let t2 = dag
.add_task(task_fn::<(), _, _>(|_: ()| {}))
.depends_on((t1,));
let t3 = dag
.add_task(task_fn::<(), _, _>(|_: ()| {}))
.depends_on((t2,));
let t4 = dag
.add_task(task_fn(|_: ()| async { "done" }))
.depends_on(t3);
.add_task(task_fn::<(), _, _>(|_: ()| "done"))
.depends_on((t3,));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -103,82 +105,43 @@ async fn test_unit_type_chains() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_nested_option_result_types() -> DagResult<()> {
// Test deeply nested generic types
type ComplexType = Result<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]
async fn test_string_types_variety() -> DagResult<()> {
// Test various string types
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { "static str" }));
let t2 = dag.add_task(task_fn(|_: ()| async { String::from("owned string") }));
let t3 = dag.add_task(task_fn(|_: ()| async {
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| "static str"));
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| String::from("owned string")));
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
std::borrow::Cow::Borrowed("cow str")
}));
let t4 = dag.add_task(task_fn(|_: ()| async { Box::new("boxed str") }));
let t5 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str 2") }));
let t6 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str") }));
let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| Box::new("boxed str")));
let t5 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
std::sync::Arc::new("arc str 2")
}));
let t6 = dag.add_task(task_fn::<(), _, _>(|_: ()| std::sync::Arc::new("arc str")));
let combined = dag
.add_task(task_fn(
|(s1, s2, s3, s4, s5, s6): (
.add_task(task_fn::<
(
&str,
String,
std::borrow::Cow<str>,
Box<&str>,
std::sync::Arc<&str>,
std::sync::Arc<&str>,
)| async move { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) },
),
_,
_,
>(
|(s1, s2, s3, s4, s5, s6): (
&&str,
&String,
&std::borrow::Cow<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));
@@ -199,13 +162,17 @@ async fn test_generic_type_constraints() -> DagResult<()> {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { Wrapper(42) }));
let t2 = dag.add_task(task_fn(|_: ()| async { Wrapper("hello") }));
let t3 = dag.add_task(task_fn(|_: ()| async { Wrapper(vec![1, 2, 3]) }));
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(42)));
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper("hello")));
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(vec![1, 2, 3])));
let combined = dag
.add_task(task_fn(
|(w1, w2, w3): (Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>)| async move {
.add_task(task_fn::<
(Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>),
_,
_,
>(
|(w1, w2, w3): (&Wrapper<i32>, &Wrapper<&str>, &Wrapper<Vec<i32>>)| {
format!("{:?} {:?} {:?}", w1, w2, w3)
},
))
@@ -226,7 +193,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async {
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
let arc = Arc::new(Mutex::new(100));
// Extract value instead of passing Arc
let guard = arc.lock().unwrap();
@@ -234,7 +201,7 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
}));
let t2 = dag
.add_task(task_fn(|val: i32| async move { val * 2 }))
.add_task(task_fn::<i32, _, _>(|&val: &i32| val * 2))
.depends_on(t1);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -249,17 +216,19 @@ async fn test_array_types() -> DagResult<()> {
// Test with fixed-size arrays
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { [1, 2, 3, 4, 5] }));
let t2 = dag.add_task(task_fn(|_: ()| async { [6, 7, 8, 9, 10] }));
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| [1, 2, 3, 4, 5]));
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| [6, 7, 8, 9, 10]));
let t3 = dag
.add_task(task_fn(|(a1, a2): ([i32; 5], [i32; 5])| async move {
.add_task(task_fn::<([i32; 5], [i32; 5]), _, _>(
|(a1, a2): (&[i32; 5], &[i32; 5])| {
let mut sum = 0;
for i in 0..5 {
sum += a1[i] + a2[i];
}
sum
}))
},
))
.depends_on((t1, t2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -283,16 +252,16 @@ async fn test_enum_variants() -> DagResult<()> {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { MyEnum::Unit }));
let t2 = dag.add_task(task_fn(|_: ()| async {
let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Unit));
let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
MyEnum::Tuple(42, "test".to_string())
}));
let t3 = dag.add_task(task_fn(|_: ()| async { MyEnum::Struct { x: 10, y: 20 } }));
let t4 = dag.add_task(task_fn(|_: ()| async { MyEnum::LargeVariant([0; 1024]) }));
let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 }));
let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024])));
let combined = dag
.add_task(task_fn(
|(e1, e2, e3, e4): (MyEnum, MyEnum, MyEnum, MyEnum)| async move {
.add_task(task_fn::<(MyEnum, MyEnum, MyEnum, MyEnum), _, _>(
|(e1, e2, e3, e4): (&MyEnum, &MyEnum, &MyEnum, &MyEnum)| {
matches!(e1, MyEnum::Unit)
&& matches!(e2, MyEnum::Tuple(42, _))
&& matches!(e3, MyEnum::Struct { x: 10, y: 20 })
+17 -12
View File
@@ -5,6 +5,7 @@ use dagx::{task, DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::task::yield_now;
#[tokio::test]
async fn test_empty_dag_execution() -> DagResult<()> {
@@ -22,7 +23,7 @@ async fn test_single_task_no_deps() -> DagResult<()> {
// Test the absolute minimum: one task with no dependencies
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -38,11 +39,9 @@ async fn test_single_task_unit_input_output() -> DagResult<()> {
let executed = Arc::new(AtomicUsize::new(0));
let exec_clone = executed.clone();
let task = dag.add_task(task_fn(move |_: ()| {
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let exec = exec_clone.clone();
async move {
exec.fetch_add(1, Ordering::SeqCst);
}
}));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -60,7 +59,7 @@ async fn test_zero_sized_type_task() -> DagResult<()> {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { ZeroSized }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| ZeroSized));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -86,7 +85,7 @@ async fn test_dag_with_only_source_tasks() -> DagResult<()> {
let dag = DagRunner::new();
let tasks: Vec<_> = (0..100)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -132,7 +131,7 @@ async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> {
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
} else {
// DAG with a task
let task = dag.add_task(task_fn(move |_: ()| async move { i }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
assert_eq!(dag.get(task)?, i);
}
@@ -151,7 +150,7 @@ async fn test_dag_with_never_type_simulation() -> DagResult<()> {
let dag = DagRunner::new();
// This task completes immediately but returns a Result containing "never" type
let task = dag.add_task(task_fn(|_: ()| async { Result::<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?;
@@ -164,11 +163,17 @@ async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
// Test a task that does the absolute minimum async work
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async {
// Just yield once to the runtime
tokio::task::yield_now().await;
struct YieldTask;
#[task]
impl YieldTask {
async fn run() -> i32 {
yield_now().await;
1337
}));
}
}
let task = dag.add_task(YieldTask);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
+2 -2
View File
@@ -80,7 +80,7 @@ async fn test_custom_type_passthrough() {
let person_result = dag.get(process).unwrap();
let company_result = dag.get(transform).unwrap();
assert_eq!(person_result, "Alice is 30 years old");
assert_eq!(person_result.as_str(), "Alice is 30 years old");
assert_eq!(company_result.name, "Alice's Company");
assert_eq!(company_result.employees.len(), 1);
}
@@ -103,7 +103,7 @@ async fn test_custom_type_diamond_pattern() {
let result_a = dag.get(path_a).unwrap();
let result_b = dag.get(path_b).unwrap();
assert_eq!(result_a, "Alice is 30 years old");
assert_eq!(result_a.as_str(), "Alice is 30 years old");
assert_eq!(result_b.name, "Alice's Company");
}
+4 -4
View File
@@ -54,7 +54,7 @@ fn test_handle_is_immutable_reference() {
let builder = dag.add_task(NoInput);
// Convert to handle (only works because Input = ())
let handle: TaskHandle<i32> = builder.into();
let handle: TaskHandle<_> = builder.into();
// TaskHandle is Copy/Clone - just an ID wrapper
let _copy1 = handle;
@@ -108,7 +108,7 @@ fn test_ordering_prevents_cycles() {
// So we can only create DAGs, never cycles!
// Let's create a valid DAG instead: A→B (B depends on A)
let a_handle: TaskHandle<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);
// Now we could try: a_handle.depends_on(b_handle)?
// But TaskHandle has no such method!
@@ -138,8 +138,8 @@ fn test_unit_tasks_cannot_create_cycles() {
let task2 = dag.add_task(Task2);
// Both have unit input, so we can convert to handles
let _handle1: TaskHandle<i32> = task1.into();
let _handle2: TaskHandle<i32> = task2.into();
let _handle1: TaskHandle<_> = task1.into();
let _handle2: TaskHandle<_> = task2.into();
// Now we have two handles, but...
// TaskHandle has no methods to add dependencies!
+27 -30
View File
@@ -1,7 +1,7 @@
//! Tests for dependency resolution order and transitive dependencies
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::{Arc, Mutex};
@@ -12,72 +12,69 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
let execution_log = Arc::new(Mutex::new(Vec::new()));
// Create a chain: A -> B -> C -> D -> E
let a = {
let log = execution_log.clone();
dag.add_task(task_fn(move |_: ()| {
let log = log.clone();
async move {
log.lock().unwrap().push(("A_start", 0));
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;
log.lock().unwrap().push(("A_end", 1));
self.log.lock().unwrap().push(("A_end", 1));
1
}
}))
};
}
// Create a chain: A -> B -> C -> D -> E
let a = dag.add_task(ATask {
log: execution_log.clone(),
});
let b = {
let log = execution_log.clone();
dag.add_task(task_fn(move |x: i32| {
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let log = log.clone();
async move {
log.lock().unwrap().push(("B_start", x));
assert_eq!(x, 1, "B should receive 1 from A");
log.lock().unwrap().push(("B_end", x + 1));
x + 1
}
}))
.depends_on(a)
};
let c = {
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();
async move {
log.lock().unwrap().push(("C_start", x));
assert_eq!(x, 2, "C should receive 2 from B");
log.lock().unwrap().push(("C_end", x + 1));
x + 1
}
}))
.depends_on(b)
};
let d = {
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();
async move {
log.lock().unwrap().push(("D_start", x));
assert_eq!(x, 3, "D should receive 3 from C");
log.lock().unwrap().push(("D_end", x + 1));
x + 1
}
}))
.depends_on(c)
};
let e = {
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();
async move {
log.lock().unwrap().push(("E_start", x));
assert_eq!(x, 4, "E should receive 4 from D");
log.lock().unwrap().push(("E_end", x + 1));
x + 1
}
}))
.depends_on(d)
};
@@ -108,12 +105,12 @@ async fn test_transitive_dependencies() -> DagResult<()> {
// A -> B -> C (creates transitive A -> C relationship)
let dag = DagRunner::new();
let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
let b = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(a);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -129,12 +126,12 @@ async fn test_transitive_dependencies() -> DagResult<()> {
async fn test_deep_chain_50_levels() -> DagResult<()> {
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 0 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let mut handles = vec![first.into()];
for _i in 1..=50 {
let next = dag
.add_task(task_fn(move |x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + 1))
.depends_on(handles.last().unwrap());
handles.push(next);
}
@@ -149,12 +146,12 @@ async fn test_deep_chain_50_levels() -> DagResult<()> {
async fn test_deep_chain_200_levels() -> DagResult<()> {
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 1 }));
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let mut current = first.into();
for _ in 0..200 {
current = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(current);
}
+19 -17
View File
@@ -8,9 +8,9 @@ use futures::FutureExt;
async fn test_single_dependency() -> DagResult<()> {
let dag = DagRunner::new();
let source = dag.add_task(task_fn(|_: ()| async { 42 }));
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let dependent = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -25,21 +25,23 @@ async fn test_dependency_data_flow() -> DagResult<()> {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }))
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
.into();
let sum = dag
.add_task(task_fn(|v: Vec<i32>| async move { v.iter().sum::<i32>() }))
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
v.iter().sum::<i32>()
}))
.depends_on(source);
let product = dag
.add_task(task_fn(
|v: Vec<i32>| async move { v.iter().product::<i32>() },
))
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
v.iter().product::<i32>()
}))
.depends_on(source);
let final_result = dag
.add_task(task_fn(|(s, p): (i32, i32)| async move { s + p }))
.add_task(task_fn::<(i32, i32), _, _>(|(s, p): (&i32, &i32)| s + p))
.depends_on((&sum, &product));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -56,14 +58,14 @@ async fn test_dependencies_with_different_types() -> DagResult<()> {
// Test that dependencies with different types work correctly
let dag = DagRunner::new();
let int_source = dag.add_task(task_fn(|_: ()| async { 42 }));
let string_source = dag.add_task(task_fn(|_: ()| async { "hello".to_string() }));
let bool_source = dag.add_task(task_fn(|_: ()| async { true }));
let int_source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let string_source = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
let bool_source = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
let combined = dag
.add_task(task_fn(|(i, s, b): (i32, String, bool)| async move {
format!("{}: {} ({})", s, i, b)
}))
.add_task(task_fn::<(i32, String, bool), _, _>(
|(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b),
))
.depends_on((int_source, string_source, bool_source));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -77,12 +79,12 @@ async fn test_shared_dependencies() -> DagResult<()> {
// Test that a single node can be a dependency for multiple downstream nodes
let dag = DagRunner::new();
let shared: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let shared: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Create 10 tasks that all depend on the shared node
let dependents: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x * (i + 1) }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * (i + 1)))
.depends_on(shared)
})
.collect();
@@ -103,7 +105,7 @@ async fn test_multiple_source_nodes() -> DagResult<()> {
let dag = DagRunner::new();
let sources: Vec<_> = (0..20)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 10 })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10)))
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
+27 -29
View File
@@ -8,10 +8,10 @@ use futures::FutureExt;
async fn test_two_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 10 }));
let b = dag.add_task(task_fn(|_: ()| async { 20 }));
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
.depends_on((a, b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -24,12 +24,12 @@ async fn test_two_dependencies() -> DagResult<()> {
async fn test_three_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 }));
let b = dag.add_task(task_fn(|_: ()| async { 2 }));
let c = dag.add_task(task_fn(|_: ()| async { 3 }));
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let c = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
let sum = dag
.add_task(task_fn(
|(x, y, z): (i32, i32, i32)| async move { x + y + z },
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(x, y, z): (&i32, &i32, &i32)| x + y + z,
))
.depends_on((a, b, c));
@@ -44,13 +44,13 @@ async fn test_four_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let deps: Vec<_> = (1..=4)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into())
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
.collect();
let sum = dag
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move {
a + b + c + d
}))
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d,
))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -64,12 +64,12 @@ async fn test_five_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let deps: Vec<_> = (1..=5)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into())
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
.collect();
let sum = dag
.add_task(task_fn(
|(a, b, c, d, e): (i32, i32, i32, i32, i32)| async move { a + b + c + d + e },
.add_task(task_fn::<(i32, i32, i32, i32, i32), _, _>(
|(a, b, c, d, e): (&i32, &i32, &i32, &i32, &i32)| a + b + c + d + e,
))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4]));
@@ -84,16 +84,14 @@ async fn test_six_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let deps: Vec<_> = (1..=6)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into())
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
.collect();
let sum = dag.add_task(task_fn(
|(a, b, c, d, e, f): (i32, i32, i32, i32, i32, i32)| async move {
a + b + c + d + e + f
}
)).depends_on((
&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]
));
let sum = dag
.add_task(task_fn::<(i32, i32, i32, i32, i32, i32), _, _>(
|(a, b, c, d, e, f): (&i32, &i32, &i32, &i32, &i32, &i32)| a + b + c + d + e + f,
))
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3], &deps[4], &deps[5]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -106,12 +104,12 @@ async fn test_seven_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let deps: Vec<_> = (1..=7)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into())
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
.collect();
let sum = dag
.add_task(task_fn(
|(a, b, c, d, e, f, g): (i32, i32, i32, i32, i32, i32, i32)| async move {
.add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32), _, _>(
|(a, b, c, d, e, f, g): (&i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
a + b + c + d + e + f + g
},
))
@@ -130,12 +128,12 @@ async fn test_eight_dependencies() -> DagResult<()> {
let dag = DagRunner::new();
let deps: Vec<_> = (1..=8)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })).into())
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)).into())
.collect();
let sum = dag
.add_task(task_fn(
|(a, b, c, d, e, f, g, h): (i32, i32, i32, i32, i32, i32, i32, i32)| async move {
.add_task(task_fn::<(i32, i32, i32, i32, i32, i32, i32, i32), _, _>(
|(a, b, c, d, e, f, g, h): (&i32, &i32, &i32, &i32, &i32, &i32, &i32, &i32)| {
a + b + c + d + e + f + g + h
},
))
+16 -16
View File
@@ -8,7 +8,7 @@ use futures::FutureExt;
async fn test_result_not_found_error() {
let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Try to get result before running
let result = dag.get(t1);
@@ -27,11 +27,11 @@ async fn test_nested_result_error_handling() {
let dag = DagRunner::new();
// Task that returns a Result
let task1 = dag.add_task(task_fn(|_: ()| async {
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<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))
.await
@@ -46,7 +46,7 @@ async fn test_nested_result_error_handling() {
async fn test_option_none_not_error() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { None::<i32> }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::<i32>));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -66,11 +66,11 @@ async fn test_custom_error_types() {
let dag = DagRunner::new();
let task1 = dag.add_task(task_fn(|_: ()| async {
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<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 {
code: 500,
details: "Processing failed".to_string(),
@@ -82,13 +82,13 @@ async fn test_custom_error_types() {
.unwrap();
// Check custom errors are preserved
match dag.get(task1).unwrap() {
match dag.get(task1).unwrap().as_ref() {
Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
_ => panic!("Wrong error type"),
}
match dag.get(task2).unwrap() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500),
match dag.get(task2).unwrap().as_ref() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500),
_ => panic!("Wrong error type"),
}
}
@@ -97,13 +97,13 @@ async fn test_custom_error_types() {
async fn test_error_in_tuple_dependency() {
let dag = DagRunner::new();
let ok_task = dag.add_task(task_fn(|_: ()| async { 10 }));
let err_task = dag.add_task(task_fn(|_: ()| async {
let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error in dependency");
}));
let dependent = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((ok_task, err_task));
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -116,11 +116,11 @@ async fn test_error_in_tuple_dependency() {
async fn test_string_panic_vs_structured_panic() {
let dag = DagRunner::new();
let string_panic = dag.add_task(task_fn(|_: ()| async {
let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("String panic message");
}));
let formatted_panic = dag.add_task(task_fn(|_: ()| async {
let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Formatted panic: value={}, code={}", 42, "ERROR");
}));
@@ -136,7 +136,7 @@ async fn test_anyhow_like_error_chain() {
// Test error chains with simple string-based errors
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async {
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Simulate an error chain with nested Results
let _inner: Result<i32, String> = Err("Inner 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();
assert!(result.is_err());
if let Err(e) = result {
if let Err(e) = result.as_ref() {
assert_eq!(e, "Outer error");
}
}
+77 -71
View File
@@ -1,26 +1,28 @@
//! Tests for error propagation through the DAG
use crate::common::task_fn;
use dagx::{DagRunner, TaskHandle};
use dagx::{task, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn test_linear_error_propagation() {
let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
let t2 = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
panic!("Error at t2 with input {}", x);
}))
.depends_on(t1);
let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t2);
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);
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -46,20 +48,20 @@ async fn test_diamond_error_propagation() {
// \ /
// sink
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x / 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
.depends_on(source);
let right = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
panic!("Right path fails with {}", x);
}))
.depends_on(source);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
.depends_on((&left, &right));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -77,41 +79,35 @@ async fn test_error_stops_at_boundary() {
// Branch with error
let error_source: TaskHandle<_> = dag
.add_task(task_fn({
.add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone();
move |_: ()| {
move |_: ()| -> i32 {
let tracker = tracker.clone();
async move {
tracker.fetch_add(1, Ordering::SeqCst);
panic!("Error source");
}
}
}))
.into();
let error_dependent = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let tracker = execution_tracker.clone();
move |_: i32| {
move |_: &i32| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
42
}
}
}))
.depends_on(error_source);
// Independent branch
let independent = dag.add_task(task_fn({
let independent = dag.add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone();
move |_: ()| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(10, Ordering::SeqCst);
10
}
}
}));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -133,22 +129,24 @@ async fn test_multiple_error_sources_convergence() {
// Multiple error sources
let error1 = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 1");
}))
.into();
let error2 = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 2");
}))
.into();
let success = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let success = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Convergence point depends on all three
let convergence = dag
.add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 }))
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(_, _, _): (&i32, &i32, &i32)| 100,
))
.depends_on((&error1, &error2, &success));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -169,7 +167,7 @@ async fn test_error_in_wide_fanout() {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
panic!("Source fails");
}))
.into();
@@ -177,7 +175,7 @@ async fn test_error_in_wide_fanout() {
// Create wide fanout from failing source
let dependents: Vec<_> = (0..20)
.map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source)
})
.collect();
@@ -199,24 +197,26 @@ async fn test_selective_error_propagation() {
// Source that produces a Result
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, String>::Err("Source error".to_string())
}))
.into();
// Handler that processes the Result
let handler = dag
.add_task(task_fn(|result: Result<i32, String>| async move {
.add_task(task_fn::<Result<_, _>, _, _>(
|result: &Result<i32, String>| {
match result {
Ok(val) => val * 2,
Err(_) => -1, // Default value on error
}
}))
},
))
.depends_on(source);
// Further processing
let final_task = dag
.add_task(task_fn(|val: i32| async move {
.add_task(task_fn::<i32, _, _>(|&val: &i32| {
if val < 0 {
"Handled error case".to_string()
} else {
@@ -244,42 +244,50 @@ async fn test_error_propagation_timing() {
let dag = DagRunner::new();
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
let t1 = dag.add_task(task_fn({
let order = propagation_order.clone();
move |_: ()| {
let order = order.clone();
async move {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
order.lock().push("t1_complete");
struct T1Task {
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[task]
impl T1Task {
async fn run(&self) -> i32 {
sleep(Duration::from_millis(10)).await;
self.order.lock().push("t1_complete");
1
}
}
}));
let t1 = dag.add_task(T1Task {
order: propagation_order.clone(),
});
struct T2Task {
order: Arc<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
.add_task(task_fn({
let order = propagation_order.clone();
move |x: i32| {
let order = order.clone();
async move {
order.lock().push("t2_start");
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
panic!("t2 fails with {}", x);
}
}
}))
.add_task(T2Task {
order: propagation_order.clone(),
})
.depends_on(t1);
let _t3 = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let order = propagation_order.clone();
move |x: i32| {
move |x: &i32| {
let order = order.clone();
async move {
order.lock().push("t3_should_not_run");
x + 1
}
}
}))
.depends_on(t2);
@@ -307,53 +315,51 @@ async fn test_partial_branch_failure_propagation() {
// \ | /
// sink
let root: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
// Branch A
let a1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(root);
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);
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);
// Branch B (with error) - use Result instead of panic
let b1 = dag
.add_task(task_fn(|x: i32| async move { Ok(x * 2) }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| Ok::<i32, &str>(x * 2)))
.depends_on(root);
let b2 = dag
.add_task(task_fn(|_x: Result<i32, &str>| async move {
Err("Branch B fails at b2")
.add_task(task_fn::<Result<_, _>, _, _>(|_x: &Result<i32, &str>| {
Err::<i32, &str>("Branch B fails at b2")
}))
.depends_on(b1);
let b3 = dag
.add_task(task_fn(
|x: Result<i32, &str>| async move { x.map(|v| v * 3) },
))
.add_task(task_fn::<Result<_, _>, _, _>(|x: &Result<i32, &str>| {
x.map(|v| v * 3)
}))
.depends_on(b2);
// Branch C
let c1 = dag
.add_task(task_fn(|x: i32| async move { x - 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
.depends_on(root);
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);
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);
// Sink depends on all branches - handle Result from B
let sink = dag
.add_task(task_fn(
|(a, b, c): (i32, Result<i32, &str>, i32)| async move {
match b {
.add_task(task_fn::<(i32, Result<_, _>, _), _, _>(
|(a, b, c): (&i32, &Result<i32, &str>, &i32)| match b {
Ok(b_val) => Ok(a + b_val + c),
Err(e) => Err(e),
}
Err(e) => Err(*e),
},
))
.depends_on((&a3, &b3, &c3));
+18 -26
View File
@@ -11,17 +11,17 @@ async fn test_fallback_on_error() -> DagResult<()> {
let dag = DagRunner::new();
// Primary task that fails
let primary = dag.add_task(task_fn(|_: ()| async {
let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Primary task failed");
#[allow(unreachable_code)]
42
}));
// Fallback task
let fallback = dag.add_task(task_fn(|_: ()| async { 100 }));
let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
// Selector that uses fallback if primary fails
let _selector = dag.add_task(task_fn(|_: ()| async {}));
let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {}));
// Run DAG
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -41,11 +41,10 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
let attempt_counter = Arc::new(AtomicUsize::new(0));
// Task that would fail on first attempts (simulated)
let task = dag.add_task(task_fn({
let task = dag.add_task(task_fn::<(), _, _>({
let counter = attempt_counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
let attempt = counter.fetch_add(1, Ordering::SeqCst);
// In real retry, this would be retried
@@ -57,7 +56,6 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
Ok(42)
}
}
}));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -79,10 +77,9 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
.map(|i| {
let failures = failure_count.clone();
let circuit = circuit_open.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let failures = failures.clone();
let circuit = circuit.clone();
async move {
// Check if circuit is open
if circuit.load(Ordering::SeqCst) {
return Err("Circuit breaker open");
@@ -98,7 +95,6 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
}
Ok(i)
}
}))
})
.collect();
@@ -120,7 +116,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
// Some tasks that might fail
let _tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
if i % 3 == 0 {
panic!("Task {} failed", i);
}
@@ -130,7 +126,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
.collect();
// Aggregator that handles failures gracefully
let aggregator = dag.add_task(task_fn(|_: ()| async {
let aggregator = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// In real scenario, would check task results and use defaults
vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks
}));
@@ -152,15 +148,13 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
let mut tasks: Vec<_> = (0..10)
.map(|i| {
let errors = errors.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let errors = errors.clone();
async move {
if i % 2 == 0 {
errors.lock().push(format!("Error from task {}", i));
return Err(format!("Task {} error", i));
}
Ok(i)
}
}))
})
.collect();
@@ -187,16 +181,16 @@ async fn test_graceful_degradation() -> DagResult<()> {
let dag = DagRunner::new();
// Critical task
let critical = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
let critical = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
// Optional enhancement that fails - returns Result
let _enhancement = dag.add_task(task_fn(|_: ()| async {
let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Err::<Vec<i32>, &str>("Enhancement failed")
}));
// Final task that works with what's available
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
// Here just uses base
base.iter().sum::<i32>()
@@ -216,22 +210,22 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
let dag = DagRunner::new();
// Group A: contains an error - use Result
let a1 = dag.add_task(task_fn(|_: ()| async { 10 }));
let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let a2 = dag
.add_task(task_fn(|_x: i32| async move {
.add_task(task_fn::<i32, _, _>(|_x: &i32| {
Err::<i32, &str>("Group A error")
}))
.depends_on(a1);
// Group B: should be isolated from Group A's error
let b1 = dag.add_task(task_fn(|_: ()| async { 20 }));
let b1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let b2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(b1);
// Group C: depends on B but not A
let c1 = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(b2);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -252,21 +246,19 @@ async fn test_compensation_action() -> DagResult<()> {
let compensation_triggered = Arc::new(AtomicBool::new(false));
// Action that fails
let action = dag.add_task(task_fn(|_: ()| async {
let action = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Perform some action that fails
panic!("Action failed");
}));
// Compensation task (runs independently)
let compensation = dag.add_task(task_fn({
let compensation = dag.add_task(task_fn::<(), _, _>({
let triggered = compensation_triggered.clone();
move |_: ()| {
let triggered = triggered.clone();
async move {
triggered.store(true, Ordering::SeqCst);
"Compensation executed"
}
}
}));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
+41 -49
View File
@@ -15,7 +15,7 @@ async fn test_empty_dag() {
#[tokio::test]
async fn test_single_task() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
.unwrap();
@@ -26,18 +26,18 @@ async fn test_single_task() {
async fn test_deep_chain() {
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 }));
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
let b = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(a);
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);
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);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -54,7 +54,7 @@ async fn test_wide_parallel() {
let dag = DagRunner::new();
let tasks: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -70,15 +70,15 @@ async fn test_wide_parallel() {
async fn test_diamond_dependency() {
let dag = DagRunner::new();
let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
let b = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(a);
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);
let d = dag
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
.depends_on((&b, &c));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -100,13 +100,11 @@ struct User {
async fn test_different_output_types() {
let dag = DagRunner::new();
let string_task = dag.add_task(task_fn(|_: ()| async { "hello".to_string() }));
let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] }));
let struct_task = dag.add_task(task_fn(|_: ()| async {
User {
let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string()));
let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3]));
let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User {
name: "Alice".to_string(),
age: 30,
}
}));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -124,22 +122,22 @@ async fn test_different_output_types() {
async fn test_multiple_sinks() {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
// Branch 1
let branch1 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source);
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);
// Branch 2
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);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -156,12 +154,12 @@ async fn test_single_task_inline_path() {
let dag = DagRunner::new();
// Single task in its own layer
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
let t2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(t1);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -176,17 +174,17 @@ async fn test_multi_task_spawn_path() {
// Test multi-task layer spawn path (line 411-456 in runner.rs)
let dag = DagRunner::new();
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
// Create multiple tasks in the same layer (all depend on source)
let t1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source);
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);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -205,14 +203,14 @@ async fn test_producer_consumer_channels() {
// Producer
let producer: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }))
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
.into();
// Consumer
let consumer = dag
.add_task(task_fn(
|data: Vec<i32>| async move { data.iter().sum::<i32>() },
))
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
data.iter().sum::<i32>()
}))
.depends_on(producer);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -227,17 +225,17 @@ async fn test_multi_consumer_fanout() {
// Test fanout with multiple consumers from one producer
let dag = DagRunner::new();
let producer: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Multiple consumers
let c1 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(producer);
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);
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);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -255,17 +253,17 @@ async fn test_compute_layers_with_dependents_tracking() {
let dag = DagRunner::new();
// Create diamond pattern to ensure dependents are tracked correctly
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(source);
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);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
.depends_on((&left, &right));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -286,33 +284,27 @@ async fn test_layer_execution_order() {
let dag = DagRunner::new();
let order1 = execution_order.clone();
let t1 = dag.add_task(task_fn(move |_: ()| {
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let order = order1.clone();
async move {
order.lock().push(1);
1
}
}));
let order2 = execution_order.clone();
let t2 = dag
.add_task(task_fn(move |x: i32| {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order2.clone();
async move {
order.lock().push(2);
x + 1
}
}))
.depends_on(t1);
let order3 = execution_order.clone();
let t3 = dag
.add_task(task_fn(move |x: i32| {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order3.clone();
async move {
order.lock().push(3);
x + 1
}
}))
.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 {
let step1 = dag.add_task(Multiply::new(2)).depends_on(source);
let step2 = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(step1);
let step3 = dag.add_task(Multiply::new(3)).depends_on(step2);
final_nodes.push(step3);
@@ -276,7 +276,7 @@ async fn integration_test_parallel_execution_timing() -> DagResult<()> {
// Create 5 independent tasks that each sleep for 100ms
let tasks: Vec<_> = (0..num_parallel_tasks)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
sleep(sleep_duration).await;
i
}))
@@ -314,7 +314,7 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
// Layer 1: 4 parallel tasks (should take ~50ms)
let parallel_tasks: Vec<_> = (0..4)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
sleep(sleep_duration).await;
i * 10
}))
@@ -324,21 +324,21 @@ async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
// Layer 2: Sequential chain of 3 tasks depending on the first parallel task
// (should add ~150ms)
let seq1 = dag
.add_task(task_fn(move |x: i32| async move {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await;
x + 1
}))
.depends_on(parallel_tasks[0]);
let seq2 = dag
.add_task(task_fn(move |x: i32| async move {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await;
x + 1
}))
.depends_on(seq1);
let seq3 = dag
.add_task(task_fn(move |x: i32| async move {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
sleep(sleep_duration).await;
x + 1
}))
-1
View File
@@ -1,5 +1,4 @@
// Test module for execution tests
mod basic;
mod examples;
mod results;
+9 -9
View File
@@ -9,8 +9,8 @@ use futures::FutureExt;
async fn test_result_type() {
let dag = DagRunner::new();
let success_task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) }));
let error_task = dag.add_task(task_fn(|_: ()| async {
let success_task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
let error_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Err::<i32, String>("error".to_string())
}));
@@ -25,7 +25,7 @@ async fn test_result_type() {
#[tokio::test]
async fn test_dag_result_type_works() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 100 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
// Test that DagResult<()> works with ? operator pattern
async fn run_dag(dag: &DagRunner) -> DagResult<()> {
@@ -41,7 +41,7 @@ async fn test_dag_result_type_works() {
#[tokio::test]
async fn test_task_not_executed_error() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
// Try to get result before running - should error
let result = dag.get(task);
@@ -58,7 +58,7 @@ async fn test_task_not_executed_error() {
async fn test_error_messages_are_helpful() {
// Test ResultNotFound error message
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let result = dag.get(task);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
@@ -70,7 +70,7 @@ async fn test_error_messages_are_helpful() {
#[tokio::test]
async fn test_successful_execution_returns_ok() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
// Should return Ok(())
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -84,7 +84,7 @@ async fn test_successful_execution_returns_ok() {
async fn test_stateful_tasks() {
let dag = DagRunner::new();
let input = dag.add_task(task_fn(|_: ()| async { 5 }));
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let counter = dag.add_task(Counter::new(10)).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -102,9 +102,9 @@ async fn test_task_fn_with_captured_state() {
let dag = DagRunner::new();
let multiplier = 7;
let base = dag.add_task(task_fn(|_: ()| async { 5 }));
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let scaled = dag
.add_task(task_fn(move |x: i32| async move { x * multiplier }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
.depends_on(base);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
+33 -28
View File
@@ -1,27 +1,30 @@
//! Concurrent access tests
use crate::common::task_fn;
use dagx::DagRunner;
use dagx::{task, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
struct SleepTask(usize);
#[task]
impl SleepTask {
async fn run(&self) -> usize {
tokio::time::sleep(Duration::from_millis(5)).await;
self.0
}
}
#[tokio::test]
async fn test_concurrent_run_calls() {
// Test that multiple run() calls are handled correctly
let dag = Arc::new(DagRunner::new());
// Add some tasks
let tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(Duration::from_millis(5)).await;
i
}))
})
.collect();
let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect();
// Try to run the DAG multiple times concurrently
let mut handles = JoinSet::new();
@@ -59,7 +62,7 @@ async fn test_concurrent_execution_and_building() {
// Start with some initial tasks
let initial: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
// Execute the initial DAG
@@ -81,7 +84,7 @@ async fn test_concurrent_execution_and_building() {
for i in 10..20 {
let dag = Arc::clone(&dag);
handles.spawn(async move {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
i
});
}
@@ -112,14 +115,14 @@ async fn test_dag_builder_thread_safety() {
handles.spawn(async move {
// Each thread creates a small pipeline
let source = dag.add_task(task_fn(move |_: ()| async move { thread_id }));
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| thread_id));
let transform1 = dag
.add_task(task_fn(move |x: i32| async move { x * 10 }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 10))
.depends_on(source);
let transform2 = dag
.add_task(task_fn(move |x: i32| async move { x + thread_id }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + thread_id))
.depends_on(transform1);
counter.fetch_add(1, Ordering::SeqCst);
@@ -155,7 +158,7 @@ async fn test_concurrent_get_operations() {
// Create and execute tasks
let tasks: Vec<_> = (0..100)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -188,7 +191,7 @@ async fn test_concurrent_get_operations() {
async fn test_no_race_in_dependency_tracking() {
// Test that concurrent dependency modifications don't cause races
let dag = Arc::new(DagRunner::new());
let source = dag.add_task(task_fn(|_: ()| async { 42 }));
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let mut handles = JoinSet::new();
@@ -199,7 +202,7 @@ async fn test_no_race_in_dependency_tracking() {
let source_clone = source_handle;
handles.spawn(async move {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source_clone)
});
}
@@ -239,19 +242,21 @@ async fn test_high_concurrency_stress() {
match i % 3 {
0 => {
// Add a source task
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
}
1 => {
// Add a task with a simple dependency
let source = dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn(move |x: i32| async move { x * 2 }))
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 2))
.depends_on(source);
}
2 => {
// Add a more complex subgraph
let a = dag.add_task(task_fn(move |_: ()| async move { i }));
let b = dag.add_task(task_fn(move |_: ()| async move { i + 1 }));
dag.add_task(task_fn(move |(x, y): (i32, i32)| async move { x + y }))
let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1));
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
x + y
}))
.depends_on((a, b));
}
_ => unreachable!(),
@@ -289,7 +294,7 @@ async fn test_lock_contention_measurement() {
handles.spawn(async move {
for j in 0..10 {
dag.add_task(task_fn(move |_: ()| async move { i * 10 + j }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10 + j));
ops.fetch_add(1, Ordering::SeqCst);
// Small yield to increase contention likelihood
@@ -326,7 +331,7 @@ async fn test_atomic_dag_operations() {
let dag = Arc::new(DagRunner::new());
// Create a base task
let base = dag.add_task(task_fn(|_: ()| async { 100 }));
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
// Try to create dependent tasks concurrently
let mut handles = JoinSet::new();
@@ -340,7 +345,7 @@ async fn test_atomic_dag_operations() {
handles.spawn(async move {
let dep = dag
.add_task(task_fn(move |x: i32| async move { x + i }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(base_clone);
success.fetch_add(1, Ordering::SeqCst);
@@ -381,12 +386,12 @@ async fn test_no_deadlock_on_circular_waiting() {
handles.spawn(async move {
// Each thread creates a chain of tasks
let first = dag.add_task(task_fn(move |_: ()| async move { i }));
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
let mut chain = vec![first.into()];
for j in 0..5 {
let next = dag
.add_task(task_fn(move |x: i32| async move { x + j }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j))
.depends_on(chain.last().unwrap());
chain.push(next);
+125 -158
View File
@@ -1,7 +1,7 @@
//! Tests for execution correctness and parallelism
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
@@ -14,37 +14,61 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
let layer_timing = Arc::new(Mutex::new(Vec::new()));
struct Layer0Task {
timing: Arc<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
let sources: Vec<_> = (0..3)
.map(|i| {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |_: ()| {
let timing = timing.clone();
async move {
let start = Instant::now();
timing.lock().unwrap().push((format!("L0_{}", i), start));
sleep(Duration::from_millis(20)).await;
i * 10
}
}))
dag.add_task(Layer0Task {
timing: layer_timing.clone(),
idx: i,
})
})
.collect();
struct Layer1Task {
timing: Arc<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
let layer1: Vec<_> = sources
.into_iter()
.enumerate()
.map(|(i, source)| {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |x: i32| {
let timing = timing.clone();
async move {
let start = Instant::now();
timing.lock().unwrap().push((format!("L1_{}", i), start));
sleep(Duration::from_millis(20)).await;
x + 1
}
}))
dag.add_task(Layer1Task {
idx: i,
timing: layer_timing.clone(),
})
.depends_on(source)
})
.collect();
@@ -52,14 +76,14 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
// Layer 2: Final aggregation
let final_task = {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| {
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
move |(a, b, c): (&i32, &i32, &i32)| {
let timing = timing.clone();
async move {
let start = Instant::now();
timing.lock().unwrap().push(("L2_final".to_string(), start));
a + b + c
}
}))
},
))
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
};
@@ -134,51 +158,57 @@ async fn test_completion_order_matches_dependencies() -> DagResult<()> {
let completion_order = Arc::new(Mutex::new(Vec::new()));
// Create a complex DAG with clear ordering requirements
let a = {
let order = completion_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
struct ATask {
order: Arc<Mutex<Vec<&'static str>>>,
}
#[task]
impl ATask {
async fn run(&self) -> i32 {
sleep(Duration::from_millis(10)).await;
order.lock().unwrap().push("A");
self.order.lock().unwrap().push("A");
1
}
}))
};
}
let b = {
let order = completion_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
// 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;
order.lock().unwrap().push("B");
self.order.lock().unwrap().push("B");
2
}
}))
};
}
let b = dag.add_task(BTask {
order: completion_order.clone(),
});
let c = {
let order = completion_order.clone();
dag.add_task(task_fn(move |(x, y): (i32, i32)| {
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
let order = order.clone();
async move {
order.lock().unwrap().push("C");
x + y
}
}))
.depends_on((a, b))
};
let d = {
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();
async move {
order.lock().unwrap().push("D");
x * 2
}
}))
.depends_on(c)
};
@@ -215,54 +245,70 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
let execution_order = Arc::new(Mutex::new(Vec::new()));
// Create a chain: A -> B -> C -> D
let a: TaskHandle<_> = {
let order = execution_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
struct ATask {
order: Arc<Mutex<Vec<&'static str>>>,
}
#[task]
impl ATask {
async fn run(&self) -> i32 {
sleep(Duration::from_millis(20)).await;
order.lock().unwrap().push("A");
self.order.lock().unwrap().push("A");
1
}
}))
}
// Create a chain: A -> B -> C -> D
let a: TaskHandle<_> = dag
.add_task(ATask {
order: execution_order.clone(),
})
.into();
let b = {
let order = execution_order.clone();
dag.add_task(task_fn(move |x: i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("B");
sleep(Duration::from_millis(10)).await;
x + 1
struct BTask {
order: Arc<Mutex<Vec<&'static str>>>,
}
}))
.depends_on(a)
};
let c = {
let order = execution_order.clone();
dag.add_task(task_fn(move |x: i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("C");
#[task]
impl BTask {
async fn run(&self, x: &i32) -> i32 {
self.order.lock().unwrap().push("B");
sleep(Duration::from_millis(10)).await;
x + 1
}
}))
.depends_on(b)
};
}
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 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();
async move {
order.lock().unwrap().push("D");
x + 1
}
}))
.depends_on(c)
};
@@ -282,82 +328,3 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_independent_tasks_dont_wait() -> DagResult<()> {
// Prove that independent tasks don't wait for each other
let dag = DagRunner::new();
let task_started = Arc::new(Mutex::new(Vec::new()));
let task_finished = Arc::new(Mutex::new(Vec::new()));
// Create tasks with different durations
let fast1 = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("fast1", Instant::now()));
sleep(Duration::from_millis(10)).await;
finished.lock().unwrap().push(("fast1", Instant::now()));
"fast1"
}
}))
};
let slow = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("slow", Instant::now()));
sleep(Duration::from_millis(100)).await;
finished.lock().unwrap().push(("slow", Instant::now()));
"slow"
}
}))
};
let fast2 = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("fast2", Instant::now()));
sleep(Duration::from_millis(10)).await;
finished.lock().unwrap().push(("fast2", Instant::now()));
"fast2"
}
}))
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Check results
assert_eq!(dag.get(fast1)?, "fast1");
assert_eq!(dag.get(slow)?, "slow");
assert_eq!(dag.get(fast2)?, "fast2");
// Verify that fast tasks finished before slow task
let finished = task_finished.lock().unwrap();
let fast1_finish = finished.iter().find(|x| x.0 == "fast1").unwrap().1;
let fast2_finish = finished.iter().find(|x| x.0 == "fast2").unwrap().1;
let slow_finish = finished.iter().find(|x| x.0 == "slow").unwrap().1;
assert!(
fast1_finish < slow_finish,
"fast1 should finish before slow"
);
assert!(
fast2_finish < slow_finish,
"fast2 should finish before slow"
);
Ok(())
}
-1
View File
@@ -2,7 +2,6 @@
pub mod concurrent;
pub mod execution;
pub mod proof;
pub mod spawning;
pub mod threads;
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(())
}
+1 -60
View File
@@ -5,9 +5,6 @@ use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
@@ -18,15 +15,13 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
// Create 10 independent tasks
let tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn({
dag.add_task(task_fn::<(), _, _>({
let value = spawn_count.clone();
move |_: ()| {
let value = value.clone();
async move {
value.fetch_add(1, Ordering::SeqCst);
i
}
}
}))
})
.collect();
@@ -49,57 +44,3 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_tasks_not_running_inline() -> DagResult<()> {
// Verify that tasks are spawned and CAN run on different threads than the caller
// Note: Small/fast tasks may run on the same thread if the scheduler chooses
use std::thread;
let dag = DagRunner::new();
let calling_thread = thread::current().id();
let task_threads = Arc::new(Mutex::new(Vec::new()));
// Create tasks with some work to encourage distribution
let tasks: Vec<_> = (0..20)
.map(|i| {
let threads = task_threads.clone();
dag.add_task(task_fn(move |_: ()| {
let threads = threads.clone();
async move {
threads.lock().unwrap().push(thread::current().id());
// Add some async work
sleep(Duration::from_millis(2)).await;
i
}
}))
})
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
for (i, task) in tasks.into_iter().enumerate() {
assert_eq!(dag.get(task)?, i as i32);
}
// Check if tasks ran on different threads than the caller
// This is a capability test - the scheduler MAY choose to use the same thread
let task_thread_ids = task_threads.lock().unwrap();
let different_threads = task_thread_ids
.iter()
.filter(|&&tid| tid != calling_thread)
.count();
if different_threads == 0 {
eprintln!(
"Warning: All tasks ran on the calling thread. This may indicate tasks aren't \
being spawned, or the scheduler chose not to distribute (valid for very fast tasks)"
);
}
// The key thing is that tasks completed successfully via spawner
Ok(())
}
+112 -264
View File
@@ -1,10 +1,12 @@
//! Tests for multi-threading and concurrent execution
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::ThreadId;
use std::time::{Duration, Instant};
use tokio::time::sleep;
@@ -17,17 +19,18 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> {
let dag = DagRunner::new();
let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
// Create 50 tasks with actual work to encourage multi-threading
let tasks: Vec<_> = (0..50)
.map(|i| {
let ids = thread_ids.clone();
dag.add_task(task_fn(move |_: ()| {
let ids = ids.clone();
async move {
let tid = thread::current().id();
ids.lock().unwrap().insert(tid);
struct LayerTask {
ids: Arc<Mutex<HashSet<ThreadId>>>,
idx: i32,
}
// Do some actual async work to give tokio reason to distribute
#[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
@@ -35,9 +38,17 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> {
for j in 0..1000 {
sum += j;
}
i + sum
self.idx + sum
}
}))
}
// Create 50 tasks with actual work to encourage multi-threading
let tasks: Vec<_> = (0..50)
.map(|i| {
dag.add_task(LayerTask {
ids: thread_ids.clone(),
idx: i,
})
})
.collect();
@@ -71,23 +82,22 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
let concurrent_count = Arc::new(AtomicUsize::new(0));
let max_concurrent = Arc::new(AtomicUsize::new(0));
// Create 20 tasks that track concurrent execution
let tasks: Vec<_> = (0..20)
.map(|i| {
let count = concurrent_count.clone();
let max = max_concurrent.clone();
struct LayerTask {
count: Arc<AtomicUsize>,
max: Arc<AtomicUsize>,
idx: usize,
}
dag.add_task(task_fn(move |_: ()| {
let count = count.clone();
let max = max.clone();
async move {
#[task]
impl LayerTask {
async fn run(&self) -> usize {
// Increment concurrent counter
let current = count.fetch_add(1, Ordering::SeqCst) + 1;
let current = self.count.fetch_add(1, Ordering::SeqCst) + 1;
// Track maximum concurrency
let mut prev_max = max.load(Ordering::SeqCst);
let mut prev_max = self.max.load(Ordering::SeqCst);
while current > prev_max {
match max.compare_exchange_weak(
match self.max.compare_exchange_weak(
prev_max,
current,
Ordering::SeqCst,
@@ -102,11 +112,20 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
sleep(Duration::from_millis(10)).await;
// Decrement counter
count.fetch_sub(1, Ordering::SeqCst);
self.count.fetch_sub(1, Ordering::SeqCst);
i
self.idx
}
}))
}
// Create 20 tasks that track concurrent execution
let tasks: Vec<_> = (0..20)
.map(|i| {
dag.add_task(LayerTask {
count: concurrent_count.clone(),
max: max_concurrent.clone(),
idx: i,
})
})
.collect();
@@ -141,44 +160,56 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
let concurrent_middle = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
// These should run in parallel
let parallel1 = {
let counter = concurrent_middle.clone();
dag.add_task(task_fn(move |x: i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
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 = counter.load(Ordering::SeqCst);
counter.fetch_sub(1, Ordering::SeqCst);
let count = self.counter.load(Ordering::SeqCst);
self.counter.fetch_sub(1, Ordering::SeqCst);
assert!(count > 0, "Should see concurrent execution");
x * 2
}
}))
.depends_on(source)
};
}
let parallel2 = {
let counter = concurrent_middle.clone();
dag.add_task(task_fn(move |x: i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
// These should run in parallel
let parallel1 = dag
.add_task(ParallelTask1 {
counter: concurrent_middle.clone(),
})
.depends_on(source);
struct ParallelTask2 {
counter: Arc<AtomicUsize>,
}
#[task]
impl ParallelTask2 {
async fn run(&self, x: &i32) -> i32 {
self.counter.fetch_add(1, Ordering::SeqCst);
sleep(Duration::from_millis(20)).await;
let count = counter.load(Ordering::SeqCst);
counter.fetch_sub(1, Ordering::SeqCst);
let count = self.counter.load(Ordering::SeqCst);
self.counter.fetch_sub(1, Ordering::SeqCst);
// Both should be running
assert!(count > 0, "Should see concurrent execution");
x * 3
}
}))
.depends_on(source)
};
}
let parallel2 = dag
.add_task(ParallelTask2 {
counter: concurrent_middle.clone(),
})
.depends_on(source);
let sink = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&parallel1, &parallel2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -188,197 +219,6 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_parallel_branches_with_dependencies() -> DagResult<()> {
// Test that parallel branches execute independently but respect their own dependencies
let dag = DagRunner::new();
let branch_progress = Arc::new(Mutex::new(Vec::new()));
// Source node
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
// Branch 1: source -> b1_1 -> b1_2
let b1_1 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b1_1_start", Instant::now()));
sleep(Duration::from_millis(30)).await;
progress.lock().unwrap().push(("b1_1_end", Instant::now()));
x * 2
}
}))
.depends_on(source)
};
let b1_2 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b1_2_start", Instant::now()));
sleep(Duration::from_millis(10)).await;
progress.lock().unwrap().push(("b1_2_end", Instant::now()));
x + 1
}
}))
.depends_on(b1_1)
};
// Branch 2: source -> b2_1 -> b2_2
let b2_1 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b2_1_start", Instant::now()));
sleep(Duration::from_millis(10)).await;
progress.lock().unwrap().push(("b2_1_end", Instant::now()));
x * 3
}
}))
.depends_on(source)
};
let b2_2 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b2_2_start", Instant::now()));
sleep(Duration::from_millis(30)).await;
progress.lock().unwrap().push(("b2_2_end", Instant::now()));
x + 2
}
}))
.depends_on(b2_1)
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1
assert_eq!(dag.get(b2_2)?, 32); // (10 * 3) + 2
// Analyze timing to prove parallel execution
let progress = branch_progress.lock().unwrap();
// Find timestamps
let find_time = |name: &str| -> Instant {
progress
.iter()
.find(|(n, _)| *n == name)
.map(|(_, t)| *t)
.unwrap_or_else(|| panic!("Couldn't find {}", name))
};
let b1_1_start = find_time("b1_1_start");
let b1_1_end = find_time("b1_1_end");
let b1_2_start = find_time("b1_2_start");
let b2_1_start = find_time("b2_1_start");
// Branch 1 and Branch 2 first tasks should start nearly simultaneously
let branch_start_diff = if b1_1_start > b2_1_start {
b1_1_start - b2_1_start
} else {
b2_1_start - b1_1_start
};
// Use a more generous threshold to account for coverage instrumentation overhead
// 50ms is enough to prove they're parallel (not sequential) while being robust to overhead
assert!(
branch_start_diff < Duration::from_millis(50),
"Branches should start in parallel, diff was {:?}",
branch_start_diff
);
// b1_2 should only start after b1_1 ends
assert!(b1_2_start >= b1_1_end, "b1_2 started before b1_1 finished");
Ok(())
}
#[tokio::test]
async fn test_wide_fanout_parallel_execution() -> DagResult<()> {
// Test that a single source can fan out to many parallel tasks
let dag = DagRunner::new();
let max_concurrent = Arc::new(AtomicUsize::new(0));
let current_concurrent = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
// Create 50 tasks that all depend on the source
let dependents: Vec<_> = (0..50)
.map(|i| {
let max_c = max_concurrent.clone();
let curr_c = current_concurrent.clone();
dag.add_task(task_fn(move |x: i32| {
let max_c = max_c.clone();
let curr_c = curr_c.clone();
async move {
// Track concurrency
let current = curr_c.fetch_add(1, Ordering::SeqCst) + 1;
// Update max
let mut prev_max = max_c.load(Ordering::SeqCst);
while current > prev_max {
match max_c.compare_exchange_weak(
prev_max,
current,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(v) => prev_max = v,
}
}
// Do work
sleep(Duration::from_millis(5)).await;
curr_c.fetch_sub(1, Ordering::SeqCst);
x + i
}
}))
.depends_on(source)
})
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
for (i, task) in dependents.iter().enumerate() {
assert_eq!(dag.get(task)?, 42 + i as i32);
}
// Check that we saw significant parallelism
let max_seen = max_concurrent.load(Ordering::SeqCst);
println!("Max concurrent in fanout: {}", max_seen);
assert!(
max_seen > 5,
"Expected significant parallelism in fanout, got {}",
max_seen
);
Ok(())
}
#[tokio::test]
async fn test_massive_parallel_fanout() -> DagResult<()> {
// Test extreme parallelism with 1000 parallel tasks
@@ -388,26 +228,24 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
let max_concurrent = Arc::new(AtomicUsize::new(0));
let current = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
// Create 1000 tasks all depending on the same source
let tasks: Vec<_> = (0..1000)
.map(|i| {
let comp = completed.clone();
let max = max_concurrent.clone();
let curr = current.clone();
struct FanoutTask {
comp: Arc<AtomicUsize>,
max: Arc<AtomicUsize>,
curr: Arc<AtomicUsize>,
idx: i32,
}
dag.add_task(task_fn(move |x: i32| {
let comp = comp.clone();
let max = max.clone();
let curr = curr.clone();
async move {
#[task]
impl FanoutTask {
async fn run(&self, x: &i32) -> i32 {
// Track concurrency
let c = curr.fetch_add(1, Ordering::SeqCst) + 1;
let c = self.curr.fetch_add(1, Ordering::SeqCst) + 1;
let mut prev_max = max.load(Ordering::SeqCst);
let mut prev_max = self.max.load(Ordering::SeqCst);
while c > prev_max {
match max.compare_exchange_weak(
match self.max.compare_exchange_weak(
prev_max,
c,
Ordering::SeqCst,
@@ -419,13 +257,23 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
}
// Quick work
sleep(Duration::from_micros(100)).await;
sleep(Duration::from_millis(100)).await;
curr.fetch_sub(1, Ordering::SeqCst);
comp.fetch_add(1, Ordering::SeqCst);
x + i
self.curr.fetch_sub(1, Ordering::SeqCst);
self.comp.fetch_add(1, Ordering::SeqCst);
x + self.idx
}
}))
}
// Create 1000 tasks all depending on the same source
let tasks: Vec<_> = (0..1000)
.map(|i| {
dag.add_task(FanoutTask {
comp: completed.clone(),
max: max_concurrent.clone(),
curr: current.clone(),
idx: i,
})
.depends_on(source)
})
.collect();
+12 -87
View File
@@ -1,14 +1,20 @@
//! Tests for timing and speedup verification
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner};
use futures::FutureExt;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tokio::time::sleep;
struct SleepTask(usize, Duration);
#[task]
impl SleepTask {
async fn run(&self) -> usize {
sleep(self.1).await;
self.0
}
}
#[tokio::test]
async fn test_parallel_execution_speedup() -> DagResult<()> {
// Prove parallel execution by comparing timing
@@ -19,12 +25,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
// Create independent tasks that each take 50ms
let tasks: Vec<_> = (0..num_tasks)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
sleep(work_duration).await;
i
}))
})
.map(|i| dag.add_task(SleepTask(i, work_duration)))
.collect();
let start = Instant::now();
@@ -55,79 +56,3 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
Ok(())
}
struct SyncTask;
#[dagx::task]
impl SyncTask {
fn run(&self, x: &i32) -> i32 {
// Synchronous task - no async/await
std::thread::sleep(Duration::from_millis(1)); // Blocking sleep
x * 2
}
}
struct AsyncTask;
#[dagx::task]
impl AsyncTask {
async fn run(&self, x: &i32) -> i32 {
// Async task
sleep(Duration::from_millis(1)).await; // Non-blocking sleep
x * 3
}
}
#[tokio::test]
async fn test_sync_tasks_dont_block_async_runtime() -> DagResult<()> {
// Test that sync tasks don't block the async runtime
let dag = DagRunner::new();
let start_times = Arc::new(Mutex::new(Vec::new()));
// Mix of sync and async tasks
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
// Sync tasks
let sync1 = dag.add_task(SyncTask).depends_on(source);
let sync2 = dag.add_task(SyncTask).depends_on(source);
// Async tasks
let async1 = dag.add_task(AsyncTask).depends_on(source);
let async2 = dag.add_task(AsyncTask).depends_on(source);
// Track timing
let timing_task = {
let times = start_times.clone();
dag.add_task(task_fn(move |_: ()| {
let times = times.clone();
async move {
for i in 0..5 {
times
.lock()
.unwrap()
.push((format!("tick_{}", i), Instant::now()));
sleep(Duration::from_millis(5)).await;
}
"timing"
}
}))
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
assert_eq!(dag.get(sync1)?, 20);
assert_eq!(dag.get(sync2)?, 20);
assert_eq!(dag.get(async1)?, 30);
assert_eq!(dag.get(async2)?, 30);
assert_eq!(dag.get(timing_task)?, "timing");
// Check that timing task made progress (wasn't blocked by sync tasks)
let times = start_times.lock().unwrap();
assert_eq!(
times.len(),
5,
"Timing task should have completed all ticks"
);
Ok(())
}
+39 -35
View File
@@ -14,7 +14,7 @@ async fn test_butterfly_network() -> DagResult<()> {
// Layer 0: inputs
let inputs: Vec<_> = (0..8)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move { i as f64 }))
dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64))
.into()
})
.collect();
@@ -26,25 +26,29 @@ async fn test_butterfly_network() -> DagResult<()> {
let idx2 = i * 2 + 1;
// Sum operation
layer1.push(
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a + b) / 2.0 }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
(a + b) / 2.0
}))
.depends_on((&inputs[idx1], &inputs[idx2])),
);
// Difference operation
layer1.push(
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a - b) / 2.0 }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| {
(a - b) / 2.0
}))
.depends_on((&inputs[idx1], &inputs[idx2])),
);
}
// Layer 2: crossover connections
let layer2: Vec<_> = vec![
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
.depends_on((&layer1[0], &layer1[4])),
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
.depends_on((&layer1[1], &layer1[5])),
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
.depends_on((&layer1[2], &layer1[6])),
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b }))
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
.depends_on((&layer1[3], &layer1[7])),
];
@@ -76,11 +80,13 @@ async fn test_recursive_butterfly() -> DagResult<()> {
// Butterfly connections
for i in 0..mid {
let sum = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&inputs[i], &inputs[i + mid]));
let diff = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { (a - b).abs() }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| {
(a - b).abs()
}))
.depends_on((&inputs[i], &inputs[i + mid]));
outputs.push(sum);
@@ -94,7 +100,7 @@ async fn test_recursive_butterfly() -> DagResult<()> {
let inputs: Vec<dagx::TaskHandle<i32>> = vec![10, 20, 30, 40]
.into_iter()
.map(|val| {
let task = dag.add_task(task_fn(move |_: ()| async move { val }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val));
task.into()
})
.collect();
@@ -119,28 +125,36 @@ async fn test_benes_network() -> DagResult<()> {
// 4 inputs
let inputs: Vec<_> = (0..4)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move { 1 << i }))
dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i))
.into()
}) // Powers of 2
.collect();
// First stage: sort pairs
let stage1: Vec<_> = vec![
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) }))
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
a.min(b)
}))
.depends_on((&inputs[0], &inputs[1])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) }))
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
a.max(b)
}))
.depends_on((&inputs[0], &inputs[1])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) }))
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(|(a, b): (i32, i32)| async move { a.max(b) }))
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
a.max(b)
}))
.depends_on((&inputs[2], &inputs[3])),
];
// Second stage with crossover
let outputs: Vec<_> = vec![
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b }))
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
.depends_on((&stage1[0], &stage1[2])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b }))
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
.depends_on((&stage1[1], &stage1[3])),
];
@@ -162,12 +176,10 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
let inputs: Vec<TaskHandle<_>> = (0..8)
.map(|i| {
let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::Relaxed);
i
}
}))
.into()
})
@@ -178,7 +190,7 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
.map(|i| {
// Shuffle pattern: rotate bits
let source = ((i << 1) | (i >> 2)) & 0x7;
dag.add_task(task_fn(move |x: i32| async move { x }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x))
.depends_on(inputs[source])
})
.collect();
@@ -188,21 +200,13 @@ async fn test_shuffle_exchange_network() -> DagResult<()> {
.flat_map(|i| {
let idx = i * 2;
vec![
dag.add_task(task_fn(|(a, b): (i32, i32)| async move {
if a > b {
b
} else {
a
}
}))
dag.add_task(task_fn::<(i32, i32), _, _>(
|(&a, &b): (&i32, &i32)| if a > b { b } else { a },
))
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
dag.add_task(task_fn(|(a, b): (i32, i32)| async move {
if a > b {
a
} else {
b
}
}))
dag.add_task(task_fn::<(i32, i32), _, _>(
|(&a, &b): (&i32, &i32)| if a > b { a } else { b },
))
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
]
})
+54 -61
View File
@@ -11,10 +11,10 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> {
let dag = DagRunner::new();
// Input validation
let input = dag.add_task(task_fn(|_: ()| async { 42 }));
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let validate = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
if x > 0 && x < 100 {
Ok(x)
} else {
@@ -25,37 +25,37 @@ async fn test_workflow_with_conditional_paths() -> DagResult<()> {
// Process valid input
let process = dag
.add_task(task_fn(|result: Result<i32, &str>| async move {
match result {
Ok(x) => x * 2,
.add_task(task_fn::<Result<_, _>, _, _>(
|result: &Result<i32, &str>| match result {
Ok(x) => *x * 2,
Err(_) => -1,
}
}))
},
))
.depends_on(validate);
// Parallel enhancement steps
let enhance1 = dag
.add_task(task_fn(|x: i32| async move { x + 10 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
.depends_on(process);
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);
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);
// Combine enhancements
let combine = dag
.add_task(task_fn(|(e1, e2, e3): (i32, i32, i32)| async move {
vec![e1, e2, e3]
}))
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(&e1, &e2, &e3): (&i32, &i32, &i32)| vec![e1, e2, e3],
))
.depends_on((&enhance1, &enhance2, &enhance3));
// Final aggregation
let final_result = dag
.add_task(task_fn(|values: Vec<i32>| async move {
.add_task(task_fn::<Vec<_>, _, _>(|values: &Vec<i32>| {
values.iter().sum::<i32>()
}))
.depends_on(combine);
@@ -76,7 +76,7 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
// Map phase: process 16 inputs in parallel
let mappers: Vec<dagx::TaskHandle<Vec<(&str, usize)>>> = (0..16)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Simulate word count
vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)]
}));
@@ -94,14 +94,23 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
match deps.len() {
4 => dag
.add_task(task_fn(
.add_task(task_fn::<
(
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
),
_,
_,
>(
move |inputs: (
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
Vec<(&str, usize)>,
)| async move {
let mut combined = Vec::new();
&Vec<(&str, usize)>,
&Vec<(&str, usize)>,
&Vec<(&str, usize)>,
&Vec<(&str, usize)>,
)| {
let mut combined = Vec::<(&str, usize)>::new();
combined.extend(inputs.0);
combined.extend(inputs.1);
combined.extend(inputs.2);
@@ -119,11 +128,11 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
let reducers: Vec<_> = shuffled
.iter()
.map(|shuffled_data| {
dag.add_task(task_fn(|data: Vec<(&str, usize)>| async move {
dag.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<(&str, usize)>| {
// Sum values by key
let mut totals = std::collections::HashMap::new();
for (key, value) in data {
*totals.entry(key).or_insert(0) += value;
*totals.entry(*key).or_insert(0) += value;
}
totals.len()
}))
@@ -133,8 +142,8 @@ async fn test_map_reduce_pattern() -> DagResult<()> {
// Final aggregation
let final_task = dag
.add_task(task_fn(
|(r1, r2, r3, r4): (usize, usize, usize, usize)| async move { r1 + r2 + r3 + r4 },
.add_task(task_fn::<(_, _, _, _), _, _>(
|(r1, r2, r3, r4): (&usize, &usize, &usize, &usize)| r1 + r2 + r3 + r4,
))
.depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3]));
@@ -152,36 +161,30 @@ async fn test_pipeline_with_feedback() -> DagResult<()> {
let iteration_count = Arc::new(AtomicUsize::new(0));
// Initial value
let init = dag.add_task(task_fn(|_: ()| async { 1 }));
let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
// Stage 1: Transform
let stage1 = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let counter = iteration_count.clone();
move |x: i32| {
move |x: &i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
x * 2
}
}
}))
.depends_on(init);
// Stage 2: Filter
let stage2 = dag
.add_task(task_fn(|x: i32| async move {
if x < 100 {
Some(x)
} else {
None
}
}))
.add_task(task_fn::<i32, _, _>(
|&x: &i32| if x < 100 { Some(x) } else { None },
))
.depends_on(stage1);
// Stage 3: Accumulate
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)
}))
.depends_on(stage2);
@@ -189,7 +192,7 @@ async fn test_pipeline_with_feedback() -> DagResult<()> {
// Note: Can't create actual cycles in DAG, but we simulate feedback
// by having a separate "feedback" task that would feed back in a real system
let _feedback = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
// In a real feedback loop, this would feed back to stage1
x / 2
}))
@@ -210,7 +213,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Scatter: distribute work to multiple workers
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
vec![10, 20, 30, 40, 50, 60, 70, 80]
}))
.into();
@@ -218,7 +221,7 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Workers process chunks
let workers: Vec<_> = (0..4)
.map(|worker_id| {
dag.add_task(task_fn(move |data: Vec<i32>| async move {
dag.add_task(task_fn::<Vec<_>, _, _>(move |data: &Vec<i32>| {
// Each worker processes a chunk
let chunk_size = data.len() / 4;
let start = worker_id * chunk_size;
@@ -236,8 +239,8 @@ async fn test_scatter_gather_pattern() -> DagResult<()> {
// Gather: collect results from all workers
let gather = dag
.add_task(task_fn(
|(w0, w1, w2, w3): (i32, i32, i32, i32)| async move { w0 + w1 + w2 + w3 },
.add_task(task_fn::<(i32, i32, i32, i32), _, _>(
|(w0, w1, w2, w3): (&i32, &i32, &i32, &i32)| w0 + w1 + w2 + w3,
))
.depends_on((&workers[0], &workers[1], &workers[2], &workers[3]));
@@ -255,15 +258,13 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
let phase_counter = Arc::new(AtomicUsize::new(0));
// Fork phase
let parent = dag.add_task(task_fn({
let parent = dag.add_task(task_fn::<(), _, _>({
let counter = phase_counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
counter.store(1, Ordering::SeqCst);
100
}
}
}));
// Multiple parallel paths
@@ -273,12 +274,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
for i in 0..3 {
let counter = phase_counter.clone();
let task = dag
.add_task(task_fn(move |x: i32| {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let counter = counter.clone();
async move {
assert_eq!(counter.load(Ordering::SeqCst), 1);
x + i
}
}))
.depends_on(prev1);
path1.push(task);
@@ -290,12 +289,10 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
for i in 0..3 {
let counter = phase_counter.clone();
let task = dag
.add_task(task_fn(move |x: i32| {
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let counter = counter.clone();
async move {
assert_eq!(counter.load(Ordering::SeqCst), 1);
x * 2 + i
}
}))
.depends_on(prev2);
path2.push(task);
@@ -304,29 +301,25 @@ async fn test_fork_join_with_barriers() -> DagResult<()> {
// Barrier: wait for all paths
let barrier = dag
.add_task(task_fn({
.add_task(task_fn::<(i32, i32), _, _>({
let counter = phase_counter.clone();
move |(p1, p2): (i32, i32)| {
move |(p1, p2): (&i32, &i32)| {
let counter = counter.clone();
async move {
counter.store(2, Ordering::SeqCst);
p1 + p2
}
}
}))
.depends_on((&path1[2], &path2[2]));
// Join phase
let join = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let counter = phase_counter.clone();
move |x: i32| {
move |&x: &i32| {
let counter = counter.clone();
async move {
assert_eq!(counter.load(Ordering::SeqCst), 2);
x
}
}
}))
.depends_on(barrier);
+21 -19
View File
@@ -14,10 +14,10 @@ async fn test_2d_grid_pattern() -> DagResult<()> {
// Initialize first row and column
#[allow(clippy::needless_range_loop)]
for i in 0..4 {
let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32));
grid[0][i] = Some(task.into());
if i > 0 {
let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 * 10 }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32 * 10));
grid[i][0] = Some(task.into());
}
}
@@ -29,7 +29,9 @@ async fn test_2d_grid_pattern() -> DagResult<()> {
let left = grid[i][j - 1].as_ref().unwrap();
let task = dag
.add_task(task_fn(move |(a, l): (i32, i32)| async move { a + l }))
.add_task(task_fn::<(i32, i32), _, _>(move |(a, l): (&i32, &i32)| {
a + l
}))
.depends_on((above, left));
grid[i][j] = Some(task);
}
@@ -54,7 +56,7 @@ async fn test_3d_grid_pattern() -> DagResult<()> {
vec![vec![vec![None; size]; size]; size];
// Initialize origin
let origin = dag.add_task(task_fn(|_: ()| async { 1 }));
let origin = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
grid[0][0][0] = Some(origin.into());
// Fill the 3D grid
@@ -76,18 +78,20 @@ async fn test_3d_grid_pattern() -> DagResult<()> {
let task_handle: dagx::TaskHandle<usize> = match deps.len() {
0 => {
let t = dag.add_task(task_fn(move |_: ()| async move { x + y + z }));
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| x + y + z));
t.into()
}
1 => dag
.add_task(task_fn(move |prev: usize| async move { prev + 1 }))
.add_task(task_fn::<usize, _, _>(move |prev: &usize| prev + 1))
.depends_on(deps[0]),
2 => dag
.add_task(task_fn(move |(a, b): (usize, usize)| async move { a + b }))
.add_task(task_fn::<(usize, usize), _, _>(
move |(a, b): (&usize, &usize)| a + b,
))
.depends_on((&deps[0], &deps[1])),
3 => dag
.add_task(task_fn(
move |(a, b, c): (usize, usize, usize)| async move { a + b + c },
.add_task(task_fn::<(usize, usize, usize), _, _>(
move |(a, b, c): (&usize, &usize, &usize)| a + b + c,
))
.depends_on((&deps[0], &deps[1], &deps[2])),
_ => unreachable!(),
@@ -120,7 +124,7 @@ async fn test_hexagonal_grid() -> DagResult<()> {
let mut hex_grid = std::collections::HashMap::new();
// Center hex
let center_builder = dag.add_task(task_fn(|_: ()| async { 100 }));
let center_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
let center: dagx::TaskHandle<i32> = center_builder.into();
hex_grid.insert((0, 0), center);
@@ -129,9 +133,7 @@ async fn test_hexagonal_grid() -> DagResult<()> {
for (i, &(dq, dr)) in directions.iter().enumerate() {
let neighbor: dagx::TaskHandle<i32> = dag
.add_task(task_fn(
move |c: i32| async move { c + (i as i32 + 1) * 10 },
))
.add_task(task_fn::<i32, _, _>(move |c: &i32| c + (i as i32 + 1) * 10))
.depends_on(center);
hex_grid.insert((dq, dr), neighbor);
}
@@ -159,15 +161,15 @@ async fn test_hexagonal_grid() -> DagResult<()> {
.collect();
let task: dagx::TaskHandle<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
}))
.depends_on((&neighbors[0], &neighbors[1]))
} else if neighbors.len() == 1 {
dag.add_task(task_fn(move |n: i32| async move { n + q + r }))
dag.add_task(task_fn::<i32, _, _>(move |n: &i32| n + q + r))
.depends_on(neighbors[0])
} else {
let t = dag.add_task(task_fn(move |_: ()| async move { q + r }));
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| q + r));
t.into()
};
@@ -193,7 +195,7 @@ async fn test_toroidal_grid() -> DagResult<()> {
#[allow(clippy::needless_range_loop)]
for i in 0..SIZE {
for j in 0..SIZE {
let task = dag.add_task(task_fn(move |_: ()| async move { (i * SIZE + j) as i32 }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| (i * SIZE + j) as i32));
grid[i][j] = Some(task.into());
}
}
@@ -211,8 +213,8 @@ async fn test_toroidal_grid() -> DagResult<()> {
let right = grid[i][(j + 1) % SIZE].as_ref().unwrap();
let task = dag
.add_task(task_fn(
move |(c, u, d, l, r): (i32, i32, i32, i32, i32)| async move {
.add_task(task_fn::<(_, _, _, _, _), _, _>(
move |(c, u, d, l, r): (&i32, &i32, &i32, &i32, &i32)| {
// Average of self and 4 neighbors
(c + u + d + l + r) / 5
},
+36 -36
View File
@@ -10,7 +10,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> {
// Create 8 leaf nodes
let leaves: Vec<_> = (0..8)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { 1 << i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i)))
.collect();
// Build binary tree reduction
@@ -23,7 +23,7 @@ async fn test_binary_tree_reduction() -> DagResult<()> {
for chunk in current_level.chunks(2) {
if chunk.len() == 2 {
let task = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&chunk[0], &chunk[1]));
next_level.push(task);
} else {
@@ -51,7 +51,7 @@ async fn test_n_ary_tree() -> DagResult<()> {
// Build a 3-ary tree (each node has 3 children)
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
if depth == 0 {
let task = dag.add_task(task_fn(move |_: ()| async move { value }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value));
return task.into();
}
@@ -59,9 +59,9 @@ async fn test_n_ary_tree() -> DagResult<()> {
.map(|i| build_tree(dag, depth - 1, value * 3 + i))
.collect();
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move {
value + a + b + c
}))
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
move |(a, b, c): (&i32, &i32, &i32)| value + a + b + c,
))
.depends_on((&children[0], &children[1], &children[2]))
}
@@ -88,30 +88,30 @@ async fn test_unbalanced_tree() -> DagResult<()> {
// / \
// f g
let f: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let g = dag.add_task(task_fn(|_: ()| async { 2 }));
let f: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
let g = dag.add_task(task_fn::<(), _, _>(|_: ()| 2));
let c = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(f);
let d_builder = dag.add_task(task_fn(|_: ()| async { 3 }));
let d_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
let d: dagx::TaskHandle<i32> = d_builder.into();
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);
let a = dag
.add_task(task_fn(|(c, d): (i32, i32)| async move { c + d }))
.add_task(task_fn::<(i32, i32), _, _>(|(c, d): (&i32, &i32)| c + d))
.depends_on((&c, &d));
let b = dag
.add_task(task_fn(|e: i32| async move { e + 10 }))
.add_task(task_fn::<i32, _, _>(|e: &i32| e + 10))
.depends_on(e);
let root = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a * b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
.depends_on((&a, &b));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -131,37 +131,39 @@ async fn test_trie_like_structure() -> DagResult<()> {
// Build a trie-like structure for paths
let root: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async { "root".to_string() }))
.add_task(task_fn::<(), _, _>(|_: ()| "root".to_string()))
.into();
// First level
let usr = dag
.add_task(task_fn(|r: String| async move { format!("{}/usr", r) }))
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/usr", r)))
.depends_on(root);
let etc = dag
.add_task(task_fn(|r: String| async move { format!("{}/etc", r) }))
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/etc", r)))
.depends_on(root);
let var = dag
.add_task(task_fn(|r: String| async move { format!("{}/var", r) }))
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/var", r)))
.depends_on(root);
// Second level
let usr_bin = dag
.add_task(task_fn(|u: String| async move { format!("{}/bin", u) }))
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/bin", u)))
.depends_on(usr);
let usr_lib = dag
.add_task(task_fn(|u: String| async move { format!("{}/lib", u) }))
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/lib", u)))
.depends_on(usr);
let etc_config = dag
.add_task(task_fn(|e: String| async move { format!("{}/config", e) }))
.add_task(task_fn::<String, _, _>(|e: &String| {
format!("{}/config", e)
}))
.depends_on(etc);
let var_log = dag
.add_task(task_fn(|v: String| async move { format!("{}/log", v) }))
.add_task(task_fn::<String, _, _>(|v: &String| format!("{}/log", v)))
.depends_on(var);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -189,7 +191,7 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> {
node_id: usize,
) -> dagx::TaskHandle<usize> {
if depth == 0 {
let task = dag.add_task(task_fn(move |_: ()| async move { node_id }));
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| node_id));
return task.into();
}
@@ -199,20 +201,18 @@ async fn test_balanced_k_ary_tree() -> DagResult<()> {
match k {
2 => dag
.add_task(task_fn(move |(a, b): (usize, usize)| async move {
node_id + a + b
}))
.add_task(task_fn::<(usize, usize), _, _>(
move |(a, b): (&usize, &usize)| node_id + a + b,
))
.depends_on((&children[0], &children[1])),
3 => dag
.add_task(task_fn(
move |(a, b, c): (usize, usize, usize)| async move { node_id + a + b + c },
.add_task(task_fn::<(usize, usize, usize), _, _>(
move |(a, b, c): (&usize, &usize, &usize)| node_id + a + b + c,
))
.depends_on((&children[0], &children[1], &children[2])),
4 => dag
.add_task(task_fn(
move |(a, b, c, d): (usize, usize, usize, usize)| async move {
node_id + a + b + c + d
},
.add_task(task_fn::<(usize, usize, usize, usize), _, _>(
move |(a, b, c, d): (&usize, &usize, &usize, &usize)| node_id + a + b + c + d,
))
.depends_on((&children[0], &children[1], &children[2], &children[3])),
_ => panic!("Unsupported k value"),
@@ -236,7 +236,7 @@ async fn test_merkle_tree_pattern() -> DagResult<()> {
// Simulate a Merkle tree for 8 data blocks
let data_blocks: Vec<_> = (0..8)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Simulate hash of data block
format!("hash_{}", i)
}))
@@ -253,9 +253,9 @@ async fn test_merkle_tree_pattern() -> DagResult<()> {
for pair in current_level.chunks(2) {
if pair.len() == 2 {
let hash_task = dag
.add_task(task_fn(|(left, right): (String, String)| async move {
format!("hash({}_{})", left, right)
}))
.add_task(task_fn::<(String, String), _, _>(
|(left, right): (&String, &String)| format!("hash({}_{})", left, right),
))
.depends_on((&pair[0], &pair[1]));
next_level.push(hash_task);
} else {
-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;
pub mod stress;
use crate::common::task_fn;
use dagx::DagRunner;
use futures::FutureExt;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[tokio::test]
async fn test_deep_chain_100_levels() {
let dag = Arc::new(DagRunner::new());
// Create a chain 100 levels deep
let start = Instant::now();
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let mut handles = vec![first.into()];
for _ in 1..=100 {
let next = dag
.add_task(task_fn::<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 tasks: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
.collect();
dag.run(async_std::task::spawn).await.unwrap();
@@ -55,10 +55,10 @@ async fn test_complex_dependencies_async_std() {
let b = dag.add_task(Value(20));
let sum = dag.add_task(Add).depends_on((a, b));
let double = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(sum);
dag.run(async_std::task::spawn).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60);
assert_eq!(dag.get(double).unwrap().clone(), 60);
}
+35 -37
View File
@@ -32,10 +32,10 @@ fn test_async_executor_basic() {
let future = async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 10 }));
let y = dag.add_task(task_fn(|_: ()| async { 20 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((x, y));
dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -54,18 +54,18 @@ fn test_async_executor_complex_dag() {
let dag = DagRunner::new();
// Create a diamond pattern
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 5 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(source);
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);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
.depends_on((&left, &right));
dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -88,12 +88,10 @@ fn test_async_executor_parallel_tasks() {
let tasks: Vec<_> = (0..10)
.map(|i| {
let c = counter.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
i
}
}))
})
.collect();
@@ -120,7 +118,7 @@ fn test_async_executor_with_local_spawn() {
let future = async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 42 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -138,14 +136,14 @@ fn test_async_executor_deep_chain() {
let dag = DagRunner::new();
// Build a chain of 50 tasks
let start = dag.add_task(task_fn(|_: ()| async { 0 }));
let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let mut current = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(start);
for _ in 1..50 {
current = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(current);
}
@@ -167,9 +165,9 @@ fn test_pollster_basic() {
pollster::block_on(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 100 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
let doubled = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(x);
dag.run(|fut| async { pollster::block_on(fut) })
@@ -186,11 +184,11 @@ fn test_pollster_complex() {
let dag = DagRunner::new();
// Fan-out pattern
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 7 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)).into();
let tasks: Vec<_> = (0..5)
.map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x * i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * i))
.depends_on(source)
})
.collect();
@@ -211,7 +209,7 @@ fn test_pollster_error_handling() {
let dag = DagRunner::new();
// Task that returns Result
let task = dag.add_task(task_fn(|_: ()| async { Ok::<i32, String>(42) }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
dag.run(|fut| async { pollster::block_on(fut) })
.await
@@ -230,10 +228,10 @@ fn test_futures_executor_basic() {
futures_executor::block_on(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 15 }));
let y = dag.add_task(task_fn(|_: ()| async { 25 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25));
let product = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a * b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
.depends_on((x, y));
let spawner = ThreadPool::new().unwrap();
@@ -256,7 +254,7 @@ fn test_futures_executor_threadpool() {
// Create multiple independent tasks
let tasks: Vec<_> = (0..20)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * i)))
.collect();
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
@@ -281,18 +279,18 @@ fn test_futures_executor_diamond_pattern() {
futures_executor::block_on(async {
let dag = DagRunner::new();
let a: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 8 })).into();
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 8)).into();
let b = dag
.add_task(task_fn(|x: i32| async move { x + 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(a);
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);
let d = dag
.add_task(task_fn(|(x, y): (i32, i32)| async move { x * y }))
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x * y))
.depends_on((&b, &c));
let spawner = ThreadPool::new().unwrap();
@@ -318,7 +316,7 @@ fn test_all_runtimes_compatibility() {
let executor = async_executor::Executor::new();
executor.run(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
dag.run(|fut| executor.spawn(fut)).await.unwrap();
assert_eq!(dag.get(x).unwrap(), 123);
});
@@ -328,7 +326,7 @@ fn test_all_runtimes_compatibility() {
{
pollster::block_on(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
dag.run(|fut| async { pollster::block_on(fut) })
.await
.unwrap();
@@ -340,7 +338,7 @@ fn test_all_runtimes_compatibility() {
{
futures_executor::block_on(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 123 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
let spawner = ThreadPool::new().unwrap();
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
.await
@@ -360,7 +358,7 @@ fn test_runtime_edge_cases() {
let dag = DagRunner::new();
// This tests that our API works even with !Send futures
let x = dag.add_task(task_fn(|_: ()| async {
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Local state
42
@@ -386,7 +384,7 @@ fn test_async_executor_stress() {
// Create 100 tasks
let tasks: Vec<_> = (0..100)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
// Small delay to test executor scheduling
i
}))
@@ -406,7 +404,7 @@ fn test_pollster_nested_blocks() {
// Test nested blocking - edge case
pollster::block_on(async {
let dag1 = DagRunner::new();
let x = dag1.add_task(task_fn(|_: ()| async { 10 }));
let x = dag1.add_task(task_fn::<(), _, _>(|_: ()| 10));
dag1.run(|fut| async { pollster::block_on(fut) })
.await
@@ -425,7 +423,7 @@ fn test_futures_executor_multi_pool() {
pool1.spawn_ok(async move {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 50 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 50));
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
.await
@@ -459,8 +457,8 @@ fn test_sync_tasks_on_async_executor() {
executor.run(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 5 }));
let y = dag.add_task(task_fn(|_: ()| async { 7 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 7));
let sum = dag.add_task(SyncAdd).depends_on((x, y));
dag.run(|fut| executor.spawn(fut)).await.unwrap();
@@ -483,7 +481,7 @@ fn test_sync_tasks_on_pollster() {
pollster::block_on(async {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 21 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 21));
let doubled = dag.add_task(SyncDouble).depends_on(x);
dag.run(|fut| async { pollster::block_on(fut) })
+3 -3
View File
@@ -40,7 +40,7 @@ fn test_parallel_execution_smol() {
let dag = DagRunner::new();
let tasks: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i * 2 })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
.collect();
dag.run(smol::spawn).await.unwrap();
@@ -60,11 +60,11 @@ fn test_complex_dependencies_smol() {
let b = dag.add_task(Value(20));
let sum = dag.add_task(Add).depends_on((a, b));
let double = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(sum);
dag.run(smol::spawn).await.unwrap();
assert_eq!(dag.get(double).unwrap(), 60);
assert_eq!(dag.get(double).unwrap().clone(), 60);
});
}
+7 -13
View File
@@ -41,8 +41,8 @@ async fn test_all_three_task_patterns() {
let dag = DagRunner::new();
// Create initial values
let x = dag.add_task(task_fn(|_: ()| async { 5 }));
let y = dag.add_task(task_fn(|_: ()| async { 3 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
// Pattern 1: Stateless addition
let sum = dag.add_task(StatelessAdd).depends_on((x, y)); // 5 + 3 = 8
@@ -72,28 +72,22 @@ async fn test_parallel_execution() {
let c2 = counter.clone();
let c3 = counter.clone();
let t1 = dag.add_task(task_fn(move |_: ()| {
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c1.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
1
}
}));
let t2 = dag.add_task(task_fn(move |_: ()| {
let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c2.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
2
}
}));
let t3 = dag.add_task(task_fn(move |_: ()| {
let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let c = c3.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
3
}
}));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -112,9 +106,9 @@ async fn test_task_fn_with_captured_state() {
let dag = DagRunner::new();
let multiplier = 7;
let base = dag.add_task(task_fn(|_: ()| async { 5 }));
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let scaled = dag
.add_task(task_fn(move |x: i32| async move { x * multiplier }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
.depends_on(base);
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
let dag = DagRunner::new();
let input = dag.add_task(task_fn(|_: ()| async { 5 }));
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -68,8 +68,8 @@ async fn test_readonly_state_multiple_uses() {
// Test that read-only state can be used multiple times
let dag = DagRunner::new();
let input1 = dag.add_task(task_fn(|_: ()| async { 10 }));
let input2 = dag.add_task(task_fn(|_: ()| async { 20 }));
let input1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let config = ReadOnlyConfig {
multiplier: 3,
+12 -12
View File
@@ -82,8 +82,8 @@ async fn test_stateless_basic() {
// Test basic stateless task with two inputs
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 10 }));
let y = dag.add_task(task_fn(|_: ()| async { 20 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let sum = dag.add_task(StatelessAdd).depends_on((x, y));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -97,7 +97,7 @@ async fn test_stateless_single_input() {
// Test stateless task with single input
let dag = DagRunner::new();
let input = dag.add_task(task_fn(|_: ()| async { 21 }));
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 21));
let doubled = dag.add_task(StatelessDouble).depends_on(input);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -124,9 +124,9 @@ async fn test_stateless_multiple_inputs() {
// Test stateless task with multiple inputs of different types
let dag = DagRunner::new();
let num = dag.add_task(task_fn(|_: ()| async { 42 }));
let text = dag.add_task(task_fn(|_: ()| async { "test".to_string() }));
let flag = dag.add_task(task_fn(|_: ()| async { true }));
let num = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let text = dag.add_task(task_fn::<(), _, _>(|_: ()| "test".to_string()));
let flag = dag.add_task(task_fn::<(), _, _>(|_: ()| true));
let result = dag.add_task(StatelessFormat).depends_on((num, text, flag));
@@ -160,8 +160,8 @@ async fn test_stateless_mixed_with_stateful() {
let dag = DagRunner::new();
let x = dag.add_task(task_fn(|_: ()| async { 5 }));
let y = dag.add_task(task_fn(|_: ()| async { 3 }));
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
// Stateless multiplication
let product = dag.add_task(StatelessMultiply).depends_on((x, y));
@@ -182,7 +182,7 @@ async fn test_stateless_chain() {
// Test chaining stateless tasks
let dag = DagRunner::new();
let start = dag.add_task(task_fn(|_: ()| async { 0 }));
let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
let step1 = dag.add_task(StatelessInc).depends_on(start);
let step2 = dag.add_task(StatelessInc).depends_on(step1);
let step3 = dag.add_task(StatelessInc).depends_on(step2);
@@ -203,7 +203,7 @@ async fn test_stateless_parallel() {
let tasks: Vec<_> = (1..=5)
.map(|i| {
let source = dag.add_task(task_fn(move |_: ()| async move { i }));
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
dag.add_task(StatelessSquare).depends_on(source)
})
.collect();
@@ -224,8 +224,8 @@ async fn test_stateless_with_string_types() {
// Test stateless tasks with complex types
let dag = DagRunner::new();
let hello = dag.add_task(task_fn(|_: ()| async { "Hello, ".to_string() }));
let world = dag.add_task(task_fn(|_: ()| async { "World!".to_string() }));
let hello = dag.add_task(task_fn::<(), _, _>(|_: ()| "Hello, ".to_string()));
let world = dag.add_task(task_fn::<(), _, _>(|_: ()| "World!".to_string()));
let greeting = dag.add_task(StatelessConcat).depends_on((hello, world));
+59 -42
View File
@@ -1,10 +1,11 @@
//! Tests for concurrent access patterns and race conditions
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[tokio::test]
@@ -12,13 +13,20 @@ async fn test_concurrent_result_retrieval() -> DagResult<()> {
// Test concurrent get() calls after execution
let dag = Arc::new(DagRunner::new());
struct DelayTask(usize, Duration);
#[task]
impl DelayTask {
async fn run(&self) -> usize {
tokio::time::sleep(self.1).await;
self.0
}
}
// Add tasks with minimal delays
let tasks: Vec<dagx::TaskHandle<usize>> = (0..10)
.map(|i| {
let task = dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
i as usize
}));
let task = dag.add_task(DelayTask(i, Duration::from_millis(1)));
task.into()
})
.collect();
@@ -54,7 +62,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
// Start with a few initial tasks
let initial: Vec<_> = (0..5)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
// Start building more tasks in background
@@ -63,7 +71,7 @@ async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
let build_handle = tokio::spawn(async move {
let mut tasks: Vec<dagx::TaskHandle<i32>> = Vec::new();
for i in 5..50 {
let task = dag_build.add_task(task_fn(move |_: ()| async move { i }));
let task = dag_build.add_task(task_fn::<(), _, _>(move |_: ()| i));
tasks.push(task.into());
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
@@ -108,15 +116,13 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> {
// Create a source task
let source: TaskHandle<_> = dag
.add_task(task_fn({
.add_task(task_fn::<(), _, _>({
let counter = resolution_counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
42
}
}
}))
.into();
@@ -124,12 +130,10 @@ async fn test_concurrent_dependency_resolution() -> DagResult<()> {
let dependents: Vec<_> = (0..100)
.map(|i| {
let counter = resolution_counter.clone();
dag.add_task(task_fn(move |val: i32| {
dag.add_task(task_fn::<i32, _, _>(move |val: &i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
val + i
}
}))
.depends_on(source)
})
@@ -154,38 +158,56 @@ async fn test_read_write_race_patterns() -> DagResult<()> {
let dag = DagRunner::new();
let shared_state = Arc::new(RwLock::new(Vec::new()));
struct WriterTask {
state: Arc<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
let writers: Vec<_> = (0..10)
.map(|i| {
let state = shared_state.clone();
dag.add_task(task_fn(move |_: ()| {
let state = state.clone();
async move {
let mut guard = state.write().await;
guard.push(i);
tokio::task::yield_now().await; // Force potential race
guard.push(i * 10);
i
}
}))
dag.add_task(WriterTask {
state: shared_state.clone(),
idx: i,
})
.into()
})
.collect();
struct ReaderTask {
state: Arc<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)
let readers: Vec<_> = writers
.iter()
.map(|writer| {
let state = shared_state.clone();
dag.add_task(task_fn(move |writer_id: i32| {
let state = state.clone();
async move {
let guard = state.read().await;
let contains_writer = guard.contains(&writer_id);
let len = guard.len();
(writer_id, contains_writer, len)
}
}))
dag.add_task(ReaderTask {
state: shared_state.clone(),
})
.depends_on(writer)
})
.collect();
@@ -194,7 +216,7 @@ async fn test_read_write_race_patterns() -> DagResult<()> {
// Verify readers saw their writer's data
for reader in &readers {
let (writer_id, saw_writer, _len) = dag.get(reader)?;
let ReaderTaskOutput((writer_id, saw_writer, _len)) = dag.get(reader)?;
assert!(saw_writer, "Reader didn't see writer {}", writer_id);
}
@@ -217,10 +239,9 @@ async fn test_atomic_counter_races() -> DagResult<()> {
.map(|_| {
let counter = counter.clone();
let max_seen = max_seen.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone();
let max_seen = max_seen.clone();
async move {
let mut local_max = 0;
for _ in 0..100 {
let val = counter.fetch_add(1, Ordering::SeqCst);
@@ -240,8 +261,6 @@ async fn test_atomic_counter_races() -> DagResult<()> {
}
}
}
local_max
}
}))
})
.collect();
@@ -272,12 +291,10 @@ async fn test_multiple_dag_runners_interaction() -> DagResult<()> {
let tasks: Vec<_> = (0..20)
.map(|task_id| {
let counter = counter.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
(dag_id, task_id)
}
}))
})
.collect();
+207 -169
View File
@@ -1,7 +1,7 @@
//! Tests for forced execution interleaving patterns
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
@@ -14,56 +14,47 @@ async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
let barrier = Arc::new(Barrier::new(3));
let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
struct InterleaveTask {
barrier: Arc<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
let t1 = dag.add_task(task_fn({
let barrier = barrier.clone();
let order = order.clone();
move |_: ()| {
let barrier = barrier.clone();
let order = order.clone();
async move {
order.lock().push("t1_start");
barrier.wait().await;
order.lock().push("t1_end");
1
}
}
}));
let t2 = dag.add_task(task_fn({
let barrier = barrier.clone();
let order = order.clone();
move |_: ()| {
let barrier = barrier.clone();
let order = order.clone();
async move {
order.lock().push("t2_start");
barrier.wait().await;
order.lock().push("t2_end");
2
}
}
}));
let t3 = dag.add_task(task_fn({
let barrier = barrier.clone();
let order = order.clone();
move |_: ()| {
let barrier = barrier.clone();
let order = order.clone();
async move {
order.lock().push("t3_start");
barrier.wait().await;
order.lock().push("t3_end");
3
}
}
}));
let t1 = dag.add_task(InterleaveTask {
barrier: barrier.clone(),
order: order.clone(),
prefix: "t1",
value: 1,
});
let t2 = dag.add_task(InterleaveTask {
barrier: barrier.clone(),
order: order.clone(),
prefix: "t2",
value: 2,
});
let t3 = dag.add_task(InterleaveTask {
barrier: barrier.clone(),
order: order.clone(),
prefix: "t2",
value: 3,
});
// Collector
let collector = dag
.add_task(task_fn(
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(a, b, c): (&i32, &i32, &i32)| a + b + c,
))
.depends_on((t1, t2, t3));
@@ -93,76 +84,105 @@ async fn test_alternating_execution_pattern() -> DagResult<()> {
let events: Arc<parking_lot::Mutex<Vec<String>>> =
Arc::new(parking_lot::Mutex::new(Vec::new()));
// Two chains that alternate execution
let first_a = dag.add_task(task_fn({
let turn = turn.clone();
let events = events.clone();
move |_: ()| {
let turn = turn.clone();
let events = events.clone();
async move {
while turn.load(Ordering::SeqCst) != 0 {
struct FirstA {
turn: Arc<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<String>>>,
}
#[task]
impl FirstA {
async fn run(&self) -> i32 {
while self.turn.load(Ordering::SeqCst) != 0 {
tokio::task::yield_now().await;
}
events.lock().push("A0".to_string());
turn.store(1, Ordering::SeqCst);
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 first_b = dag.add_task(task_fn({
let turn = turn.clone();
let events = events.clone();
move |_: ()| {
let turn = turn.clone();
let events = events.clone();
async move {
while turn.load(Ordering::SeqCst) != 1 {
struct FirstB {
turn: Arc<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<String>>>,
}
#[task]
impl FirstB {
async fn run(&self) -> i32 {
while self.turn.load(Ordering::SeqCst) != 1 {
tokio::task::yield_now().await;
}
events.lock().push("B0".to_string());
turn.store(2, Ordering::SeqCst);
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();
for i in 1..5 {
let turn_clone = turn.clone();
let events_clone = events.clone();
chain_a = dag
.add_task(task_fn(move |x: i32| {
let turn_clone = turn_clone.clone();
let events_clone = events_clone.clone();
async move {
while turn_clone.load(Ordering::SeqCst) != i * 2 {
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;
}
events_clone.lock().push(format!("A{}", i));
turn_clone.store(i * 2 + 1, Ordering::SeqCst);
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 {
chain_a = dag
.add_task(ChainA {
events: events.clone(),
turn: turn.clone(),
idx: i,
})
.depends_on(chain_a);
let turn_clone = turn.clone();
let events_clone = events.clone();
chain_b = dag
.add_task(task_fn(move |x: i32| {
let turn_clone = turn_clone.clone();
let events_clone = events_clone.clone();
async move {
while turn_clone.load(Ordering::SeqCst) != i * 2 + 1 {
tokio::task::yield_now().await;
}
events_clone.lock().push(format!("B{}", i));
turn_clone.store(i * 2 + 2, Ordering::SeqCst);
x + 1
}
}))
.add_task(ChainB {
turn: turn.clone(),
events: events.clone(),
idx: i,
})
.depends_on(chain_b);
}
@@ -184,55 +204,62 @@ async fn test_layered_interleaving() -> DagResult<()> {
let dag = DagRunner::new();
let layer_active = Arc::new(AtomicUsize::new(0));
struct Layer1Task {
active: Arc<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
let layer1: Vec<_> = (0..4)
.map(|i| {
let active = layer_active.clone();
dag.add_task(task_fn(move |_: ()| {
let active = active.clone();
async move {
assert_eq!(active.fetch_add(1, Ordering::SeqCst), i);
tokio::task::yield_now().await;
active.fetch_sub(1, Ordering::SeqCst);
i as i32
}
}))
dag.add_task(Layer1Task {
active: layer_active.clone(),
idx: i,
})
.into()
})
.collect();
struct Layer23Task {
active: Arc<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
let layer2: Vec<_> = (0..2)
.map(|i| {
let active = layer_active.clone();
let idx = i * 2;
dag.add_task(task_fn(move |(a, b): (i32, i32)| {
let active = active.clone();
async move {
active.fetch_add(1, Ordering::SeqCst);
tokio::task::yield_now().await;
active.fetch_sub(1, Ordering::SeqCst);
a + b
}
}))
dag.add_task(Layer23Task {
active: layer_active.clone(),
})
.depends_on((&layer1[idx], &layer1[idx + 1]))
})
.collect();
// Layer 3: 1 task depends on both layer2
let layer3 = dag
.add_task(task_fn({
let active = layer_active.clone();
move |(a, b): (i32, i32)| {
let active = active.clone();
async move {
active.fetch_add(1, Ordering::SeqCst);
tokio::task::yield_now().await;
active.fetch_sub(1, Ordering::SeqCst);
a + b
}
}
}))
.add_task(Layer23Task {
active: layer_active.clone(),
})
.depends_on((&layer2[0], &layer2[1]));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -252,82 +279,93 @@ async fn test_cross_branch_synchronization() -> DagResult<()> {
// Branch A
let a1: TaskHandle<_> = dag
.add_task(task_fn({
.add_task(task_fn::<(), _, _>({
let sync = sync_point.clone();
let events = events.clone();
move |_: ()| {
let sync = sync.clone();
let events = events.clone();
async move {
events.lock().push("A1");
sync.fetch_add(1, Ordering::SeqCst);
1
}
}
}))
.into();
let a2 = dag
.add_task(task_fn({
let sync = sync_point.clone();
let events = events.clone();
move |x: i32| {
let sync = sync.clone();
let events = events.clone();
async move {
struct A2Task {
sync: Arc<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[task]
impl A2Task {
async fn run(&self, x: &i32) -> i32 {
// Wait for B1
while sync.load(Ordering::SeqCst) < 2 {
while self.sync.load(Ordering::SeqCst) < 2 {
tokio::task::yield_now().await;
}
events.lock().push("A2");
sync.fetch_add(1, Ordering::SeqCst);
self.events.lock().push("A2");
self.sync.fetch_add(1, Ordering::SeqCst);
x + 1
}
}
}))
let a2 = dag
.add_task(A2Task {
sync: sync_point.clone(),
events: events.clone(),
})
.depends_on(a1);
// Branch B
let b1 = dag.add_task(task_fn({
let sync = sync_point.clone();
let events = events.clone();
move |_: ()| {
let sync = sync.clone();
let events = events.clone();
async move {
struct B1Task {
sync: Arc<AtomicUsize>,
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[task]
impl B1Task {
async fn run(&self) -> i32 {
// Wait for A1
while sync.load(Ordering::SeqCst) < 1 {
while self.sync.load(Ordering::SeqCst) < 1 {
tokio::task::yield_now().await;
}
events.lock().push("B1");
sync.fetch_add(1, Ordering::SeqCst);
self.events.lock().push("B1");
self.sync.fetch_add(1, Ordering::SeqCst);
10
}
}
}));
let b2 = dag
.add_task(task_fn({
let sync = sync_point.clone();
let events = events.clone();
move |x: i32| {
let sync = sync.clone();
let events = events.clone();
async move {
// 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 sync.load(Ordering::SeqCst) < 3 {
while self.sync.load(Ordering::SeqCst) < 3 {
tokio::task::yield_now().await;
}
events.lock().push("B2");
self.events.lock().push("B2");
x + 1
}
}
}))
let b2 = dag
.add_task(B2Task {
events: events.clone(),
sync: sync_point.clone(),
})
.depends_on(b1);
// Merge
let merge = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&a2, &b2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
-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;
mod handles;
use dagx::*;
#[tokio::test]
async fn test_get_with_wrong_type_returns_none() {
// Verify that accessing with wrong type returns error, not panic
let dag = DagRunner::new();
// Add task that returns i32
let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32));
// Run DAG
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
.unwrap();
// Verify correct type works
assert_eq!(dag.get(int_task), Ok(42i32));
// The type safety is enforced at compile time
// We cannot create fake handles with wrong types without accessing private fields
// This is actually better - it means type safety is compile-time enforced!
}
#[tokio::test]
async fn test_handle_preserves_type() {
// Verify TaskHandle<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);
}