feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,328 @@
|
||||
//! Tests for extremely deep dependency chains
|
||||
|
||||
use crate::common::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(|_: ()| async { 0 }));
|
||||
let mut current = (&first).into(); // Convert to TaskHandle
|
||||
|
||||
for _i in 1..1000 {
|
||||
current = dag
|
||||
.add_task(task_fn(move |prev: i32| async move { prev + 1 }))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 0 }));
|
||||
let mut main_chain: dagx::TaskHandle<i32> = (&first).into();
|
||||
let mut branches = Vec::new();
|
||||
|
||||
for i in 1..500 {
|
||||
// Main chain continues
|
||||
main_chain = dag
|
||||
.add_task(task_fn(move |prev: i32| async move { prev + 1 }))
|
||||
.depends_on(main_chain);
|
||||
|
||||
// Create a branch at every 10th node
|
||||
if i % 10 == 0 {
|
||||
let branch = dag
|
||||
.add_task(task_fn(move |main_val: i32| async move { main_val * 2 }))
|
||||
.depends_on(main_chain);
|
||||
branches.push(branch);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let mut chain_ends = Vec::new();
|
||||
|
||||
for chain_id in 0..8 {
|
||||
let first = dag.add_task(task_fn(move |_: ()| async move { chain_id * 1000 }));
|
||||
let mut current: dagx::TaskHandle<i32> = (&first).into();
|
||||
|
||||
for _ in 0..500 {
|
||||
current = dag
|
||||
.add_task(task_fn(|prev: i32| async move { prev + 1 }))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
chain_ends.push(current);
|
||||
}
|
||||
|
||||
// Add a final task that depends on all chains
|
||||
let final_task = dag
|
||||
.add_task(task_fn(
|
||||
|inputs: (i32, i32, i32, i32, i32, i32, i32, i32)| async move {
|
||||
inputs.0
|
||||
+ inputs.1
|
||||
+ inputs.2
|
||||
+ inputs.3
|
||||
+ inputs.4
|
||||
+ inputs.5
|
||||
+ inputs.6
|
||||
+ inputs.7
|
||||
},
|
||||
))
|
||||
.depends_on((
|
||||
&chain_ends[0],
|
||||
&chain_ends[1],
|
||||
&chain_ends[2],
|
||||
&chain_ends[3],
|
||||
&chain_ends[4],
|
||||
&chain_ends[5],
|
||||
&chain_ends[6],
|
||||
&chain_ends[7],
|
||||
));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Each chain: start + 500
|
||||
// Chain 0: 0 + 500 = 500
|
||||
// Chain 1: 1000 + 500 = 1500
|
||||
// ...
|
||||
// Sum = 500*8 + (0+1000+2000+...+7000) = 4000 + 28000 = 32000
|
||||
assert_eq!(dag.get(final_task)?, 32000);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_chain_execution_order() -> DagResult<()> {
|
||||
// Verify tasks in a deep chain execute in correct order
|
||||
let dag = DagRunner::new();
|
||||
let order = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let first = dag.add_task(task_fn({
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let order = order.clone();
|
||||
async move {
|
||||
assert_eq!(order.fetch_add(1, Ordering::SeqCst), 0);
|
||||
0
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut current: dagx::TaskHandle<i32> = (&first).into();
|
||||
|
||||
for expected in 1..100 {
|
||||
current = dag
|
||||
.add_task(task_fn({
|
||||
let order = order.clone();
|
||||
move |prev: i32| {
|
||||
let order = order.clone();
|
||||
async move {
|
||||
assert_eq!(order.fetch_add(1, Ordering::SeqCst), expected);
|
||||
prev + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(order.load(Ordering::SeqCst), 100);
|
||||
assert_eq!(dag.get(current)?, 99);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deep_binary_tree() -> DagResult<()> {
|
||||
// Test a deep binary tree structure (depth 10 = 1023 nodes)
|
||||
let dag = DagRunner::new();
|
||||
|
||||
fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle<i32> {
|
||||
if depth == 0 {
|
||||
let task = dag.add_task(task_fn(move |_: ()| async move { value }));
|
||||
(&task).into() // Convert TaskBuilder to TaskHandle
|
||||
} else {
|
||||
let left = build_tree(dag, depth - 1, value * 2);
|
||||
let right = build_tree(dag, depth - 1, value * 2 + 1);
|
||||
|
||||
dag.add_task(task_fn(move |(l, r): (i32, i32)| async move { l + r }))
|
||||
.depends_on((&left, &right))
|
||||
}
|
||||
}
|
||||
|
||||
let root = build_tree(&dag, 10, 1);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Sum of all values in a complete binary tree with values 1024..2047
|
||||
// This is sum from 2^10 to 2^11-1 = 1024 * 1023 / 2 + 1024 * 512 = 1047552
|
||||
let result = dag.get(root)?;
|
||||
assert!(result > 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fibonacci_chain() -> DagResult<()> {
|
||||
// Test a Fibonacci-like dependency pattern
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let f0_builder = dag.add_task(task_fn(|_: ()| async { 0i64 }));
|
||||
let f1_builder = dag.add_task(task_fn(|_: ()| async { 1i64 }));
|
||||
|
||||
let mut prev2: dagx::TaskHandle<i64> = (&f0_builder).into();
|
||||
let mut prev1: dagx::TaskHandle<i64> = (&f1_builder).into();
|
||||
|
||||
for _ in 2..50 {
|
||||
let next = dag
|
||||
.add_task(task_fn(|(a, b): (i64, i64)| async move { a + b }))
|
||||
.depends_on((&prev2, &prev1));
|
||||
|
||||
prev2 = prev1;
|
||||
prev1 = next;
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// 50th Fibonacci number (0-indexed) = 7778742049
|
||||
assert_eq!(dag.get(prev1)?, 7778742049);
|
||||
|
||||
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(|_: ()| async { 0i64 }));
|
||||
let mut current: dagx::TaskHandle<i64> = (&first).into();
|
||||
|
||||
for i in 1..=200 {
|
||||
current = dag
|
||||
.add_task(task_fn(move |sum: i64| async move { sum + i as i64 }))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a_first = dag.add_task(task_fn(|_: ()| async { 1 }));
|
||||
let b_first = dag.add_task(task_fn(|_: ()| async { 2 }));
|
||||
|
||||
let mut chain_a: dagx::TaskHandle<i32> = (&a_first).into();
|
||||
let mut chain_b: dagx::TaskHandle<i32> = (&b_first).into();
|
||||
|
||||
for i in 0..100 {
|
||||
if i % 2 == 0 {
|
||||
// A depends on B
|
||||
chain_a = dag
|
||||
.add_task(task_fn(move |b_val: i32| async move { b_val + 1 }))
|
||||
.depends_on(chain_b);
|
||||
} else {
|
||||
// B depends on A
|
||||
chain_b = dag
|
||||
.add_task(task_fn(move |a_val: i32| async move { a_val + 2 }))
|
||||
.depends_on(chain_a);
|
||||
}
|
||||
}
|
||||
|
||||
// Final task depends on both chains
|
||||
let final_task = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((&chain_a, &chain_b));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = dag.get(final_task)?;
|
||||
assert!(result > 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[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(|_: ()| async { 0u64 }));
|
||||
let mut current: dagx::TaskHandle<u64> = (&first).into();
|
||||
|
||||
for _ in 1..10_000 {
|
||||
current = dag
|
||||
.add_task(task_fn(|prev: u64| async move { prev + 1 }))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(current)?, 9_999);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! 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 tasks: Vec<_> = (0..10_000)
|
||||
.map(|i| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
i
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 10_000);
|
||||
|
||||
// Spot check some results
|
||||
assert_eq!(dag.get(&tasks[0])?, 0);
|
||||
assert_eq!(dag.get(&tasks[9_999])?, 9_999);
|
||||
assert_eq!(dag.get(&tasks[5_000])?, 5_000);
|
||||
|
||||
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 |_: ()| async 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(|a: i32| async move { a }))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((deps[0], deps[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn(
|
||||
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
|
||||
))
|
||||
.depends_on((deps[0], deps[1], deps[2])),
|
||||
4 => dag
|
||||
.add_task(task_fn(|(a, b, c, d): (i32, i32, i32, i32)| async move {
|
||||
a + b + c + d
|
||||
}))
|
||||
.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(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((¤t_layer[0], ¤t_layer[1]));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(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 |_: ()| async move { i % 10 })))
|
||||
.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(move |(a, b, c): (i32, i32, i32)| async move {
|
||||
a + b + c + i as i32
|
||||
}))
|
||||
.depends_on((&sources[idx1], &sources[idx2], &sources[idx3]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
chain_id * 100
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut current: dagx::TaskHandle<i32> = (&first).into();
|
||||
|
||||
for _i in 1..100 {
|
||||
current = dag
|
||||
.add_task(task_fn({
|
||||
let counter = counter.clone();
|
||||
move |prev: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
prev + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(current);
|
||||
}
|
||||
|
||||
chain_ends.push(current);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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]
|
||||
#[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();
|
||||
async move {
|
||||
completed.fetch_add(1, Ordering::Relaxed);
|
||||
task_id
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(completed.load(Ordering::Relaxed), 100_000);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Boundary and edge case tests for dagx
|
||||
//!
|
||||
//! Tests extreme values, limits, and edge cases that might not be covered
|
||||
//! by normal usage patterns.
|
||||
|
||||
mod deep_chains;
|
||||
mod large_scale;
|
||||
mod type_limits;
|
||||
mod zero_tasks;
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Tests for type system edge cases and limits
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zero_sized_types_throughout() -> DagResult<()> {
|
||||
// Test with ZSTs at every level
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Empty;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PhantomWrapper<T> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Task producing ZST
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async { Empty }));
|
||||
|
||||
// Task that also produces ZST (independent since we can't extract custom types)
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
|
||||
// Task with PhantomData
|
||||
let _t3 = dag.add_task(task_fn(|_: ()| async {
|
||||
PhantomWrapper::<i32> {
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}));
|
||||
|
||||
// Task that depends on t2 (uses primitive type)
|
||||
let t4 = dag
|
||||
.add_task(task_fn(|val: i32| async move {
|
||||
assert_eq!(val, 42);
|
||||
"success"
|
||||
}))
|
||||
.depends_on(&t2);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t1)?, Empty);
|
||||
assert_eq!(dag.get(t2)?, 42);
|
||||
assert_eq!(dag.get(t4)?, "success");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_large_value_types() -> DagResult<()> {
|
||||
// Test with very large value types
|
||||
#[derive(Clone)]
|
||||
struct LargeStruct {
|
||||
data: [u8; 8192], // 8KB
|
||||
more_data: Vec<u64>,
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async {
|
||||
let large = LargeStruct {
|
||||
data: [42; 8192],
|
||||
more_data: vec![1; 1000],
|
||||
};
|
||||
// Return a calculated value instead of the struct itself
|
||||
large.data[0] as usize + large.more_data.len()
|
||||
}));
|
||||
|
||||
let t2 = dag
|
||||
.add_task(task_fn(|val: usize| async move {
|
||||
val // Just pass through to verify
|
||||
}))
|
||||
.depends_on(&t1);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t2)?, 1042);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unit_type_chains() -> DagResult<()> {
|
||||
// Test chains of unit type operations
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async {}));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async {})).depends_on(&t1);
|
||||
let t3 = dag.add_task(task_fn(|_: ()| async {})).depends_on(t2);
|
||||
let t4 = dag
|
||||
.add_task(task_fn(|_: ()| async { "done" }))
|
||||
.depends_on(&t3);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
dag.get(t1)?; // Verify no error
|
||||
dag.get(t2)?;
|
||||
dag.get(t3)?;
|
||||
assert_eq!(dag.get(t4)?, "done");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_option_result_types() -> DagResult<()> {
|
||||
// Test deeply nested generic types
|
||||
type ComplexType = Result<Option<Result<Vec<Option<i32>>, String>>, &'static str>;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async {
|
||||
let val: ComplexType = Ok(Some(Ok(vec![Some(42), None, Some(13)])));
|
||||
val
|
||||
}));
|
||||
|
||||
let t2 = dag
|
||||
.add_task(task_fn(|complex: ComplexType| async move {
|
||||
match complex {
|
||||
Ok(Some(Ok(vec))) => vec.iter().filter_map(|x| *x).sum::<i32>(),
|
||||
_ => 0,
|
||||
}
|
||||
}))
|
||||
.depends_on(&t1);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t2)?, 55);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tuple_of_tuples() -> DagResult<()> {
|
||||
// Test nested tuple types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async { ((1, 2), (3, 4)) }));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async { ((5, 6), (7, 8)) }));
|
||||
|
||||
let t3 = dag
|
||||
.add_task(task_fn(
|
||||
|inputs: (((i32, i32), (i32, i32)), ((i32, i32), (i32, i32)))| async move {
|
||||
let (((a, b), (c, d)), ((e, f), (g, h))) = inputs;
|
||||
a + b + c + d + e + f + g + h
|
||||
},
|
||||
))
|
||||
.depends_on((&t1, &t2));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t3)?, 36);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_string_types_variety() -> DagResult<()> {
|
||||
// Test various string types
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async { "static str" }));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async { String::from("owned string") }));
|
||||
let t3 = dag.add_task(task_fn(|_: ()| async {
|
||||
std::borrow::Cow::Borrowed("cow str")
|
||||
}));
|
||||
let t4 = dag.add_task(task_fn(|_: ()| async { Box::new("boxed str") }));
|
||||
let t5 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str 2") }));
|
||||
let t6 = dag.add_task(task_fn(|_: ()| async { std::sync::Arc::new("arc str") }));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn(
|
||||
|(s1, s2, s3, s4, s5, s6): (
|
||||
&str,
|
||||
String,
|
||||
std::borrow::Cow<str>,
|
||||
Box<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
std::sync::Arc<&str>,
|
||||
)| async move { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) },
|
||||
))
|
||||
.depends_on((&t1, &t2, &t3, &t4, &t5, &t6));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { Wrapper(42) }));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async { Wrapper("hello") }));
|
||||
let t3 = dag.add_task(task_fn(|_: ()| async { Wrapper(vec![1, 2, 3]) }));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn(
|
||||
|(w1, w2, w3): (Wrapper<i32>, Wrapper<&str>, Wrapper<Vec<i32>>)| async move {
|
||||
format!("{:?} {:?} {:?}", w1, w2, w3)
|
||||
},
|
||||
))
|
||||
.depends_on((&t1, &t2, &t3));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let t1 = dag.add_task(task_fn(|_: ()| async {
|
||||
let arc = Arc::new(Mutex::new(100));
|
||||
// Extract value instead of passing Arc
|
||||
let guard = arc.lock().unwrap();
|
||||
*guard
|
||||
}));
|
||||
|
||||
let t2 = dag
|
||||
.add_task(task_fn(|val: i32| async move { val * 2 }))
|
||||
.depends_on(&t1);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(t2)?, 200);
|
||||
|
||||
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(|_: ()| async { [1, 2, 3, 4, 5] }));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async { [6, 7, 8, 9, 10] }));
|
||||
|
||||
let t3 = dag
|
||||
.add_task(task_fn(|(a1, a2): ([i32; 5], [i32; 5])| async move {
|
||||
let mut sum = 0;
|
||||
for i in 0..5 {
|
||||
sum += a1[i] + a2[i];
|
||||
}
|
||||
sum
|
||||
}))
|
||||
.depends_on((&t1, &t2));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { MyEnum::Unit }));
|
||||
let t2 = dag.add_task(task_fn(|_: ()| async {
|
||||
MyEnum::Tuple(42, "test".to_string())
|
||||
}));
|
||||
let t3 = dag.add_task(task_fn(|_: ()| async { MyEnum::Struct { x: 10, y: 20 } }));
|
||||
let t4 = dag.add_task(task_fn(|_: ()| async { MyEnum::LargeVariant([0; 1024]) }));
|
||||
|
||||
let combined = dag
|
||||
.add_task(task_fn(
|
||||
|(e1, e2, e3, e4): (MyEnum, MyEnum, MyEnum, MyEnum)| async move {
|
||||
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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert!(dag.get(combined)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Tests for empty DAGs and minimal task configurations
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, Task};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 42 }));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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();
|
||||
async move {
|
||||
exec.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { ZeroSized }));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dag_with_only_source_tasks() -> DagResult<()> {
|
||||
// Test a DAG with multiple source tasks but no dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..100)
|
||||
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_self_contained_stateful_task() -> DagResult<()> {
|
||||
// Test a single stateful task with internal state
|
||||
struct Counter {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Counter {
|
||||
async fn run(&self) -> i32 {
|
||||
self.value * 2
|
||||
}
|
||||
}
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(Counter { value: 21 });
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 42);
|
||||
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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
// DAG with a task
|
||||
let task = dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { Result::<i32, Never>::Ok(42) }));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let task = dag.add_task(task_fn(|_: ()| async {
|
||||
// Just yield once to the runtime
|
||||
tokio::task::yield_now().await;
|
||||
1337
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(task)?, 1337);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user