Update tests, examples, benches
This commit is contained in:
1 parent
0137296fb4
commit
86787b3a0d
71 files changed
+1522
-3278
No files matched your search
+21
-21
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
|
||||
@@ -119,13 +118,15 @@ async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
|
||||
// Bottom layer: 2048 nodes - convert to TaskHandles
|
||||
let mut current_layer: Vec<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((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| fut).await?;
|
||||
@@ -189,7 +190,7 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
|
||||
// Create 1000 source tasks
|
||||
let sources: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i % 10 }))
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
@@ -201,9 +202,9 @@ async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
|
||||
let idx2 = (i * 13) % 1000;
|
||||
let idx3 = (i * 17) % 1000;
|
||||
|
||||
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| async move {
|
||||
a + b + c + i as i32
|
||||
}))
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32,
|
||||
))
|
||||
.depends_on((&sources[idx1], &sources[idx2], &sources[idx3]))
|
||||
})
|
||||
.collect();
|
||||
@@ -231,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();
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,5 +1,4 @@
|
||||
// Test module for execution tests
|
||||
|
||||
mod basic;
|
||||
mod examples;
|
||||
mod results;
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod execution;
|
||||
pub mod proof;
|
||||
pub mod spawning;
|
||||
pub mod threads;
|
||||
pub mod timing;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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((¶llel1, ¶llel2));
|
||||
|
||||
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
|
||||
@@ -188,197 +219,6 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parallel_branches_with_dependencies() -> DagResult<()> {
|
||||
// Test that parallel branches execute independently but respect their own dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let branch_progress = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Source node
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
|
||||
|
||||
// Branch 1: source -> b1_1 -> b1_2
|
||||
let b1_1 = {
|
||||
let progress = branch_progress.clone();
|
||||
dag.add_task(task_fn(move |x: i32| {
|
||||
let progress = progress.clone();
|
||||
async move {
|
||||
progress
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(("b1_1_start", Instant::now()));
|
||||
sleep(Duration::from_millis(30)).await;
|
||||
progress.lock().unwrap().push(("b1_1_end", Instant::now()));
|
||||
x * 2
|
||||
}
|
||||
}))
|
||||
.depends_on(source)
|
||||
};
|
||||
|
||||
let b1_2 = {
|
||||
let progress = branch_progress.clone();
|
||||
dag.add_task(task_fn(move |x: i32| {
|
||||
let progress = progress.clone();
|
||||
async move {
|
||||
progress
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(("b1_2_start", Instant::now()));
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
progress.lock().unwrap().push(("b1_2_end", Instant::now()));
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(b1_1)
|
||||
};
|
||||
|
||||
// Branch 2: source -> b2_1 -> b2_2
|
||||
let b2_1 = {
|
||||
let progress = branch_progress.clone();
|
||||
dag.add_task(task_fn(move |x: i32| {
|
||||
let progress = progress.clone();
|
||||
async move {
|
||||
progress
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(("b2_1_start", Instant::now()));
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
progress.lock().unwrap().push(("b2_1_end", Instant::now()));
|
||||
x * 3
|
||||
}
|
||||
}))
|
||||
.depends_on(source)
|
||||
};
|
||||
|
||||
let b2_2 = {
|
||||
let progress = branch_progress.clone();
|
||||
dag.add_task(task_fn(move |x: i32| {
|
||||
let progress = progress.clone();
|
||||
async move {
|
||||
progress
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(("b2_2_start", Instant::now()));
|
||||
sleep(Duration::from_millis(30)).await;
|
||||
progress.lock().unwrap().push(("b2_2_end", Instant::now()));
|
||||
x + 2
|
||||
}
|
||||
}))
|
||||
.depends_on(b2_1)
|
||||
};
|
||||
|
||||
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(b2_2)?, 32); // (10 * 3) + 2
|
||||
|
||||
// Analyze timing to prove parallel execution
|
||||
let progress = branch_progress.lock().unwrap();
|
||||
|
||||
// Find timestamps
|
||||
let find_time = |name: &str| -> Instant {
|
||||
progress
|
||||
.iter()
|
||||
.find(|(n, _)| *n == name)
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap_or_else(|| panic!("Couldn't find {}", name))
|
||||
};
|
||||
|
||||
let b1_1_start = find_time("b1_1_start");
|
||||
let b1_1_end = find_time("b1_1_end");
|
||||
let b1_2_start = find_time("b1_2_start");
|
||||
let b2_1_start = find_time("b2_1_start");
|
||||
|
||||
// Branch 1 and Branch 2 first tasks should start nearly simultaneously
|
||||
let branch_start_diff = if b1_1_start > b2_1_start {
|
||||
b1_1_start - b2_1_start
|
||||
} else {
|
||||
b2_1_start - b1_1_start
|
||||
};
|
||||
|
||||
// Use a more generous threshold to account for coverage instrumentation overhead
|
||||
// 50ms is enough to prove they're parallel (not sequential) while being robust to overhead
|
||||
assert!(
|
||||
branch_start_diff < Duration::from_millis(50),
|
||||
"Branches should start in parallel, diff was {:?}",
|
||||
branch_start_diff
|
||||
);
|
||||
|
||||
// b1_2 should only start after b1_1 ends
|
||||
assert!(b1_2_start >= b1_1_end, "b1_2 started before b1_1 finished");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_fanout_parallel_execution() -> DagResult<()> {
|
||||
// Test that a single source can fan out to many parallel tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let max_concurrent = Arc::new(AtomicUsize::new(0));
|
||||
let current_concurrent = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
|
||||
|
||||
// Create 50 tasks that all depend on the source
|
||||
let dependents: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
let max_c = max_concurrent.clone();
|
||||
let curr_c = current_concurrent.clone();
|
||||
|
||||
dag.add_task(task_fn(move |x: i32| {
|
||||
let max_c = max_c.clone();
|
||||
let curr_c = curr_c.clone();
|
||||
async move {
|
||||
// Track concurrency
|
||||
let current = curr_c.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
||||
// Update max
|
||||
let mut prev_max = max_c.load(Ordering::SeqCst);
|
||||
while current > prev_max {
|
||||
match max_c.compare_exchange_weak(
|
||||
prev_max,
|
||||
current,
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(v) => prev_max = v,
|
||||
}
|
||||
}
|
||||
|
||||
// Do work
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
|
||||
curr_c.fetch_sub(1, Ordering::SeqCst);
|
||||
x + i
|
||||
}
|
||||
}))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
|
||||
|
||||
// Verify results
|
||||
for (i, task) in dependents.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, 42 + i as i32);
|
||||
}
|
||||
|
||||
// Check that we saw significant parallelism
|
||||
let max_seen = max_concurrent.load(Ordering::SeqCst);
|
||||
println!("Max concurrent in fanout: {}", max_seen);
|
||||
assert!(
|
||||
max_seen > 5,
|
||||
"Expected significant parallelism in fanout, got {}",
|
||||
max_seen
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
// Test extreme parallelism with 1000 parallel tasks
|
||||
@@ -388,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) })
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
@@ -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?;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user