refactor: consolidate some tests/examples/benchmarks
This commit is contained in:
1 parent
49be497964
commit
b8204c4ed9
83 files changed
+569
-10068
No files matched your search
-1348
File diff suppressed because it is too large.
Load diff
@@ -1,70 +1,11 @@
|
||||
//! Tests for extremely deep dependency chains
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 0));
|
||||
let mut current = first.into(); // Convert to TaskHandle
|
||||
|
||||
for _i in 1..1000 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&prev: &i32| prev + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 999);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 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::<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::<i32, _, _>(move |main_val: &i32| main_val * 2))
|
||||
.depends_on(main_chain);
|
||||
branches.push(branch);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(main_chain)?, 499);
|
||||
|
||||
// Check some branches
|
||||
if !branches.is_empty() {
|
||||
assert_eq!(dag.get(branches[0])?, 20); // 10 * 2
|
||||
assert_eq!(dag.get(branches[4])?, 100); // 50 * 2
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_parallel_deep_chains() -> DagResult<()> {
|
||||
// Test 10 parallel chains of depth 500 each
|
||||
@@ -223,30 +164,6 @@ async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_with_accumulator() -> DagResult<()> {
|
||||
// Test a deep chain where each node accumulates a value
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Use a simpler accumulation pattern - just sum
|
||||
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::<i64, _, _>(move |sum: &i64| sum + i as i64))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(current)?;
|
||||
assert_eq!(result, 20100); // sum of 1..=200
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
// Test a zigzag pattern: two chains that cross-reference each other
|
||||
@@ -285,25 +202,3 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(tarpaulin, ignore)] // This test is extremely resource intensive
|
||||
async fn test_10000_level_chain_stress() -> DagResult<()> {
|
||||
// Ultimate deep chain test
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<u64, _, _>(|prev: &u64| prev + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 9_999);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
//! Large scale stress tests with thousands of nodes and edges
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_10000_independent_tasks() -> DagResult<()> {
|
||||
// Test with 10,000 independent tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut tasks: Vec<_> = (0..10_000)
|
||||
.map(|i| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
// Spot check some results
|
||||
assert_eq!(dag.get(tasks.swap_remove(9_999))?, 9_999);
|
||||
assert_eq!(dag.get(tasks.swap_remove(5_000))?, 5_000);
|
||||
assert_eq!(dag.get(tasks.swap_remove(0))?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_5000_node_pyramid() -> DagResult<()> {
|
||||
// Create a pyramid structure with ~5000 nodes
|
||||
// Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Bottom layer: 2048 nodes - convert to TaskHandles
|
||||
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(
|
||||
move |_: ()| {
|
||||
if i < 1024 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let layer_sizes = vec![512, 128, 32, 8, 2];
|
||||
|
||||
for layer_size in layer_sizes {
|
||||
let mut next_layer = Vec::with_capacity(layer_size);
|
||||
let group_size = current_layer.len() / layer_size;
|
||||
|
||||
for i in 0..layer_size {
|
||||
let start = i * group_size;
|
||||
let deps: Vec<_> = current_layer[start..start + group_size.min(4)]
|
||||
.iter()
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
let task = match deps.len() {
|
||||
1 => dag
|
||||
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((deps[0], deps[1])),
|
||||
3 => dag
|
||||
.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::<(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!(),
|
||||
};
|
||||
next_layer.push(task);
|
||||
}
|
||||
current_layer = next_layer;
|
||||
}
|
||||
|
||||
// Final layer combines the last 2
|
||||
let root = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// We created 1024 ones and 1024 zeros at the bottom
|
||||
// The sum should propagate up to 1024
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0, "Expected positive result, got {}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> {
|
||||
// Create a pyramid structure with ~5000 nodes
|
||||
// Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Bottom layer: 2048 nodes - convert to TaskHandles
|
||||
let mut current_layer: Vec<dagx::TaskHandle<i32>> = (0..2048)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(
|
||||
move |_: ()| {
|
||||
if i < 1024 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let layer_sizes = vec![512, 128, 32, 8, 2];
|
||||
|
||||
for layer_size in layer_sizes {
|
||||
let mut next_layer = Vec::with_capacity(layer_size);
|
||||
let group_size = current_layer.len() / layer_size;
|
||||
|
||||
for i in 0..layer_size {
|
||||
let start = i * group_size;
|
||||
let deps: Vec<_> = current_layer[start..start + group_size.min(4)]
|
||||
.iter()
|
||||
.take(4)
|
||||
.collect();
|
||||
|
||||
let task = match deps.len() {
|
||||
1 => dag
|
||||
.add_task(task_fn::<i32, _, _>(|&a: &i32| a))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((deps[0], deps[1])),
|
||||
3 => dag
|
||||
.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::<(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!(),
|
||||
};
|
||||
next_layer.push(task);
|
||||
}
|
||||
current_layer = next_layer;
|
||||
}
|
||||
|
||||
// Final layer combines the last 2
|
||||
let root = dag
|
||||
.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?;
|
||||
|
||||
// We created 1024 ones and 1024 zeros at the bottom
|
||||
// The sum should propagate up to 1024
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0, "Expected positive result, got {}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> {
|
||||
// Test a very wide DAG with 1000 sources and 1000 sinks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 1000 source tasks
|
||||
let sources: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create 1000 sink tasks, each depending on 3 random sources
|
||||
let sinks: Vec<_> = (0..1000)
|
||||
.map(|i| {
|
||||
let idx1 = (i * 7) % 1000;
|
||||
let idx2 = (i * 13) % 1000;
|
||||
let idx3 = (i * 17) % 1000;
|
||||
|
||||
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();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some sinks completed correctly
|
||||
let result0 = dag.get(sinks[0])?;
|
||||
let result500 = dag.get(sinks[500])?;
|
||||
let result999 = dag.get(sinks[999])?;
|
||||
|
||||
assert!(result0 >= 0);
|
||||
assert!(result500 >= 500);
|
||||
assert!(result999 >= 999);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
|
||||
// Test 10000 nodes arranged as 100 parallel chains of 100 nodes each
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut chain_ends = Vec::new();
|
||||
|
||||
for chain_id in 0..100 {
|
||||
let first = dag.add_task(task_fn::<(), _, _>({
|
||||
let counter = counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
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::<i32, _, _>({
|
||||
let counter = counter.clone();
|
||||
move |&prev: &i32| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
prev + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
chain_ends.push(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// All 10,000 tasks should have executed
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
// Each chain should end with start + 99
|
||||
for (chain_id, &chain_end) in chain_ends.iter().enumerate() {
|
||||
let expected = chain_id as i32 * 100 + 99;
|
||||
assert_eq!(dag.get(chain_end)?, expected);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(tarpaulin, ignore)] // This test is very resource intensive
|
||||
async fn test_100000_nodes_stress() -> DagResult<()> {
|
||||
// Ultimate stress test: 100,000 nodes
|
||||
let dag = DagRunner::new();
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 100,000 tasks in batches
|
||||
let batch_size = 1000;
|
||||
let num_batches = 100;
|
||||
|
||||
for batch in 0..num_batches {
|
||||
let _: Vec<_> = (0..batch_size)
|
||||
.map(|i| {
|
||||
let completed = completed.clone();
|
||||
let task_id = batch * batch_size + i;
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let completed = completed.clone();
|
||||
completed.fetch_add(1, Ordering::Relaxed);
|
||||
task_id
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(completed.load(Ordering::Relaxed), 100_000);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,6 +4,5 @@
|
||||
//! by normal usage patterns.
|
||||
|
||||
mod deep_chains;
|
||||
mod large_scale;
|
||||
mod type_limits;
|
||||
mod zero_tasks;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for type system edge cases and limits
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::marker::PhantomData;
|
||||
@@ -108,89 +108,6 @@ async fn test_unit_type_chains() -> DagResult<()> {
|
||||
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::<(), _, _>(|_: ()| "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::<(), _, _>(|_: ()| 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::<
|
||||
(
|
||||
&str,
|
||||
String,
|
||||
std::borrow::Cow<str>,
|
||||
Box<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
),
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
|(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));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(combined)?;
|
||||
assert!(result.contains("static str"));
|
||||
assert!(result.contains("owned string"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generic_type_constraints() -> DagResult<()> {
|
||||
// Test with types that have trait bounds
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Wrapper<T: Clone + Send + 'static>(T);
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<
|
||||
(Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>),
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
|(w1, w2, w3): (&Wrapper<i32>, &Wrapper<&str>, &Wrapper<Vec<i32>>)| {
|
||||
format!("{:?} {:?} {:?}", w1, w2, w3)
|
||||
},
|
||||
))
|
||||
.depends_on((t1, t2, t3));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(combined)?;
|
||||
assert!(result.contains("Wrapper(42)"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
// Test types that wrap references
|
||||
@@ -216,71 +133,3 @@ async fn test_reference_wrapper_types() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_array_types() -> DagResult<()> {
|
||||
// Test with fixed-size arrays
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<([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| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t3)?, 55);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_enum_variants() -> DagResult<()> {
|
||||
// Test with various enum types
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MyEnum {
|
||||
Unit,
|
||||
Tuple(i32, String),
|
||||
Struct { x: i32, y: i32 },
|
||||
LargeVariant([u8; 1024]),
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 }));
|
||||
let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024])));
|
||||
|
||||
let combined = dag
|
||||
.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 })
|
||||
&& matches!(e4, MyEnum::LargeVariant(_))
|
||||
},
|
||||
))
|
||||
.depends_on((t1, t2, t3, t4));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert!(dag.get(combined)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,89 +1,10 @@
|
||||
//! Tests for empty DAGs and minimal task configurations
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::task::yield_now;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag_execution() -> DagResult<()> {
|
||||
// Test that an empty DAG can be created and run without issues
|
||||
let dag = DagRunner::new();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Should complete successfully with no tasks
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 42));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 42);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_unit_input_output() -> DagResult<()> {
|
||||
// Test a task that takes unit and returns unit
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let executed = Arc::new(AtomicUsize::new(0));
|
||||
let exec_clone = executed.clone();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let exec = exec_clone.clone();
|
||||
exec.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
dag.get(task)?; // Just verify it doesn't error
|
||||
assert_eq!(executed.load(Ordering::SeqCst), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zero_sized_type_task() -> DagResult<()> {
|
||||
// Test with zero-sized types
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ZeroSized;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| ZeroSized));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, ZeroSized);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag_multiple_runs() -> DagResult<()> {
|
||||
// Test that an empty DAG can be run multiple times
|
||||
let dag = DagRunner::new();
|
||||
|
||||
for _ in 0..10 {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
||||
// Test a DAG with multiple source tasks but no dependencies
|
||||
@@ -127,47 +48,6 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> {
|
||||
// Test alternating between empty DAG and DAG with tasks
|
||||
for i in 0..5 {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
if i % 2 == 0 {
|
||||
// Empty DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
} else {
|
||||
// DAG with a task
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_never_type_simulation() -> DagResult<()> {
|
||||
// Test handling of tasks that conceptually "never" complete
|
||||
// (simulated by immediate completion with special marker)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Never {}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// This task completes immediately but returns a Result containing "never" type
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, Never>::Ok(42)));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, Ok(42));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> {
|
||||
// Test a task that does the absolute minimum async work
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
// Common test utilities and helpers for the test suite
|
||||
|
||||
// Re-export task_fn for internal testing
|
||||
// Note: task_fn is #[doc(hidden)] and not part of the documented public API
|
||||
pub use dagx::task_fn;
|
||||
|
||||
// Re-export commonly used test structs
|
||||
#[allow(dead_code)]
|
||||
pub mod tasks {
|
||||
use dagx::task;
|
||||
|
||||
// Simple seed task with mutable state
|
||||
pub struct Seed {
|
||||
pub value: i32,
|
||||
}
|
||||
|
||||
impl Seed {
|
||||
pub fn new(v: i32) -> Self {
|
||||
Self { value: v }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Seed {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless increment task
|
||||
pub struct Inc;
|
||||
|
||||
impl Inc {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Inc {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless add task
|
||||
pub struct Add;
|
||||
|
||||
impl Add {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
// Stateless multiply task
|
||||
pub struct Mul;
|
||||
|
||||
impl Mul {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Mul {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a * b
|
||||
}
|
||||
}
|
||||
|
||||
// Stateful counter task
|
||||
pub struct Counter {
|
||||
pub count: i32,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new(initial: i32) -> Self {
|
||||
Self { count: initial }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Counter {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.count += input;
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
// Task that outputs multiple types combined as string
|
||||
pub struct OutputResult;
|
||||
|
||||
impl OutputResult {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl OutputResult {
|
||||
async fn run(s: &String, n: &usize, f: &bool) -> String {
|
||||
format!("{s}:{n}:{f}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Test that custom types work without any trait implementations
|
||||
// Test that custom types work with the #[task] macro
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -84,28 +84,6 @@ async fn test_custom_type_passthrough() {
|
||||
assert_eq!(company_result.employees.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_diamond_pattern() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source
|
||||
let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into();
|
||||
|
||||
// Two paths from source
|
||||
let path_a = dag.add_task(ProcessPerson).depends_on(fetch);
|
||||
let path_b = dag.add_task(PersonToCompany).depends_on(fetch);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result_a = dag.get(path_a).unwrap();
|
||||
let result_b = dag.get(path_b).unwrap();
|
||||
|
||||
assert_eq!(result_a.as_str(), "Alice is 30 years old");
|
||||
assert_eq!(result_b.name, "Alice's Company");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_type_fan_out() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Dependency tests module
|
||||
|
||||
pub mod resolution;
|
||||
pub mod single;
|
||||
pub mod tuples;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for dependency resolution order and transitive dependencies
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -99,68 +99,3 @@ async fn test_dependency_resolution_order() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_transitive_dependencies() -> DagResult<()> {
|
||||
// Test that transitive dependencies work correctly
|
||||
// A -> B -> C (creates transitive A -> C relationship)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(b);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(a)?, 10);
|
||||
assert_eq!(dag.get(b)?, 20);
|
||||
assert_eq!(dag.get(c)?, 25); // 20 + 5
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_50_levels() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<i32, _, _>(move |&x: &i32| x + 1))
|
||||
.depends_on(handles.last().unwrap());
|
||||
handles.push(next);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(handles.last().unwrap())?, 50);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_200_levels() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let mut current = first.into();
|
||||
|
||||
for _ in 0..200 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 201);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
//! Tests for single dependencies and basic dependency data flow
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_dependency() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let dependent = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(dependent)?, 43);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependency_data_flow() -> DagResult<()> {
|
||||
// Test that data flows correctly through dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
|
||||
.into();
|
||||
|
||||
let sum = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
|
||||
v.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(source);
|
||||
|
||||
let product = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|v: &Vec<i32>| {
|
||||
v.iter().product::<i32>()
|
||||
}))
|
||||
.depends_on(source);
|
||||
|
||||
let final_result = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(s, p): (&i32, &i32)| s + p))
|
||||
.depends_on((&sum, &product));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Note: sum and product are not sinks (final_result depends on them)
|
||||
// Only final_result is a sink and can be retrieved
|
||||
assert_eq!(dag.get(final_result)?, 135); // 15+120
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 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::<(i32, String, bool), _, _>(
|
||||
|(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b),
|
||||
))
|
||||
.depends_on((int_source, string_source, bool_source));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(combined)?, "hello: 42 (true)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Create 10 tasks that all depend on the shared node
|
||||
let dependents: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * (i + 1)))
|
||||
.depends_on(shared)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all dependents got the correct value from shared
|
||||
for (i, handle) in dependents.iter().enumerate() {
|
||||
assert_eq!(dag.get(handle)?, 42 * (i as i32 + 1));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_source_nodes() -> DagResult<()> {
|
||||
// Test DAG with multiple source nodes (no dependencies)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sources: Vec<_> = (0..20)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
for (i, source) in sources.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(source)?, i as i32 * 10);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for tuple dependencies (2-8 dependencies)
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
//! Tests for different error types and error handling
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagError, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_not_found_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Try to get result before running
|
||||
let result = dag.get(t1);
|
||||
assert!(matches!(result, Err(DagError::ResultNotFound { .. })));
|
||||
|
||||
// Run and verify it works after
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t1).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_result_error_handling() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task that returns a Result
|
||||
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, String>::Err("Internal error".to_string())
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, String>::Ok(42)));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Both tasks complete successfully (they return Results)
|
||||
assert_eq!(dag.get(task1).unwrap(), Err("Internal error".to_string()));
|
||||
assert_eq!(dag.get(task2).unwrap(), Ok(42));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_option_none_not_error() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::<i32>));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// None is a valid result, not an error
|
||||
assert_eq!(dag.get(task).unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_error_types() {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CustomError {
|
||||
ValidationError(String),
|
||||
ProcessingError { code: i32, details: String },
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string()))
|
||||
}));
|
||||
|
||||
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
Result::<i32, CustomError>::Err(CustomError::ProcessingError {
|
||||
code: 500,
|
||||
details: "Processing failed".to_string(),
|
||||
})
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check custom errors are preserved
|
||||
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().as_ref() {
|
||||
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500),
|
||||
_ => panic!("Wrong error type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_tuple_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((ok_task, err_task));
|
||||
|
||||
let result = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(dag.get(dependent).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_panic_vs_structured_panic() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("String panic message");
|
||||
}));
|
||||
|
||||
let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Formatted panic: value={}, code={}", 42, "ERROR");
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Both should result in errors
|
||||
assert!(dag.get(string_panic).is_err());
|
||||
assert!(dag.get(formatted_panic).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| {
|
||||
// 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());
|
||||
|
||||
outer
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the chained error
|
||||
let result = dag.get(task).unwrap();
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Err(e) = result.as_ref() {
|
||||
assert_eq!(e, "Outer error");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,5 @@
|
||||
//!
|
||||
//! Tests for error scenarios and recovery patterns.
|
||||
|
||||
mod error_types;
|
||||
mod propagation;
|
||||
mod recovery;
|
||||
+1
-178
@@ -1,9 +1,8 @@
|
||||
//! Tests for error propagation through the DAG
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@@ -76,100 +75,6 @@ async fn test_diamond_error_propagation() {
|
||||
assert!(dag.get(sink).is_err()); // Fails due to right dependency
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_stops_at_boundary() {
|
||||
let dag = DagRunner::new();
|
||||
let execution_tracker = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Branch with error
|
||||
let error_source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| -> i32 {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(1, Ordering::SeqCst);
|
||||
panic!("Error source");
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
|
||||
let error_dependent = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: &i32| {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
|
||||
42
|
||||
}
|
||||
}))
|
||||
.depends_on(error_source);
|
||||
|
||||
// Independent branch
|
||||
let independent = dag.add_task(task_fn::<(), _, _>({
|
||||
let tracker = execution_tracker.clone();
|
||||
move |_: ()| {
|
||||
let tracker = tracker.clone();
|
||||
tracker.fetch_add(10, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Error branch: source runs but dependent doesn't
|
||||
assert!(dag.get(error_source).is_err());
|
||||
assert!(dag.get(error_dependent).is_err());
|
||||
|
||||
// Independent branch runs
|
||||
assert_eq!(dag.get(independent).unwrap(), 10);
|
||||
|
||||
// Check execution: 1 (error_source) + 10 (independent) = 11
|
||||
assert_eq!(execution_tracker.load(Ordering::SeqCst), 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_error_sources_convergence() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Multiple error sources
|
||||
let error1 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Error source 1");
|
||||
}))
|
||||
.into();
|
||||
|
||||
let error2 = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Error source 2");
|
||||
}))
|
||||
.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), _, _>(
|
||||
|(_, _, _): (&i32, &i32, &i32)| 100,
|
||||
))
|
||||
.depends_on((&error1, &error2, &success));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// All error sources fail
|
||||
assert!(dag.get(error1).is_err());
|
||||
assert!(dag.get(error2).is_err());
|
||||
|
||||
// Success task works
|
||||
assert_eq!(dag.get(success).unwrap(), 42);
|
||||
|
||||
// Convergence fails due to any error dependency
|
||||
assert!(dag.get(convergence).is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_in_wide_fanout() {
|
||||
let dag = DagRunner::new();
|
||||
@@ -312,85 +217,3 @@ async fn test_error_propagation_timing() {
|
||||
assert!(order.contains(&"t2_start"));
|
||||
assert!(!order.contains(&"t3_should_not_run"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_branch_failure_propagation() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// root
|
||||
// / | \
|
||||
// a1 b1 c1
|
||||
// | | |
|
||||
// a2 b2 c2
|
||||
// | (err) |
|
||||
// a3 b3 c3
|
||||
// \ | /
|
||||
// sink
|
||||
|
||||
let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
// Branch A
|
||||
let a1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(root);
|
||||
let a2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(a1);
|
||||
let a3 = dag
|
||||
.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::<i32, _, _>(|&x: &i32| Ok::<i32, &str>(x * 2)))
|
||||
.depends_on(root);
|
||||
let b2 = dag
|
||||
.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::<Result<_, _>, _, _>(|x: &Result<i32, &str>| {
|
||||
x.map(|v| v * 3)
|
||||
}))
|
||||
.depends_on(b2);
|
||||
|
||||
// Branch C
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
|
||||
.depends_on(root);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
|
||||
.depends_on(c1);
|
||||
let c3 = dag
|
||||
.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::<(i32, Result<_, _>, _), _, _>(
|
||||
|(a, b, c): (&i32, &Result<i32, &str>, &i32)| match b {
|
||||
Ok(b_val) => Ok(a + b_val + c),
|
||||
Err(e) => Err(*e),
|
||||
},
|
||||
))
|
||||
.depends_on((&a3, &b3, &c3));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Branch A succeeds
|
||||
assert_eq!(dag.get(a3).unwrap(), 106);
|
||||
|
||||
// Branch B fails at b2
|
||||
assert_eq!(dag.get(b1).unwrap(), Ok(200));
|
||||
assert_eq!(dag.get(b2).unwrap(), Err("Branch B fails at b2"));
|
||||
assert_eq!(dag.get(b3).unwrap(), Err("Branch B fails at b2")); // Propagated error
|
||||
|
||||
// Branch C succeeds
|
||||
assert_eq!(dag.get(c3).unwrap(), 94);
|
||||
|
||||
// Sink fails due to branch B
|
||||
assert_eq!(dag.get(sink).unwrap(), Err("Branch B fails at b2"));
|
||||
}
|
||||
+1
-129
@@ -1,42 +1,11 @@
|
||||
//! Tests for error recovery patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fallback_on_error() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Primary task that fails
|
||||
let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
panic!("Primary task failed");
|
||||
#[allow(unreachable_code)]
|
||||
42
|
||||
}));
|
||||
|
||||
// Fallback task
|
||||
let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
// Selector that uses fallback if primary fails
|
||||
let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {}));
|
||||
|
||||
// Run DAG
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Primary should fail
|
||||
assert!(dag.get(primary).is_err());
|
||||
|
||||
// Fallback should succeed
|
||||
assert_eq!(dag.get(fallback)?, 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retry_pattern_simulation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -113,38 +82,6 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_partial_recovery_with_defaults() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Some tasks that might fail
|
||||
let _tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
if i % 3 == 0 {
|
||||
panic!("Task {} failed", i);
|
||||
}
|
||||
i * 10
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregator that handles failures gracefully
|
||||
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
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
let result = dag.get(aggregator)?;
|
||||
assert_eq!(result.len(), 10);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -183,36 +120,6 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_graceful_degradation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Critical task
|
||||
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::<(), _, _>(|_: ()| {
|
||||
Err::<Vec<i32>, &str>("Enhancement failed")
|
||||
}));
|
||||
|
||||
// Final task that works with what's available
|
||||
let final_task = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|base: &Vec<i32>| {
|
||||
// Would combine with enhancement if available
|
||||
// Here just uses base
|
||||
base.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(critical);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Critical path works
|
||||
assert_eq!(dag.get(final_task)?, 15);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
@@ -248,38 +155,3 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compensation_action() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let compensation_triggered = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Action that fails
|
||||
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 triggered = compensation_triggered.clone();
|
||||
move |_: ()| {
|
||||
let triggered = triggered.clone();
|
||||
triggered.store(true, Ordering::SeqCst);
|
||||
"Compensation executed"
|
||||
}
|
||||
}));
|
||||
|
||||
let _ = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
|
||||
// Action failed
|
||||
assert!(dag.get(action).is_err());
|
||||
|
||||
// Compensation ran successfully
|
||||
assert_eq!(dag.get(compensation)?, "Compensation executed");
|
||||
assert!(compensation_triggered.load(Ordering::SeqCst));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// Basic execution tests for DagRunner
|
||||
use crate::common::task_fn;
|
||||
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag() {
|
||||
let dag = DagRunner::new();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(b);
|
||||
let d = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(c);
|
||||
let e = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(d);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, c, d are not sinks (they have dependents in the chain)
|
||||
// Only e is a sink and can be retrieved
|
||||
assert_eq!(dag.get(e).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_parallel() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, and c are not sinks (they have dependents)
|
||||
// Only d is a sink and can be retrieved
|
||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_output_types() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||
let user = dag.get(struct_task).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
assert_eq!(user.age, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_sinks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(branch2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should wait for both sinks
|
||||
|
||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_inline_path() {
|
||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Single task in its own layer
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
// Create multiple tasks in the same layer (all depend on source)
|
||||
let t1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(source);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
|
||||
.depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t1).unwrap(), 101);
|
||||
assert_eq!(dag.get(t2).unwrap(), 102);
|
||||
assert_eq!(dag.get(t3).unwrap(), 103);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_producer_consumer_channels() {
|
||||
// Test channel creation and management (lines 301-337 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Producer
|
||||
let producer: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]))
|
||||
.into();
|
||||
|
||||
// Consumer
|
||||
let consumer = dag
|
||||
.add_task(task_fn::<Vec<_>, _, _>(|data: &Vec<i32>| {
|
||||
data.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(consumer).unwrap(), 15);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multi_consumer_fanout() {
|
||||
// Test fanout with multiple consumers from one producer
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Multiple consumers
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(producer);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(producer);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compute_layers_with_dependents_tracking() {
|
||||
// Test layer computation with dependent tracking (lines 590-600 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create diamond pattern to ensure dependents are tracked correctly
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(source);
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layer_execution_order() {
|
||||
// Test that layers execute in correct topological order
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let order1 = execution_order.clone();
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let order = order1.clone();
|
||||
order.lock().push(1);
|
||||
1
|
||||
}));
|
||||
|
||||
let order2 = execution_order.clone();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order2.clone();
|
||||
order.lock().push(2);
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(t1);
|
||||
|
||||
let order3 = execution_order.clone();
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order3.clone();
|
||||
order.lock().push(3);
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let order = execution_order.lock().clone();
|
||||
assert_eq!(order, vec![1, 2, 3]);
|
||||
assert_eq!(dag.get(t3).unwrap(), 3);
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
//! Integration tests for end-to-end functionality
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, Task};
|
||||
|
||||
use std::time::Instant;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// === Shared Test Tasks ===
|
||||
|
||||
struct LoadValue {
|
||||
value: i32,
|
||||
}
|
||||
impl LoadValue {
|
||||
fn new(value: i32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
#[dagx::task]
|
||||
impl LoadValue {
|
||||
async fn run(&self) -> i32 {
|
||||
self.value // Read-only state: use &self
|
||||
}
|
||||
}
|
||||
|
||||
struct Multiply {
|
||||
factor: i32,
|
||||
}
|
||||
impl Multiply {
|
||||
fn new(factor: i32) -> Self {
|
||||
Self { factor }
|
||||
}
|
||||
}
|
||||
#[dagx::task]
|
||||
impl Multiply {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.factor // Read-only state: use &self
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b // Stateless: no self needed
|
||||
}
|
||||
}
|
||||
|
||||
// === Integration Tests ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_etl_pipeline() -> DagResult<()> {
|
||||
// Simulate an Extract-Transform-Load pipeline
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Extract: Load data from sources
|
||||
let source1 = dag.add_task(LoadValue::new(10));
|
||||
let source2 = dag.add_task(LoadValue::new(20));
|
||||
let source3 = dag.add_task(LoadValue::new(30));
|
||||
|
||||
// Transform: Process each source
|
||||
let transform1 = dag.add_task(Multiply::new(2)).depends_on(source1);
|
||||
let transform2 = dag.add_task(Multiply::new(3)).depends_on(source2);
|
||||
let transform3 = dag.add_task(Multiply::new(4)).depends_on(source3);
|
||||
|
||||
// Load: Aggregate results
|
||||
let combine12 = dag.add_task(Add).depends_on((&transform1, &transform2));
|
||||
let final_result = dag.add_task(Add).depends_on((&combine12, &transform3));
|
||||
|
||||
// Execute pipeline
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify
|
||||
let result = dag.get(final_result)?;
|
||||
assert_eq!(result, (10 * 2) + (20 * 3) + (30 * 4)); // 20 + 60 + 120 = 200
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_multi_layer_dag() -> DagResult<()> {
|
||||
// Complex multi-layer DAG
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 1: Sources
|
||||
let a = dag.add_task(LoadValue::new(1));
|
||||
let b = dag.add_task(LoadValue::new(2));
|
||||
let c = dag.add_task(LoadValue::new(3));
|
||||
|
||||
// Layer 2: First transformations
|
||||
let ab = dag.add_task(Add).depends_on((&a, &b));
|
||||
let bc = dag.add_task(Add).depends_on((&b, &c));
|
||||
|
||||
// Layer 3: Second transformations
|
||||
let double_ab = dag.add_task(Multiply::new(2)).depends_on(ab);
|
||||
let double_bc = dag.add_task(Multiply::new(2)).depends_on(bc);
|
||||
|
||||
// Layer 4: Final result
|
||||
let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_result)?;
|
||||
// (1+2)*2 + (2+3)*2 = 6 + 10 = 16
|
||||
assert_eq!(result, 16);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_multiple_sinks() -> DagResult<()> {
|
||||
// DAG with multiple output sinks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(10));
|
||||
|
||||
// Multiple independent branches
|
||||
let branch1 = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let branch2 = dag.add_task(Multiply::new(3)).depends_on(source);
|
||||
let branch3 = dag.add_task(Multiply::new(4)).depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// All branches should complete
|
||||
assert_eq!(dag.get(branch1)?, 20);
|
||||
assert_eq!(dag.get(branch2)?, 30);
|
||||
assert_eq!(dag.get(branch3)?, 40);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_diamond_cascade() -> DagResult<()> {
|
||||
// Multiple diamond patterns in sequence
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Diamond 1
|
||||
let source1 = dag.add_task(LoadValue::new(10));
|
||||
let left1 = dag.add_task(Multiply::new(2)).depends_on(source1);
|
||||
let right1 = dag.add_task(Multiply::new(3)).depends_on(source1);
|
||||
let sink1 = dag.add_task(Add).depends_on((&left1, &right1));
|
||||
|
||||
// Diamond 2 (depends on Diamond 1)
|
||||
let left2 = dag.add_task(Multiply::new(2)).depends_on(sink1);
|
||||
let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1);
|
||||
let sink2 = dag.add_task(Add).depends_on((&left2, &right2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(sink2)?;
|
||||
// Diamond 1: (10*2) + (10*3) = 50
|
||||
// Diamond 2: (50*2) + (50*3) = 250
|
||||
assert_eq!(result, 250);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_wide_and_deep() -> DagResult<()> {
|
||||
// Combination of wide (parallel) and deep (sequential) patterns
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Wide: 5 parallel sources
|
||||
let sources: Vec<_> = (0..5)
|
||||
.map(|i| dag.add_task(LoadValue::new(i * 10)))
|
||||
.collect();
|
||||
|
||||
// Deep: Chain each source through 3 transformations
|
||||
let mut final_nodes = Vec::new();
|
||||
for source in &sources {
|
||||
let step1 = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let step2 = dag
|
||||
.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);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify each chain
|
||||
for (i, node) in final_nodes.iter().enumerate() {
|
||||
let expected = ((i as i32 * 10) * 2 + 5) * 3;
|
||||
assert_eq!(dag.get(node)?, expected);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_error_recovery() -> DagResult<()> {
|
||||
// Test that DAG handles normal execution gracefully
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(42));
|
||||
let transform = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Should be able to access results
|
||||
assert_eq!(dag.get(transform)?, 84);
|
||||
|
||||
// Getting valid task should return ok
|
||||
let result = dag.get(transform);
|
||||
assert!(result.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_large_scale() -> DagResult<()> {
|
||||
// Large-scale integration test
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 100 sources
|
||||
let sources: Vec<_> = (0..100).map(|i| dag.add_task(LoadValue::new(i))).collect();
|
||||
|
||||
// Create 50 aggregation tasks (each combines 2 sources)
|
||||
let aggregations: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
dag.add_task(Add)
|
||||
.depends_on((&sources[i * 2], &sources[i * 2 + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create 25 second-level aggregations
|
||||
let second_level: Vec<_> = (0..25)
|
||||
.map(|i| {
|
||||
dag.add_task(Add)
|
||||
.depends_on((&aggregations[i * 2], &aggregations[i * 2 + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some results
|
||||
assert_eq!(dag.get(aggregations[0])?, 1); // First two
|
||||
assert_eq!(dag.get(second_level[0])?, 1 + (2 + 3)); // First four
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_reused_values() -> DagResult<()> {
|
||||
// Test that a single source can be used by multiple consumers
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(LoadValue::new(100));
|
||||
|
||||
// Use source in multiple different ways
|
||||
let double = dag.add_task(Multiply::new(2)).depends_on(source);
|
||||
let triple = dag.add_task(Multiply::new(3)).depends_on(source);
|
||||
let quadruple = dag.add_task(Multiply::new(4)).depends_on(source);
|
||||
|
||||
// Combine some of the results
|
||||
let combined = dag.add_task(Add).depends_on((&double, &triple));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(double)?, 200);
|
||||
assert_eq!(dag.get(triple)?, 300);
|
||||
assert_eq!(dag.get(quadruple)?, 400);
|
||||
assert_eq!(dag.get(combined)?, 500);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_parallel_execution_timing() -> DagResult<()> {
|
||||
// Test that independent tasks actually run in parallel by measuring execution time
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sleep_duration = Duration::from_millis(100);
|
||||
let num_parallel_tasks = 5;
|
||||
|
||||
// Create 5 independent tasks that each sleep for 100ms
|
||||
let tasks: Vec<_> = (0..num_parallel_tasks)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
sleep(sleep_duration).await;
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify all tasks completed
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
|
||||
// If tasks ran sequentially, it would take 5 * 100ms = 500ms
|
||||
// With parallelism, it should take ~100ms (plus overhead)
|
||||
// We'll check it's less than 300ms (generous margin for CI environments)
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(300),
|
||||
"Parallel execution took {:?}, expected < 300ms (sequential would be 500ms)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> {
|
||||
// Test mixed parallel and sequential execution patterns
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let sleep_duration = Duration::from_millis(50);
|
||||
|
||||
// Layer 1: 4 parallel tasks (should take ~50ms)
|
||||
let parallel_tasks: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
sleep(sleep_duration).await;
|
||||
i * 10
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Sequential chain of 3 tasks depending on the first parallel task
|
||||
// (should add ~150ms)
|
||||
let seq1 = dag
|
||||
.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::<i32, _, _>(move |&x: &i32| {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(seq1);
|
||||
|
||||
let seq3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(seq2);
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(seq3)?, 3); // 0 + 1 + 1 + 1
|
||||
|
||||
// Expected time: ~200ms (4 layers of 50ms each: parallel layer + 3 sequential)
|
||||
// If everything ran sequentially: 7 * 50ms = 350ms
|
||||
// We'll check it's less than 300ms
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(300),
|
||||
"Execution took {:?}, expected < 300ms (fully sequential would be 350ms)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
// Also verify it took more than just the parallel layer
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(150),
|
||||
"Execution took {:?}, expected >= 150ms (should include sequential chain)",
|
||||
elapsed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+168
-3
@@ -1,4 +1,169 @@
|
||||
// Test module for execution tests
|
||||
// Basic execution tests for DagRunner
|
||||
use dagx::task_fn;
|
||||
|
||||
mod basic;
|
||||
mod results;
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_dag() {
|
||||
let dag = DagRunner::new();
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_wide_parallel() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(a);
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(a);
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Note: a, b, and c are not sinks (they have dependents)
|
||||
// Only d is a sink and can be retrieved
|
||||
assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_output_types() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(string_task).unwrap(), "hello");
|
||||
assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]);
|
||||
let user = dag.get(struct_task).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
assert_eq!(user.age, 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_sinks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(branch2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap(); // Should wait for both sinks
|
||||
|
||||
assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1
|
||||
assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task_inline_path() {
|
||||
// Test single-task layer inline execution path (line 367-410 in runner.rs)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Single task in its own layer
|
||||
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into();
|
||||
let t2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
|
||||
.depends_on(t2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 42)).into();
|
||||
|
||||
// Multiple consumers
|
||||
let c1 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(producer);
|
||||
let c2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(producer);
|
||||
let c3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(producer);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(c1).unwrap(), 84);
|
||||
assert_eq!(dag.get(c2).unwrap(), 52);
|
||||
assert_eq!(dag.get(c3).unwrap(), 37);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Result and error handling tests for DagRunner
|
||||
use crate::common::task_fn;
|
||||
|
||||
use crate::common::tasks::Counter;
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_type() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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())
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(success_task).unwrap(), Ok(42));
|
||||
assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_result_type_works() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
|
||||
// Test that DagResult<()> works with ? operator pattern
|
||||
async fn run_dag(dag: &DagRunner) -> DagResult<()> {
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let result = run_dag(&dag).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(dag.get(task).unwrap(), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_not_executed_error() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Try to get result before running - should error
|
||||
let result = dag.get(task);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(DagError::ResultNotFound { task_id }) => {
|
||||
assert_eq!(task_id, 0); // First task has ID 0
|
||||
}
|
||||
_ => panic!("Expected ResultNotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_messages_are_helpful() {
|
||||
// Test ResultNotFound error message
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
let result = dag.get(task);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{}", result.unwrap_err());
|
||||
assert!(err_msg.contains("Result not found"));
|
||||
assert!(err_msg.contains("task #0")); // Task ID
|
||||
assert!(err_msg.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_successful_execution_returns_ok() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
// Should return Ok(())
|
||||
let result = dag
|
||||
.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should be able to get value
|
||||
assert_eq!(dag.get(task).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateful_tasks() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let counter = dag.add_task(Counter::new(10)).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5
|
||||
}
|
||||
|
||||
// Cycle detection test removed - it required access to private fields
|
||||
// The public API correctly prevents creating cycles, so this is tested implicitly
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_captured_state() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let multiplier = 7;
|
||||
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let scaled = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
|
||||
.depends_on(base);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(scaled).unwrap(), 35);
|
||||
}
|
||||
@@ -4,18 +4,11 @@
|
||||
#![allow(clippy::needless_borrow)]
|
||||
#![allow(clippy::redundant_clone)]
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
mod boundaries;
|
||||
mod dependencies;
|
||||
mod errors;
|
||||
mod execution;
|
||||
mod parallelism;
|
||||
mod patterns;
|
||||
mod performance;
|
||||
mod runtimes;
|
||||
mod task_patterns;
|
||||
mod timing;
|
||||
mod tracing;
|
||||
mod type_safety;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Concurrent access tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
//! Tests for execution correctness and parallelism
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
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)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_completion_order_matches_dependencies() -> DagResult<()> {
|
||||
// Verify that tasks complete in dependency order
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let completion_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
self.order.lock().unwrap().push("A");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Create a complex DAG with clear ordering requirements
|
||||
let a = dag.add_task(ATask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
self.order.lock().unwrap().push("B");
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag.add_task(BTask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
let c = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("C");
|
||||
x + y
|
||||
}))
|
||||
.depends_on((a, b))
|
||||
};
|
||||
|
||||
let d = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x * 2
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2
|
||||
|
||||
// Check completion order
|
||||
let order = completion_order.lock().unwrap();
|
||||
|
||||
// A and B can complete in any order (parallel)
|
||||
assert!(order.contains(&"A"));
|
||||
assert!(order.contains(&"B"));
|
||||
|
||||
// C must come after both A and B
|
||||
let c_pos = order.iter().position(|name| *name == "C").unwrap();
|
||||
let a_pos = order.iter().position(|name| *name == "A").unwrap();
|
||||
let b_pos = order.iter().position(|name| *name == "B").unwrap();
|
||||
assert!(c_pos > a_pos, "C should complete after A");
|
||||
assert!(c_pos > b_pos, "C should complete after B");
|
||||
|
||||
// D must come after C
|
||||
let d_pos = order.iter().position(|name| *name == "D").unwrap();
|
||||
assert!(d_pos > c_pos, "D should complete after C");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
|
||||
// Prove that dependent tasks wait for their dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
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();
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("B");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag
|
||||
.add_task(BTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(a);
|
||||
|
||||
struct CTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl CTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("C");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let c = dag
|
||||
.add_task(CTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(b);
|
||||
|
||||
let d = {
|
||||
let order = execution_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(a)?, 1);
|
||||
assert_eq!(dag.get(b)?, 2);
|
||||
assert_eq!(dag.get(c)?, 3);
|
||||
assert_eq!(dag.get(d)?, 4);
|
||||
|
||||
// Verify execution order
|
||||
let order = execution_order.lock().unwrap();
|
||||
let expected = vec!["A", "B", "C", "D"];
|
||||
assert_eq!(order.as_slice(), expected.as_slice());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Parallelism tests module
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod execution;
|
||||
pub mod spawning;
|
||||
pub mod threads;
|
||||
pub mod timing;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for spawner functionality
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -1,81 +1,13 @@
|
||||
//! Tests for multi-threading and concurrent execution
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::ThreadId;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tasks_run_on_different_threads() -> DagResult<()> {
|
||||
// Prove that tasks CAN run on multiple different threads (true parallelism capability)
|
||||
// Note: Tokio may choose to run fast tasks on the same thread for efficiency
|
||||
use std::thread;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
|
||||
|
||||
struct LayerTask {
|
||||
ids: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl LayerTask {
|
||||
async fn run(&self) -> i32 {
|
||||
let tid = thread::current().id();
|
||||
self.ids.lock().unwrap().insert(tid);
|
||||
|
||||
// Yield to give tokio reason to distribute
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
|
||||
// Some CPU work too
|
||||
let mut sum = 0;
|
||||
for j in 0..1000 {
|
||||
sum += j;
|
||||
}
|
||||
self.idx + sum
|
||||
}
|
||||
}
|
||||
|
||||
// Create 50 tasks with actual work to encourage multi-threading
|
||||
let tasks: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
dag.add_task(LayerTask {
|
||||
ids: thread_ids.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
for task in tasks.into_iter() {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
// With a multi-threaded runtime and sufficient work, we typically see multiple threads
|
||||
// But we can't guarantee it (scheduler's choice), so we just verify the capability exists
|
||||
let unique_threads = thread_ids.lock().unwrap().len();
|
||||
|
||||
// On a multi-core system with sufficient work, tokio should use multiple threads
|
||||
// This is a soft assertion - if it fails, it means tasks CAN'T use multiple threads
|
||||
if unique_threads == 1 {
|
||||
eprintln!(
|
||||
"Warning: All tasks ran on 1 thread. This may indicate parallelism isn't working, \
|
||||
or tokio chose not to distribute work (valid for small/fast tasks)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
// Prove concurrent execution using atomic counters to track simultaneous tasks
|
||||
@@ -155,73 +87,6 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_parallel_execution() -> DagResult<()> {
|
||||
// Test diamond pattern: source -> (parallel1, parallel2) -> sink
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let concurrent_middle = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
struct ParallelTask1 {
|
||||
counter: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ParallelTask1 {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.counter.fetch_add(1, Ordering::SeqCst);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let count = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
// These should run in parallel
|
||||
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 = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
// Both should be running
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 3
|
||||
}
|
||||
}
|
||||
|
||||
let parallel2 = dag
|
||||
.add_task(ParallelTask2 {
|
||||
counter: concurrent_middle.clone(),
|
||||
})
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¶llel1, ¶llel2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
// Test extreme parallelism with 1000 parallel tasks
|
||||
|
||||
+151
-3
@@ -1,8 +1,11 @@
|
||||
//! Tests for timing and speedup verification
|
||||
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx::{task, task_fn, DagResult, DagRunner};
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
struct SleepTask(usize, Duration);
|
||||
@@ -49,7 +52,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
// Parallel execution should be significantly faster
|
||||
assert!(
|
||||
elapsed < sequential_time / 2,
|
||||
elapsed < sequential_time / 4,
|
||||
"Parallel execution too slow: {:?} vs sequential {:?}",
|
||||
elapsed,
|
||||
sequential_time
|
||||
@@ -57,3 +60,148 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
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)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
//! Butterfly pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_butterfly_network() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Simplified butterfly network - avoid tuples entirely
|
||||
// Layer 0: inputs
|
||||
let inputs: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 1: butterfly operations (compute pairs separately)
|
||||
let mut layer1 = Vec::new();
|
||||
for i in 0..4 {
|
||||
let idx1 = i * 2;
|
||||
let idx2 = i * 2 + 1;
|
||||
// Sum operation
|
||||
layer1.push(
|
||||
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::<(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::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
|
||||
.depends_on((&layer1[0], &layer1[4])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
|
||||
.depends_on((&layer1[1], &layer1[5])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b))
|
||||
.depends_on((&layer1[2], &layer1[6])),
|
||||
dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b))
|
||||
.depends_on((&layer1[3], &layer1[7])),
|
||||
];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify some outputs exist
|
||||
for task in &layer2 {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recursive_butterfly() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
fn build_butterfly(
|
||||
dag: &DagRunner,
|
||||
inputs: Vec<dagx::TaskHandle<i32>>,
|
||||
) -> Vec<dagx::TaskHandle<i32>> {
|
||||
if inputs.len() <= 2 {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
let mid = inputs.len() / 2;
|
||||
let mut outputs = Vec::new();
|
||||
|
||||
// Butterfly connections
|
||||
for i in 0..mid {
|
||||
let sum = dag
|
||||
.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::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| {
|
||||
(a - b).abs()
|
||||
}))
|
||||
.depends_on((&inputs[i], &inputs[i + mid]));
|
||||
|
||||
outputs.push(sum);
|
||||
outputs.push(diff);
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
// Create 4 inputs
|
||||
let inputs: Vec<dagx::TaskHandle<i32>> = vec![10, 20, 30, 40]
|
||||
.into_iter()
|
||||
.map(|val| {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let outputs = build_butterfly(&dag, inputs);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(outputs.len(), 4);
|
||||
for output in outputs {
|
||||
let _ = dag.get(output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_benes_network() -> DagResult<()> {
|
||||
// Simplified Benes network - avoid tuples
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// 4 inputs
|
||||
let inputs: Vec<_> = (0..4)
|
||||
.map(|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::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.min(b)
|
||||
}))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.max(b)
|
||||
}))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.min(b)
|
||||
}))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| {
|
||||
a.max(b)
|
||||
}))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
];
|
||||
|
||||
// Second stage with crossover
|
||||
let outputs: Vec<_> = vec![
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
|
||||
.depends_on((&stage1[0], &stage1[2])),
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b))
|
||||
.depends_on((&stage1[1], &stage1[3])),
|
||||
];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
for output in outputs {
|
||||
let _ = dag.get(output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shuffle_exchange_network() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 8 inputs
|
||||
let inputs: Vec<TaskHandle<_>> = (0..8)
|
||||
.map(|i| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
i
|
||||
}))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Shuffle stage: bit rotation
|
||||
let shuffled: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
// Shuffle pattern: rotate bits
|
||||
let source = ((i << 1) | (i >> 2)) & 0x7;
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x))
|
||||
.depends_on(inputs[source])
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Exchange stage: adjacent pairs
|
||||
let exchanged: Vec<_> = (0..4)
|
||||
.flat_map(|i| {
|
||||
let idx = i * 2;
|
||||
vec![
|
||||
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::<(i32, i32), _, _>(
|
||||
|(&a, &b): (&i32, &i32)| if a > b { a } else { b },
|
||||
))
|
||||
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 8);
|
||||
|
||||
for task in exchanged {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
//! Complex DAG pattern tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_with_conditional_paths() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Input validation
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
let validate = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
|
||||
if x > 0 && x < 100 {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err("Invalid input")
|
||||
}
|
||||
}))
|
||||
.depends_on(input);
|
||||
|
||||
// Process valid input
|
||||
let process = dag
|
||||
.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::<i32, _, _>(|&x: &i32| x + 10))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance2 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance3 = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 5))
|
||||
.depends_on(process);
|
||||
|
||||
// Combine enhancements
|
||||
let combine = dag
|
||||
.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::<Vec<_>, _, _>(|values: &Vec<i32>| {
|
||||
values.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(combine);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(process)?, 84);
|
||||
let result = dag.get(final_result)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_map_reduce_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// 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 |_: ()| {
|
||||
// Simulate word count
|
||||
vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)]
|
||||
}));
|
||||
task.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Shuffle phase: group by key (simplified - just 4 reducers)
|
||||
let shuffled: Vec<_> = (0..4)
|
||||
.map(|reducer_id| {
|
||||
// Each reducer gets data from 4 mappers
|
||||
let mapper_indices: Vec<usize> = (0..4).map(|i| reducer_id * 4 + i).collect();
|
||||
|
||||
let deps: Vec<_> = mapper_indices.iter().map(|&i| mappers[i]).collect();
|
||||
|
||||
match deps.len() {
|
||||
4 => dag
|
||||
.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)>,
|
||||
)| {
|
||||
let mut combined = Vec::<(&str, usize)>::new();
|
||||
combined.extend(inputs.0);
|
||||
combined.extend(inputs.1);
|
||||
combined.extend(inputs.2);
|
||||
combined.extend(inputs.3);
|
||||
combined
|
||||
},
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2], &deps[3])),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Reduce phase
|
||||
let reducers: Vec<_> = shuffled
|
||||
.iter()
|
||||
.map(|shuffled_data| {
|
||||
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.len()
|
||||
}))
|
||||
.depends_on(shuffled_data)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Final aggregation
|
||||
let final_task = dag
|
||||
.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]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_task)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pipeline_with_feedback() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let iteration_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Initial value
|
||||
let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
|
||||
// Stage 1: Transform
|
||||
let stage1 = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let counter = iteration_count.clone();
|
||||
move |x: &i32| {
|
||||
let counter = counter.clone();
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
x * 2
|
||||
}
|
||||
}))
|
||||
.depends_on(init);
|
||||
|
||||
// Stage 2: Filter
|
||||
let stage2 = dag
|
||||
.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::<Option<_>, _, _>(|opt: &Option<i32>| {
|
||||
opt.map(|x| x + 10).unwrap_or(0)
|
||||
}))
|
||||
.depends_on(stage2);
|
||||
|
||||
// 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::<i32, _, _>(|&x: &i32| {
|
||||
// In a real feedback loop, this would feed back to stage1
|
||||
x / 2
|
||||
}))
|
||||
.depends_on(stage3);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(stage1)?, 2);
|
||||
assert_eq!(dag.get(stage3)?, 12);
|
||||
assert_eq!(iteration_count.load(Ordering::SeqCst), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_scatter_gather_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Scatter: distribute work to multiple workers
|
||||
let source: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
vec![10, 20, 30, 40, 50, 60, 70, 80]
|
||||
}))
|
||||
.into();
|
||||
|
||||
// Workers process chunks
|
||||
let workers: Vec<_> = (0..4)
|
||||
.map(|worker_id| {
|
||||
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;
|
||||
let end = if worker_id == 3 {
|
||||
data.len()
|
||||
} else {
|
||||
start + chunk_size
|
||||
};
|
||||
|
||||
data[start..end].iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Gather: collect results from all workers
|
||||
let gather = dag
|
||||
.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]));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of 10+20+30+40+50+60+70+80 = 360
|
||||
assert_eq!(dag.get(gather)?, 360);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fork_join_with_barriers() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
let phase_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Fork phase
|
||||
let parent = dag.add_task(task_fn::<(), _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
counter.store(1, Ordering::SeqCst);
|
||||
100
|
||||
}
|
||||
}));
|
||||
|
||||
// Multiple parallel paths
|
||||
let mut path1 = Vec::new();
|
||||
let parent_handle: dagx::TaskHandle<i32> = parent.into();
|
||||
let mut prev1 = parent_handle;
|
||||
for i in 0..3 {
|
||||
let counter = phase_counter.clone();
|
||||
let task = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
x + i
|
||||
}))
|
||||
.depends_on(prev1);
|
||||
path1.push(task);
|
||||
prev1 = task;
|
||||
}
|
||||
|
||||
let mut path2 = Vec::new();
|
||||
let mut prev2 = parent_handle;
|
||||
for i in 0..3 {
|
||||
let counter = phase_counter.clone();
|
||||
let task = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
x * 2 + i
|
||||
}))
|
||||
.depends_on(prev2);
|
||||
path2.push(task);
|
||||
prev2 = task;
|
||||
}
|
||||
|
||||
// Barrier: wait for all paths
|
||||
let barrier = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |(p1, p2): (&i32, &i32)| {
|
||||
let counter = counter.clone();
|
||||
counter.store(2, Ordering::SeqCst);
|
||||
p1 + p2
|
||||
}
|
||||
}))
|
||||
.depends_on((&path1[2], &path2[2]));
|
||||
|
||||
// Join phase
|
||||
let join = dag
|
||||
.add_task(task_fn::<i32, _, _>({
|
||||
let counter = phase_counter.clone();
|
||||
move |&x: &i32| {
|
||||
let counter = counter.clone();
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 2);
|
||||
x
|
||||
}
|
||||
}))
|
||||
.depends_on(barrier);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let _ = dag.get(join)?;
|
||||
assert_eq!(phase_counter.load(Ordering::SeqCst), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
//! Grid pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_2d_grid_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a 4x4 grid where each cell depends on the cell above and to the left
|
||||
let mut grid: Vec<Vec<Option<dagx::TaskHandle<i32>>>> = vec![vec![None; 4]; 4];
|
||||
|
||||
// Initialize first row and column
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..4 {
|
||||
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 |_: ()| i as i32 * 10));
|
||||
grid[i][0] = Some(task.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the grid with dependencies
|
||||
for i in 1..4 {
|
||||
for j in 1..4 {
|
||||
let above = grid[i - 1][j].as_ref().unwrap();
|
||||
let left = grid[i][j - 1].as_ref().unwrap();
|
||||
|
||||
let task = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(move |(a, l): (&i32, &i32)| {
|
||||
a + l
|
||||
}))
|
||||
.depends_on((above, left));
|
||||
grid[i][j] = Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check corner values
|
||||
let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?;
|
||||
assert!(bottom_right > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_3d_grid_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a 3x3x3 grid
|
||||
let size = 3;
|
||||
let mut grid: Vec<Vec<Vec<Option<dagx::TaskHandle<usize>>>>> =
|
||||
vec![vec![vec![None; size]; size]; size];
|
||||
|
||||
// Initialize origin
|
||||
let origin = dag.add_task(task_fn::<(), _, _>(|_: ()| 1));
|
||||
grid[0][0][0] = Some(origin.into());
|
||||
|
||||
// Fill the 3D grid
|
||||
for x in 0..size {
|
||||
for y in 0..size {
|
||||
for z in 0..size {
|
||||
if x == 0 && y == 0 && z == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let deps: Vec<dagx::TaskHandle<usize>> = vec![
|
||||
if x > 0 { grid[x - 1][y][z] } else { None },
|
||||
if y > 0 { grid[x][y - 1][z] } else { None },
|
||||
if z > 0 { grid[x][y][z - 1] } else { None },
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let task_handle: dagx::TaskHandle<usize> = match deps.len() {
|
||||
0 => {
|
||||
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| x + y + z));
|
||||
t.into()
|
||||
}
|
||||
1 => dag
|
||||
.add_task(task_fn::<usize, _, _>(move |prev: &usize| prev + 1))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.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::<(usize, usize, usize), _, _>(
|
||||
move |(a, b, c): (&usize, &usize, &usize)| a + b + c,
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2])),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
grid[x][y][z] = Some(task_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Check that all cells have values
|
||||
for row in &grid {
|
||||
for col in row {
|
||||
for task in col.iter().flatten() {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hexagonal_grid() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Hexagonal grid with axial coordinates
|
||||
let mut hex_grid = std::collections::HashMap::new();
|
||||
|
||||
// Center hex
|
||||
let center_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
let center: dagx::TaskHandle<i32> = center_builder.into();
|
||||
hex_grid.insert((0, 0), center);
|
||||
|
||||
// Ring 1 (6 hexes around center)
|
||||
let directions = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)];
|
||||
|
||||
for (i, &(dq, dr)) in directions.iter().enumerate() {
|
||||
let neighbor: dagx::TaskHandle<i32> = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |c: &i32| c + (i as i32 + 1) * 10))
|
||||
.depends_on(center);
|
||||
hex_grid.insert((dq, dr), neighbor);
|
||||
}
|
||||
|
||||
// Ring 2 (12 hexes)
|
||||
for &(q, r) in &[
|
||||
(2, 0),
|
||||
(1, 1),
|
||||
(0, 2),
|
||||
(-1, 2),
|
||||
(-2, 2),
|
||||
(-2, 1),
|
||||
(-2, 0),
|
||||
(-1, -1),
|
||||
(0, -2),
|
||||
(1, -2),
|
||||
(2, -2),
|
||||
(2, -1),
|
||||
] {
|
||||
// Find neighbors that exist
|
||||
let neighbors: Vec<_> = directions
|
||||
.iter()
|
||||
.filter_map(|&(dq, dr)| hex_grid.get(&(q - dq, r - dr)).cloned())
|
||||
.take(2) // Just use first 2 neighbors for simplicity
|
||||
.collect();
|
||||
|
||||
let task: dagx::TaskHandle<i32> = if neighbors.len() == 2 {
|
||||
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::<i32, _, _>(move |n: &i32| n + q + r))
|
||||
.depends_on(neighbors[0])
|
||||
} else {
|
||||
let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| q + r));
|
||||
t.into()
|
||||
};
|
||||
|
||||
hex_grid.insert((q, r), task);
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify center and some ring values
|
||||
assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_toroidal_grid() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
const SIZE: usize = 4;
|
||||
let mut grid: Vec<Vec<Option<dagx::TaskHandle<i32>>>> = vec![vec![None; SIZE]; SIZE];
|
||||
|
||||
// Initialize all cells
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..SIZE {
|
||||
for j in 0..SIZE {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| (i * SIZE + j) as i32));
|
||||
grid[i][j] = Some(task.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Create toroidal connections (wrapping edges)
|
||||
let mut torus_tasks = Vec::new();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..SIZE {
|
||||
for j in 0..SIZE {
|
||||
let curr = grid[i][j].as_ref().unwrap();
|
||||
let up = grid[(i + SIZE - 1) % SIZE][j].as_ref().unwrap();
|
||||
let down = grid[(i + 1) % SIZE][j].as_ref().unwrap();
|
||||
let left = grid[i][(j + SIZE - 1) % SIZE].as_ref().unwrap();
|
||||
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)| {
|
||||
// Average of self and 4 neighbors
|
||||
(c + u + d + l + r) / 5
|
||||
},
|
||||
))
|
||||
.depends_on((curr, up, down, left, right));
|
||||
|
||||
torus_tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all torus tasks completed
|
||||
for task in torus_tasks {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//! DAG pattern tests
|
||||
//!
|
||||
//! Tests for various DAG topologies and patterns.
|
||||
|
||||
mod butterfly;
|
||||
mod complex;
|
||||
mod grid;
|
||||
mod tree;
|
||||
@@ -1,282 +0,0 @@
|
||||
//! Tree pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner, TaskHandle};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_binary_tree_reduction() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 8 leaf nodes
|
||||
let leaves: Vec<_> = (0..8)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i)))
|
||||
.collect();
|
||||
|
||||
// Build binary tree reduction
|
||||
let mut current_level: Vec<dagx::TaskHandle<i32>> =
|
||||
leaves.into_iter().map(|t| t.into()).collect();
|
||||
|
||||
while current_level.len() > 1 {
|
||||
let mut next_level = Vec::new();
|
||||
|
||||
for chunk in current_level.chunks(2) {
|
||||
if chunk.len() == 2 {
|
||||
let task = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&chunk[0], &chunk[1]));
|
||||
next_level.push(task);
|
||||
} else {
|
||||
next_level.push(chunk[0]);
|
||||
}
|
||||
}
|
||||
|
||||
current_level = next_level;
|
||||
}
|
||||
|
||||
let root = current_level[0];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255
|
||||
assert_eq!(dag.get(root)?, 255);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_n_ary_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// 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 |_: ()| value));
|
||||
return task.into();
|
||||
}
|
||||
|
||||
let children: Vec<_> = (0..3)
|
||||
.map(|i| build_tree(dag, depth - 1, value * 3 + i))
|
||||
.collect();
|
||||
|
||||
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]))
|
||||
}
|
||||
|
||||
let root = build_tree(&dag, 3, 1);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unbalanced_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create an unbalanced tree
|
||||
// root
|
||||
// / \
|
||||
// a b
|
||||
// / \ \
|
||||
// c d e
|
||||
// / \
|
||||
// f g
|
||||
|
||||
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::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(f);
|
||||
|
||||
let d_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 3));
|
||||
let d: dagx::TaskHandle<i32> = d_builder.into();
|
||||
|
||||
let e = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(g);
|
||||
|
||||
let a = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(c, d): (&i32, &i32)| c + d))
|
||||
.depends_on((&c, &d));
|
||||
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|e: &i32| e + 10))
|
||||
.depends_on(e);
|
||||
|
||||
let root = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
|
||||
.depends_on((&a, &b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(f)?, 1);
|
||||
assert_eq!(dag.get(c)?, 2);
|
||||
assert_eq!(dag.get(a)?, 5); // 2 + 3
|
||||
assert_eq!(dag.get(b)?, 16); // 6 + 10
|
||||
assert_eq!(dag.get(root)?, 80); // 5 * 16
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trie_like_structure() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a trie-like structure for paths
|
||||
let root: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>(|_: ()| "root".to_string()))
|
||||
.into();
|
||||
|
||||
// First level
|
||||
let usr = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/usr", r)))
|
||||
.depends_on(root);
|
||||
|
||||
let etc = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/etc", r)))
|
||||
.depends_on(root);
|
||||
|
||||
let var = dag
|
||||
.add_task(task_fn::<String, _, _>(|r: &String| format!("{}/var", r)))
|
||||
.depends_on(root);
|
||||
|
||||
// Second level
|
||||
let usr_bin = dag
|
||||
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/bin", u)))
|
||||
.depends_on(usr);
|
||||
|
||||
let usr_lib = dag
|
||||
.add_task(task_fn::<String, _, _>(|u: &String| format!("{}/lib", u)))
|
||||
.depends_on(usr);
|
||||
|
||||
let etc_config = dag
|
||||
.add_task(task_fn::<String, _, _>(|e: &String| {
|
||||
format!("{}/config", e)
|
||||
}))
|
||||
.depends_on(etc);
|
||||
|
||||
let var_log = dag
|
||||
.add_task(task_fn::<String, _, _>(|v: &String| format!("{}/log", v)))
|
||||
.depends_on(var);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(usr_bin)?, "root/usr/bin");
|
||||
assert_eq!(dag.get(usr_lib)?, "root/usr/lib");
|
||||
assert_eq!(dag.get(etc_config)?, "root/etc/config");
|
||||
assert_eq!(dag.get(var_log)?, "root/var/log");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_balanced_k_ary_tree() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a perfectly balanced 4-ary tree of depth 3
|
||||
const K: usize = 4;
|
||||
const DEPTH: usize = 3;
|
||||
|
||||
fn create_balanced_tree(
|
||||
dag: &DagRunner,
|
||||
depth: usize,
|
||||
k: usize,
|
||||
node_id: usize,
|
||||
) -> dagx::TaskHandle<usize> {
|
||||
if depth == 0 {
|
||||
let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| node_id));
|
||||
return task.into();
|
||||
}
|
||||
|
||||
let children: Vec<_> = (0..k)
|
||||
.map(|i| create_balanced_tree(dag, depth - 1, k, node_id * k + i))
|
||||
.collect();
|
||||
|
||||
match k {
|
||||
2 => dag
|
||||
.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::<(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::<(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"),
|
||||
}
|
||||
}
|
||||
|
||||
let root = create_balanced_tree(&dag, DEPTH, K, 0);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_merkle_tree_pattern() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Simulate a Merkle tree for 8 data blocks
|
||||
let data_blocks: Vec<_> = (0..8)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Simulate hash of data block
|
||||
format!("hash_{}", i)
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Convert to TaskHandles
|
||||
let mut current_level: Vec<dagx::TaskHandle<String>> =
|
||||
data_blocks.into_iter().map(|t| t.into()).collect();
|
||||
|
||||
while current_level.len() > 1 {
|
||||
let mut next_level = Vec::new();
|
||||
|
||||
for pair in current_level.chunks(2) {
|
||||
if pair.len() == 2 {
|
||||
let hash_task = dag
|
||||
.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 {
|
||||
next_level.push(pair[0]);
|
||||
}
|
||||
}
|
||||
|
||||
current_level = next_level;
|
||||
}
|
||||
|
||||
let root_hash = current_level[0];
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root_hash)?;
|
||||
assert!(result.starts_with("hash"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//! Stress tests and benchmarks
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
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| async move { tokio::spawn(fut).await.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| async move { tokio::spawn(fut).await.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,64 +0,0 @@
|
||||
//! Runtime compatibility tests for async-std
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_basic_dag_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(async_std::task::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parallel_execution_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(async_std::task::spawn).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_complex_dependencies_async_std() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.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().clone(), 60);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//! Runtime integration tests - verify dagx works correctly across different runtimes
|
||||
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_integration_tokio() -> DagResult<()> {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sum)?, 30);
|
||||
Ok(())
|
||||
}
|
||||
+145
-4
@@ -1,7 +1,148 @@
|
||||
//! Runtime tests module
|
||||
|
||||
pub mod compatibility;
|
||||
pub mod quirky;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
mod async_std;
|
||||
mod smol;
|
||||
use dagx::{task_fn, DagResult, DagRunner};
|
||||
use futures_util::{future::BoxFuture, task::SpawnExt, Future};
|
||||
use test_case::test_case;
|
||||
|
||||
type BoxFunc = Box<
|
||||
dyn Fn(
|
||||
BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||
) -> BoxFuture<'static, DagResult<Arc<dyn Any + Send + Sync>>>,
|
||||
>;
|
||||
|
||||
trait RuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>;
|
||||
}
|
||||
|
||||
struct SmolRuntimeTest;
|
||||
|
||||
impl RuntimeTest for SmolRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
smol::block_on(f(Box::new(|fut| Box::pin(smol::spawn(fut)))));
|
||||
}
|
||||
}
|
||||
|
||||
struct FuturesExecutorRuntimeTest;
|
||||
|
||||
impl RuntimeTest for FuturesExecutorRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
let spawner = futures_executor::ThreadPool::new().unwrap();
|
||||
|
||||
futures_executor::block_on(f(Box::new(move |fut| {
|
||||
Box::pin(spawner.spawn_with_handle(fut).unwrap())
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
struct PollsterRuntimeTest;
|
||||
|
||||
impl RuntimeTest for PollsterRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
pollster::block_on(f(Box::new(|fut| {
|
||||
Box::pin(async { pollster::block_on(fut) })
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
static ASYNC_EXECUTOR: async_executor::Executor = async_executor::Executor::new();
|
||||
|
||||
struct AsyncExecutorRuntimeTest;
|
||||
|
||||
impl RuntimeTest for AsyncExecutorRuntimeTest {
|
||||
fn run_dagx_test<F>(self, f: impl Fn(BoxFunc) -> F)
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
smol::future::block_on(
|
||||
ASYNC_EXECUTOR.run(f(Box::new(|fut| Box::pin(ASYNC_EXECUTOR.spawn(fut))))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_basic_dag(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
})
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_parallel_execution(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test_case(SmolRuntimeTest)]
|
||||
#[test_case(FuturesExecutorRuntimeTest)]
|
||||
#[test_case(PollsterRuntimeTest)]
|
||||
#[test_case(AsyncExecutorRuntimeTest)]
|
||||
fn test_complex_dependencies(runner: impl RuntimeTest) {
|
||||
runner.run_dagx_test(|spawner| async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(sum);
|
||||
|
||||
dag.run(spawner).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
});
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
//! Tests for quirky/edge-case async runtimes
|
||||
//!
|
||||
//! This file tests dagx with unusual async runtimes to ensure
|
||||
//! the spawner parameter works correctly across different executor types.
|
||||
//!
|
||||
//! Tested runtimes:
|
||||
//! - async-executor: Minimal executor from smol ecosystem
|
||||
//! - pollster: Blocking/synchronous executor
|
||||
//! - futures-executor: Basic futures crate executor
|
||||
|
||||
// Note: executor.run() synchronously executes futures to completion.
|
||||
// We don't need the return value in these tests, so we allow unused results.
|
||||
#![allow(clippy::let_underscore_future)]
|
||||
#![allow(unused_must_use)]
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagRunner, TaskHandle};
|
||||
use futures_executor::ThreadPool;
|
||||
use futures_util::task::SpawnExt;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// =======================
|
||||
// async-executor Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_basic() {
|
||||
// async-executor is a minimal executor from the smol ecosystem
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
let sum = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((x, y));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 30);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_complex_dag() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a diamond pattern
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)).into();
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(source);
|
||||
|
||||
let right = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 3))
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
|
||||
.depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_parallel_tasks() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 10 independent tasks
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let c = counter.clone();
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
// All tasks should have run
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
|
||||
// Verify results
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i as i32);
|
||||
}
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_with_local_spawn() {
|
||||
// Test with LocalExecutor (single-threaded)
|
||||
let executor = async_executor::LocalExecutor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(x).unwrap(), 42);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_deep_chain() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
let future = async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a chain of 50 tasks
|
||||
let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0));
|
||||
let mut current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(start);
|
||||
|
||||
for _ in 1..50 {
|
||||
current = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(current).unwrap(), 50);
|
||||
};
|
||||
|
||||
executor.run(future);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// pollster Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_pollster_basic() {
|
||||
// pollster is a blocking executor - it blocks the thread
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
|
||||
let doubled = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(x);
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(doubled).unwrap(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pollster_complex() {
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Fan-out pattern
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)).into();
|
||||
|
||||
let tasks: Vec<_> = (0..5)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * i))
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), 7 * i as i32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pollster_error_handling() {
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task that returns Result
|
||||
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::<i32, String>(42)));
|
||||
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(task).unwrap(), Ok(42));
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// futures-executor Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_basic() {
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15));
|
||||
let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25));
|
||||
let product = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b))
|
||||
.depends_on((x, y));
|
||||
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_threadpool() {
|
||||
let pool = futures_executor::ThreadPool::new().unwrap();
|
||||
let spawner = pool.clone();
|
||||
|
||||
pool.spawn_ok(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create multiple independent tasks
|
||||
let tasks: Vec<_> = (0..20)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * i)))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), (i * i) as i32);
|
||||
}
|
||||
});
|
||||
|
||||
// Give tasks time to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Note: LocalPool test removed because it cannot run within another executor context
|
||||
// (cargo test framework may use tokio, causing EnterError)
|
||||
// LocalPool is single-threaded anyway, so futures-executor ThreadPool is more representative
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_diamond_pattern() {
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 8)).into();
|
||||
|
||||
let b = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
|
||||
.depends_on(a);
|
||||
|
||||
let c = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
|
||||
.depends_on(a);
|
||||
|
||||
let d = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x * y))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Cross-Runtime Tests
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_all_runtimes_compatibility() {
|
||||
// This test verifies the same DAG works across all runtimes
|
||||
|
||||
// Test 1: async-executor
|
||||
{
|
||||
let executor = async_executor::Executor::new();
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 2: pollster
|
||||
{
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
dag.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 3: futures-executor
|
||||
{
|
||||
futures_executor::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123));
|
||||
let spawner = ThreadPool::new().unwrap();
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
assert_eq!(dag.get(x).unwrap(), 123);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runtime_edge_cases() {
|
||||
// Test with LocalExecutor (no Send requirement)
|
||||
let executor = async_executor::LocalExecutor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// This tests that our API works even with !Send futures
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| {
|
||||
// Local state
|
||||
|
||||
42
|
||||
}));
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(x).unwrap(), 42);
|
||||
});
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Stress Tests for Quirky Runtimes
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_async_executor_stress() {
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create 100 tasks
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| {
|
||||
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
// Small delay to test executor scheduling
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| executor.spawn(fut)).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i as i32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
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::<(), _, _>(|_: ()| 10));
|
||||
|
||||
dag1.run(|fut| async { pollster::block_on(fut) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag1.get(x).unwrap(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_futures_executor_multi_pool() {
|
||||
// Test with multiple thread pools
|
||||
let pool1 = futures_executor::ThreadPool::new().unwrap();
|
||||
let spawner = pool1.clone();
|
||||
|
||||
pool1.spawn_ok(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 50));
|
||||
|
||||
dag.run(|fut| spawner.spawn_with_handle(fut).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
assert_eq!(dag.get(x).unwrap(), 50);
|
||||
});
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Edge Case: Sync Tasks with Quirky Runtimes
|
||||
// =======================
|
||||
|
||||
#[test]
|
||||
fn test_sync_tasks_on_async_executor() {
|
||||
struct SyncAdd;
|
||||
|
||||
#[dagx::task]
|
||||
impl SyncAdd {
|
||||
fn run(a: &i32, b: &i32) -> i32 {
|
||||
// Synchronous task
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
let executor = async_executor::Executor::new();
|
||||
|
||||
executor.run(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 12);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_tasks_on_pollster() {
|
||||
struct SyncDouble;
|
||||
|
||||
#[dagx::task]
|
||||
impl SyncDouble {
|
||||
fn run(x: &i32) -> i32 {
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
pollster::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(doubled).unwrap(), 42);
|
||||
});
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
//! Runtime compatibility tests for smol
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::DagRunner;
|
||||
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&mut self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
#[dagx::task]
|
||||
impl Add {
|
||||
async fn run(&mut self, a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_dag_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Value(2));
|
||||
let y = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((x, y));
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_execution_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..10)
|
||||
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2)))
|
||||
.collect();
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
for (i, task) in tasks.into_iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_dependencies_smol() {
|
||||
smol::block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(10));
|
||||
let b = dag.add_task(Value(20));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
let double = dag
|
||||
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
|
||||
.depends_on(sum);
|
||||
|
||||
dag.run(smol::spawn).await.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap().clone(), 60);
|
||||
});
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Combined task pattern tests
|
||||
use crate::common::task_fn;
|
||||
// Comprehensive test showing all three patterns working together
|
||||
|
||||
use dagx::*;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Pattern 1: Stateless - Pure computation, no state
|
||||
struct StatelessAdd;
|
||||
#[task]
|
||||
impl StatelessAdd {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Read-only state - Configuration data
|
||||
struct ReadOnlyMultiplier(i32);
|
||||
#[task]
|
||||
impl ReadOnlyMultiplier {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.0 // Read-only access
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Mutable state - Accumulates state
|
||||
struct MutableAccumulator(i32);
|
||||
#[task]
|
||||
impl MutableAccumulator {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.0 += input; // Modifies state
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_three_task_patterns() {
|
||||
// Comprehensive test showing all three patterns working together
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create initial values
|
||||
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
|
||||
|
||||
// Pattern 2: Read-only multiplication
|
||||
let doubled = dag.add_task(ReadOnlyMultiplier(2)).depends_on(sum); // 8 * 2 = 16
|
||||
|
||||
// Pattern 3: Mutable accumulation
|
||||
let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 8);
|
||||
assert_eq!(dag.get(doubled).unwrap(), 16);
|
||||
assert_eq!(dag.get(accumulated).unwrap(), 26);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parallel_execution() {
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create 3 independent tasks that should run in parallel
|
||||
let c1 = counter.clone();
|
||||
let c2 = counter.clone();
|
||||
let c3 = counter.clone();
|
||||
|
||||
let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c1.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}));
|
||||
|
||||
let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c2.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
2
|
||||
}));
|
||||
|
||||
let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| {
|
||||
let c = c3.clone();
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
3
|
||||
}));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All three tasks should have run
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 3);
|
||||
assert_eq!(dag.get(t1).unwrap(), 1);
|
||||
assert_eq!(dag.get(t2).unwrap(), 2);
|
||||
assert_eq!(dag.get(t3).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_task_fn_with_captured_state() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let multiplier = 7;
|
||||
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5));
|
||||
let scaled = dag
|
||||
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * multiplier))
|
||||
.depends_on(base);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(scaled).unwrap(), 35);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Test module for task pattern tests
|
||||
|
||||
mod combined;
|
||||
mod readonly;
|
||||
mod stateless;
|
||||
@@ -1,91 +0,0 @@
|
||||
// Read-only state task pattern tests
|
||||
use crate::common::task_fn;
|
||||
// These tests verify the read-only state pattern using &self
|
||||
|
||||
use dagx::*;
|
||||
|
||||
// Read-only state tasks
|
||||
struct ReadOnlyValue(i32);
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyValue {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0 // Read-only access
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadOnlyMultiplier(i32);
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyMultiplier {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
input * self.0 // Read-only access to state
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadOnlyConfig {
|
||||
multiplier: i32,
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ReadOnlyConfig {
|
||||
async fn run(&self, input: &i32) -> i32 {
|
||||
(input * self.multiplier) + self.offset
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readonly_state_basic() {
|
||||
// Test basic read-only state task using &self
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let value = dag.add_task(ReadOnlyValue(42));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(value).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 5));
|
||||
let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result).unwrap(), 35);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 10));
|
||||
let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
|
||||
|
||||
let config = ReadOnlyConfig {
|
||||
multiplier: 3,
|
||||
offset: 5,
|
||||
};
|
||||
|
||||
let result1 = dag.add_task(config).depends_on(input1);
|
||||
// Note: Can't reuse config because it was moved, so create another
|
||||
let config2 = ReadOnlyConfig {
|
||||
multiplier: 3,
|
||||
offset: 5,
|
||||
};
|
||||
let result2 = dag.add_task(config2).depends_on(input2);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5
|
||||
assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
// Stateless task pattern tests
|
||||
use crate::common::task_fn;
|
||||
|
||||
use dagx::*;
|
||||
|
||||
// Stateless task structs
|
||||
struct StatelessAdd;
|
||||
|
||||
#[task]
|
||||
impl StatelessAdd {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessDouble;
|
||||
|
||||
#[task]
|
||||
impl StatelessDouble {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessSource;
|
||||
|
||||
#[task]
|
||||
impl StatelessSource {
|
||||
async fn run() -> i32 {
|
||||
42
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessFormat;
|
||||
|
||||
#[task]
|
||||
impl StatelessFormat {
|
||||
async fn run(n: &i32, s: &String, b: &bool) -> String {
|
||||
format!("{}: {} ({})", s, n, b)
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessMultiply;
|
||||
|
||||
#[task]
|
||||
impl StatelessMultiply {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a * b
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessInc;
|
||||
|
||||
#[task]
|
||||
impl StatelessInc {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessSquare;
|
||||
|
||||
#[task]
|
||||
impl StatelessSquare {
|
||||
async fn run(x: &i32) -> i32 {
|
||||
x * x
|
||||
}
|
||||
}
|
||||
|
||||
struct StatelessConcat;
|
||||
|
||||
#[task]
|
||||
impl StatelessConcat {
|
||||
async fn run(a: &String, b: &String) -> String {
|
||||
format!("{}{}", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_basic() {
|
||||
// Test basic stateless task with two inputs
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(sum).unwrap(), 30);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_single_input() {
|
||||
// Test stateless task with single input
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 21));
|
||||
let doubled = dag.add_task(StatelessDouble).depends_on(input);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(doubled).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_no_input() {
|
||||
// Test stateless source task with no inputs
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(StatelessSource);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(source).unwrap(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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::<(), _, _>(|_: ()| 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));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(result).unwrap(), "test: 42 (true)");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_mixed_with_stateful() {
|
||||
// Test mixing stateless and stateful tasks
|
||||
// Define Counter inline for this test
|
||||
struct Counter {
|
||||
count: i32,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new(initial: i32) -> Self {
|
||||
Self { count: initial }
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Counter {
|
||||
async fn run(&mut self, input: &i32) -> i32 {
|
||||
self.count += input;
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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));
|
||||
|
||||
// Stateful counter
|
||||
let counted = dag.add_task(Counter::new(10)).depends_on(product);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(product).unwrap(), 15);
|
||||
assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_chain() {
|
||||
// Test chaining stateless tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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);
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(step1).unwrap(), 1);
|
||||
assert_eq!(dag.get(step2).unwrap(), 2);
|
||||
assert_eq!(dag.get(step3).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_parallel() {
|
||||
// Test parallel execution of stateless tasks
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (1..=5)
|
||||
.map(|i| {
|
||||
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
|
||||
dag.add_task(StatelessSquare).depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(tasks[0]).unwrap(), 1);
|
||||
assert_eq!(dag.get(tasks[1]).unwrap(), 4);
|
||||
assert_eq!(dag.get(tasks[2]).unwrap(), 9);
|
||||
assert_eq!(dag.get(tasks[3]).unwrap(), 16);
|
||||
assert_eq!(dag.get(tasks[4]).unwrap(), 25);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stateless_with_string_types() {
|
||||
// Test stateless tasks with complex types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
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));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(greeting).unwrap(), "Hello, World!");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for concurrent access patterns and race conditions
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tests for forced execution interleaving patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -272,113 +272,3 @@ async fn test_layered_interleaving() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_branch_synchronization() -> DagResult<()> {
|
||||
// Test synchronization between parallel branches
|
||||
let dag = DagRunner::new();
|
||||
let sync_point = Arc::new(AtomicUsize::new(0));
|
||||
let events = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Branch A
|
||||
let a1: TaskHandle<_> = dag
|
||||
.add_task(task_fn::<(), _, _>({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
events.lock().push("A1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}
|
||||
}))
|
||||
.into();
|
||||
|
||||
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 self.sync.load(Ordering::SeqCst) < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("A2");
|
||||
self.sync.fetch_add(1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let a2 = dag
|
||||
.add_task(A2Task {
|
||||
sync: sync_point.clone(),
|
||||
events: events.clone(),
|
||||
})
|
||||
.depends_on(a1);
|
||||
|
||||
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 self.sync.load(Ordering::SeqCst) < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("B1");
|
||||
self.sync.fetch_add(1, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}
|
||||
|
||||
// Branch B
|
||||
let b1 = dag.add_task(B1Task {
|
||||
events: events.clone(),
|
||||
sync: sync_point.clone(),
|
||||
});
|
||||
|
||||
struct B2Task {
|
||||
sync: Arc<AtomicUsize>,
|
||||
events: Arc<parking_lot::Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl B2Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
// Wait for A2
|
||||
while self.sync.load(Ordering::SeqCst) < 3 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
self.events.lock().push("B2");
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let b2 = dag
|
||||
.add_task(B2Task {
|
||||
events: events.clone(),
|
||||
sync: sync_point.clone(),
|
||||
})
|
||||
.depends_on(b1);
|
||||
|
||||
// Merge
|
||||
let merge = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((&a2, &b2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(merge)?, 13); // 2 + 11
|
||||
|
||||
// Check forced interleaving
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(event_list, vec!["A1", "B1", "A2", "B2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,3 @@
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
mod with_tracing;
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
mod without_tracing;
|
||||
@@ -52,33 +52,6 @@ async fn test_tracing_with_subscriber() {
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_with_complex_dag() {
|
||||
// Initialize tracing subscriber
|
||||
let _ = fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create a diamond pattern
|
||||
let source = dag.add_task(Value(10)).into();
|
||||
let left = dag.add_task(Add).depends_on((&source, &source));
|
||||
let right = dag.add_task(Multiply).depends_on((&source, &source));
|
||||
let sink = dag.add_task(Add).depends_on((&left, &right));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// source = 10
|
||||
// left = 10 + 10 = 20
|
||||
// right = 10 * 10 = 100
|
||||
// sink = 20 + 100 = 120
|
||||
assert_eq!(dag.get(sink).unwrap(), 120);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_inline_execution() {
|
||||
// Initialize tracing subscriber
|
||||
@@ -103,38 +76,3 @@ async fn test_tracing_inline_execution() {
|
||||
// c = 2 * 2 = 4
|
||||
assert_eq!(dag.get(c).unwrap(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_tracing_multiple_layers() {
|
||||
// Initialize tracing subscriber
|
||||
let _ = fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")),
|
||||
)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Layer 0
|
||||
let v1 = dag.add_task(Value(1));
|
||||
let v2 = dag.add_task(Value(2)).into();
|
||||
let v3 = dag.add_task(Value(3));
|
||||
|
||||
// Layer 1
|
||||
let sum12 = dag.add_task(Add).depends_on((v1, &v2));
|
||||
let mul23 = dag.add_task(Multiply).depends_on((&v2, v3));
|
||||
|
||||
// Layer 2
|
||||
let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// v1 = 1, v2 = 2, v3 = 3
|
||||
// sum12 = 1 + 2 = 3
|
||||
// mul23 = 2 * 3 = 6
|
||||
// final_sum = 3 + 6 = 9
|
||||
assert_eq!(dag.get(final_sum).unwrap(), 9);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! Tests that the library works correctly without tracing feature
|
||||
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
struct Value(i32);
|
||||
|
||||
#[task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
|
||||
#[task]
|
||||
impl Add {
|
||||
async fn run(a: &i32, b: &i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_basic_dag_without_tracing() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(Value(2));
|
||||
let b = dag.add_task(Value(3));
|
||||
let sum = dag.add_task(Add).depends_on((a, b));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(sum).unwrap(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_complex_dag_without_tracing() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Create multiple layers
|
||||
let v1 = dag.add_task(Value(1));
|
||||
let v2 = dag.add_task(Value(2));
|
||||
let v3 = dag.add_task(Value(3));
|
||||
let v4 = dag.add_task(Value(4));
|
||||
|
||||
let sum12 = dag.add_task(Add).depends_on((v1, v2));
|
||||
let sum34 = dag.add_task(Add).depends_on((v3, v4));
|
||||
let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(final_sum).unwrap(), 10);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Type safety tests for TaskHandle
|
||||
use crate::common::task_fn;
|
||||
|
||||
// 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::<(), _, _>(|_: ()| 42i32));
|
||||
|
||||
// Run DAG
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.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| async move { tokio::spawn(fut).await.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| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(double).unwrap(), 200);
|
||||
assert_eq!(dag.get(triple).unwrap(), 300);
|
||||
}
|
||||
Reference in new issue
Block a user