feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,224 @@
|
||||
//! Butterfly pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
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 |_: ()| async move { i as f64 })))
|
||||
.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(|(a, b): (f64, f64)| async move { (a + b) / 2.0 }))
|
||||
.depends_on((&inputs[idx1], &inputs[idx2])),
|
||||
);
|
||||
// Difference operation
|
||||
layer1.push(
|
||||
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { (a - b) / 2.0 }))
|
||||
.depends_on((&inputs[idx1], &inputs[idx2])),
|
||||
);
|
||||
}
|
||||
|
||||
// Layer 2: crossover connections
|
||||
let layer2: Vec<_> = vec![
|
||||
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b }))
|
||||
.depends_on((&layer1[0], &layer1[4])),
|
||||
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b }))
|
||||
.depends_on((&layer1[1], &layer1[5])),
|
||||
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a + b }))
|
||||
.depends_on((&layer1[2], &layer1[6])),
|
||||
dag.add_task(task_fn(|(a, b): (f64, f64)| async move { a - b }))
|
||||
.depends_on((&layer1[3], &layer1[7])),
|
||||
];
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((&inputs[i], &inputs[i + mid]));
|
||||
|
||||
let diff = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { (a - b).abs() }))
|
||||
.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 |_: ()| async move { val }));
|
||||
(&task).into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let outputs = build_butterfly(&dag, inputs);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async move { 1 << i }))) // Powers of 2
|
||||
.collect();
|
||||
|
||||
// First stage: sort pairs
|
||||
let stage1: Vec<_> = vec![
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) }))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) }))
|
||||
.depends_on((&inputs[0], &inputs[1])),
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.min(b) }))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a.max(b) }))
|
||||
.depends_on((&inputs[2], &inputs[3])),
|
||||
];
|
||||
|
||||
// Second stage with crossover
|
||||
let outputs: Vec<_> = vec![
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b }))
|
||||
.depends_on((&stage1[0], &stage1[2])),
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move { a | b }))
|
||||
.depends_on((&stage1[1], &stage1[3])),
|
||||
];
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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<_> = (0..8)
|
||||
.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();
|
||||
|
||||
// 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(move |x: i32| async move { 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(|(a, b): (i32, i32)| async move {
|
||||
if a > b {
|
||||
b
|
||||
} else {
|
||||
a
|
||||
}
|
||||
}))
|
||||
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
|
||||
dag.add_task(task_fn(|(a, b): (i32, i32)| async move {
|
||||
if a > b {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}))
|
||||
.depends_on((&shuffled[idx], &shuffled[idx + 1])),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 8);
|
||||
|
||||
for task in exchanged {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
//! Complex DAG pattern tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
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(|_: ()| async { 42 }));
|
||||
|
||||
let validate = dag
|
||||
.add_task(task_fn(|x: i32| async move {
|
||||
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<i32, &str>| async move {
|
||||
match result {
|
||||
Ok(x) => x * 2,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}))
|
||||
.depends_on(&validate);
|
||||
|
||||
// Parallel enhancement steps
|
||||
let enhance1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 10 }))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 3 }))
|
||||
.depends_on(process);
|
||||
|
||||
let enhance3 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x - 5 }))
|
||||
.depends_on(process);
|
||||
|
||||
// Combine enhancements
|
||||
let combine = dag
|
||||
.add_task(task_fn(|(e1, e2, e3): (i32, i32, i32)| async move {
|
||||
vec![e1, e2, e3]
|
||||
}))
|
||||
.depends_on((&enhance1, &enhance2, &enhance3));
|
||||
|
||||
// Final aggregation
|
||||
let final_result = dag
|
||||
.add_task(task_fn(|values: Vec<i32>| async move {
|
||||
values.iter().sum::<i32>()
|
||||
}))
|
||||
.depends_on(combine);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async 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(
|
||||
move |inputs: (
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
Vec<(&str, usize)>,
|
||||
)| async move {
|
||||
let mut combined = Vec::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(|data: Vec<(&str, usize)>| async move {
|
||||
// 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)| async move { r1 + r2 + r3 + r4 },
|
||||
))
|
||||
.depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3]));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 1 }));
|
||||
|
||||
// Stage 1: Transform
|
||||
let stage1 = dag
|
||||
.add_task(task_fn({
|
||||
let counter = iteration_count.clone();
|
||||
move |x: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&init);
|
||||
|
||||
// Stage 2: Filter
|
||||
let stage2 = dag
|
||||
.add_task(task_fn(|x: i32| async move {
|
||||
if x < 100 {
|
||||
Some(x)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
.depends_on(stage1);
|
||||
|
||||
// Stage 3: Accumulate
|
||||
let stage3 = dag
|
||||
.add_task(task_fn(|opt: Option<i32>| async move {
|
||||
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(|x: i32| async move {
|
||||
// In a real feedback loop, this would feed back to stage1
|
||||
x / 2
|
||||
}))
|
||||
.depends_on(stage3);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 = dag.add_task(task_fn(|_: ()| async {
|
||||
vec![10, 20, 30, 40, 50, 60, 70, 80]
|
||||
}));
|
||||
|
||||
// Workers process chunks
|
||||
let workers: Vec<_> = (0..4)
|
||||
.map(|worker_id| {
|
||||
dag.add_task(task_fn(move |data: Vec<i32>| async move {
|
||||
// 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(
|
||||
|(w0, w1, w2, w3): (i32, i32, i32, i32)| async move { w0 + w1 + w2 + w3 },
|
||||
))
|
||||
.depends_on((&workers[0], &workers[1], &workers[2], &workers[3]));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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();
|
||||
async move {
|
||||
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(move |x: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
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(move |x: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
x * 2 + i
|
||||
}
|
||||
}))
|
||||
.depends_on(prev2);
|
||||
path2.push(task);
|
||||
prev2 = task;
|
||||
}
|
||||
|
||||
// Barrier: wait for all paths
|
||||
let barrier = dag
|
||||
.add_task(task_fn({
|
||||
let counter = phase_counter.clone();
|
||||
move |(p1, p2): (i32, i32)| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.store(2, Ordering::SeqCst);
|
||||
p1 + p2
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on((&path1[2], &path2[2]));
|
||||
|
||||
// Join phase
|
||||
let join = dag
|
||||
.add_task(task_fn({
|
||||
let counter = phase_counter.clone();
|
||||
move |x: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 2);
|
||||
x
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(barrier);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let _ = dag.get(join)?;
|
||||
assert_eq!(phase_counter.load(Ordering::SeqCst), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
//! 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
|
||||
for i in 0..4 {
|
||||
let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 }));
|
||||
grid[0][i] = Some((&task).into());
|
||||
if i > 0 {
|
||||
let task = dag.add_task(task_fn(move |_: ()| async move { i as i32 * 10 }));
|
||||
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(move |(a, l): (i32, i32)| async move { a + l }))
|
||||
.depends_on((above, left));
|
||||
grid[i][j] = Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 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 |_: ()| async move { x + y + z }));
|
||||
(&t).into()
|
||||
}
|
||||
1 => dag
|
||||
.add_task(task_fn(move |prev: usize| async move { prev + 1 }))
|
||||
.depends_on(deps[0]),
|
||||
2 => dag
|
||||
.add_task(task_fn(move |(a, b): (usize, usize)| async move { a + b }))
|
||||
.depends_on((&deps[0], &deps[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn(
|
||||
move |(a, b, c): (usize, usize, usize)| async move { a + b + c },
|
||||
))
|
||||
.depends_on((&deps[0], &deps[1], &deps[2])),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
grid[x][y][z] = Some(task_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 100 }));
|
||||
let center: dagx::TaskHandle<i32> = (¢er_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(
|
||||
move |c: i32| async move { 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(move |(a, b): (i32, i32)| async move {
|
||||
(a + b) / 2 + q + r
|
||||
}))
|
||||
.depends_on((&neighbors[0], &neighbors[1]))
|
||||
} else if neighbors.len() == 1 {
|
||||
dag.add_task(task_fn(move |n: i32| async move { n + q + r }))
|
||||
.depends_on(neighbors[0])
|
||||
} else {
|
||||
let t = dag.add_task(task_fn(move |_: ()| async move { q + r }));
|
||||
(&t).into()
|
||||
};
|
||||
|
||||
hex_grid.insert((q, r), task);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async 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)| async move {
|
||||
// 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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Verify all torus tasks completed
|
||||
for task in torus_tasks {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! DAG pattern tests
|
||||
//!
|
||||
//! Tests for various DAG topologies and patterns.
|
||||
|
||||
mod butterfly;
|
||||
mod complex;
|
||||
mod grid;
|
||||
mod tree;
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Tree pattern DAG tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
#[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 |_: ()| async 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(|(a, b): (i32, i32)| async move { 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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async 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(move |(a, b, c): (i32, i32, i32)| async move {
|
||||
value + a + b + c
|
||||
}))
|
||||
.depends_on((&children[0], &children[1], &children[2]))
|
||||
}
|
||||
|
||||
let root = build_tree(&dag, 3, 1);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 = dag.add_task(task_fn(|_: ()| async { 1 }));
|
||||
let g = dag.add_task(task_fn(|_: ()| async { 2 }));
|
||||
|
||||
let c = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(&f);
|
||||
|
||||
let d_builder = dag.add_task(task_fn(|_: ()| async { 3 }));
|
||||
let d: dagx::TaskHandle<i32> = (&d_builder).into();
|
||||
|
||||
let e = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 3 }))
|
||||
.depends_on(&g);
|
||||
|
||||
let a = dag
|
||||
.add_task(task_fn(|(c, d): (i32, i32)| async move { c + d }))
|
||||
.depends_on((&c, &d));
|
||||
|
||||
let b = dag
|
||||
.add_task(task_fn(|e: i32| async move { e + 10 }))
|
||||
.depends_on(e);
|
||||
|
||||
let root = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a * b }))
|
||||
.depends_on((&a, &b));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 = dag.add_task(task_fn(|_: ()| async { "root".to_string() }));
|
||||
|
||||
// First level
|
||||
let usr = dag
|
||||
.add_task(task_fn(|r: String| async move { format!("{}/usr", r) }))
|
||||
.depends_on(&root);
|
||||
|
||||
let etc = dag
|
||||
.add_task(task_fn(|r: String| async move { format!("{}/etc", r) }))
|
||||
.depends_on(&root);
|
||||
|
||||
let var = dag
|
||||
.add_task(task_fn(|r: String| async move { format!("{}/var", r) }))
|
||||
.depends_on(&root);
|
||||
|
||||
// Second level
|
||||
let usr_bin = dag
|
||||
.add_task(task_fn(|u: String| async move { format!("{}/bin", u) }))
|
||||
.depends_on(usr);
|
||||
|
||||
let usr_lib = dag
|
||||
.add_task(task_fn(|u: String| async move { format!("{}/lib", u) }))
|
||||
.depends_on(usr);
|
||||
|
||||
let etc_config = dag
|
||||
.add_task(task_fn(|e: String| async move { format!("{}/config", e) }))
|
||||
.depends_on(etc);
|
||||
|
||||
let var_log = dag
|
||||
.add_task(task_fn(|v: String| async move { format!("{}/log", v) }))
|
||||
.depends_on(var);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async 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(move |(a, b): (usize, usize)| async move {
|
||||
node_id + a + b
|
||||
}))
|
||||
.depends_on((&children[0], &children[1])),
|
||||
3 => dag
|
||||
.add_task(task_fn(
|
||||
move |(a, b, c): (usize, usize, usize)| async move { node_id + a + b + c },
|
||||
))
|
||||
.depends_on((&children[0], &children[1], &children[2])),
|
||||
4 => dag
|
||||
.add_task(task_fn(
|
||||
move |(a, b, c, d): (usize, usize, usize, usize)| async move {
|
||||
node_id + a + b + c + d
|
||||
},
|
||||
))
|
||||
.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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async 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(|(left, right): (String, String)| async move {
|
||||
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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
let result = dag.get(root_hash)?;
|
||||
assert!(result.starts_with("hash"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user