refactor: consolidate some tests/examples/benchmarks

This commit is contained in:
TechnoPorg committed 2026-02-12 23:31:19 +01:00
1 parent 49be497964
commit b8204c4ed9
83 files changed
+569 -10068

No files matched your search

-1348
View File
File diff suppressed because it is too large. Load diff
+1 -106
View File
@@ -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(())
}
-309
View File
@@ -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((&current_layer[0], &current_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((&current_layer[0], &current_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(())
}
-1
View File
@@ -4,6 +4,5 @@
//! by normal usage patterns.
mod deep_chains;
mod large_scale;
mod type_limits;
mod zero_tasks;
+1 -152
View File
@@ -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 -121
View File
@@ -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
-112
View File
@@ -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 -23
View File
@@ -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
View File
@@ -1,5 +1,4 @@
//! Dependency tests module
pub mod resolution;
pub mod single;
pub mod tuples;
+2 -67
View File
@@ -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(())
}
-122
View File
@@ -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 -1
View File
@@ -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]
-161
View File
@@ -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");
}
}
-1
View File
@@ -2,6 +2,5 @@
//!
//! Tests for error scenarios and recovery patterns.
mod error_types;
mod propagation;
mod recovery;
+1 -178
View File
@@ -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
View File
@@ -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(())
}
-317
View File
@@ -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);
}
-381
View File
@@ -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
View File
@@ -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);
}
-117
View File
@@ -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);
}
-7
View File
@@ -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 -1
View File
@@ -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};
-333
View File
@@ -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
View File
@@ -1,7 +1,6 @@
//! Parallelism tests module
pub mod concurrent;
pub mod execution;
pub mod spawning;
pub mod threads;
pub mod timing;
+1 -1
View File
@@ -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};
+2 -137
View File
@@ -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((&parallel1, &parallel2));
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
View File
@@ -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(())
}
-228
View File
@@ -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(())
}
-337
View File
@@ -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(())
}
-239
View File
@@ -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(())
}
-8
View File
@@ -1,8 +0,0 @@
//! DAG pattern tests
//!
//! Tests for various DAG topologies and patterns.
mod butterfly;
mod complex;
mod grid;
mod tree;
-282
View File
@@ -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(())
}
-102
View File
@@ -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());
}
}
-64
View File
@@ -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);
}
-34
View File
@@ -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
View File
@@ -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);
});
}
-493
View File
@@ -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);
});
}
-70
View File
@@ -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);
});
}
-119
View File
@@ -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);
}
-5
View File
@@ -1,5 +0,0 @@
// Test module for task pattern tests
mod combined;
mod readonly;
mod stateless;
-91
View File
@@ -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
}
-236
View File
@@ -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 -1
View File
@@ -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};
+2 -112
View File
@@ -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(())
}
-3
View File
@@ -5,6 +5,3 @@
#[cfg(feature = "tracing")]
mod with_tracing;
#[cfg(not(feature = "tracing"))]
mod without_tracing;
-62
View File
@@ -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);
}
-57
View File
@@ -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);
}
-72
View File
@@ -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);
}