feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,164 @@
|
||||
// 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| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap(); // Should complete without error
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_task() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 1 }));
|
||||
let b = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(&a);
|
||||
let c = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(b);
|
||||
let d = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(c);
|
||||
let e = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(d);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 |_: ()| async move { i * 2 })))
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task).unwrap(), i * 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_dependency() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let a = dag.add_task(task_fn(|_: ()| async { 10 }));
|
||||
let b = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 5 }))
|
||||
.depends_on(&a);
|
||||
let c = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(&a);
|
||||
let d = dag
|
||||
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
|
||||
.depends_on((&b, &c));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { "hello".to_string() }));
|
||||
let vec_task = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3] }));
|
||||
let struct_task = dag.add_task(task_fn(|_: ()| async {
|
||||
User {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
}
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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 = dag.add_task(task_fn(|_: ()| async { 10 }));
|
||||
|
||||
// Branch 1
|
||||
let branch1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(&source);
|
||||
let sink1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(branch1);
|
||||
|
||||
// Branch 2
|
||||
let branch2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 5 }))
|
||||
.depends_on(&source);
|
||||
let sink2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 3 }))
|
||||
.depends_on(branch2);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Example pattern tests for DagRunner
|
||||
use crate::common::task_fn;
|
||||
|
||||
use crate::common::tasks::*;
|
||||
use dagx::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_example_a_fan_out() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let base = dag.add_task(Seed::new(10)); // Input=() → no depends_on, returns TaskHandle
|
||||
let plus1 = dag.add_task(Inc::new()).depends_on(&base); // 11
|
||||
let plus2 = dag.add_task(Inc::new()).depends_on(&base); // 11
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap(); // SPEC API with spawner
|
||||
|
||||
assert_eq!(dag.get(plus1).unwrap(), 11);
|
||||
assert_eq!(dag.get(plus2).unwrap(), 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_example_b_fan_in() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let s = dag.add_task(task_fn(|_: ()| async { "hi".to_owned() }));
|
||||
let n = dag.add_task(task_fn(|_: ()| async { 42usize }));
|
||||
let f = dag.add_task(task_fn(|_: ()| async { true }));
|
||||
|
||||
let output = dag.add_task(OutputResult::new()).depends_on((&s, &n, &f));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(&dag.get(output).unwrap(), "hi:42:true");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_example_c_many_to_many() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let x = dag.add_task(Seed::new(2));
|
||||
let y = dag.add_task(Seed::new(3));
|
||||
let z = dag.add_task(Seed::new(5));
|
||||
|
||||
let sum_xy = dag.add_task(Add::new()).depends_on((&x, &y)); // 5
|
||||
let prod_yz = dag.add_task(Mul::new()).depends_on((&y, &z)); // 15
|
||||
let total = dag.add_task(Add::new()).depends_on((&sum_xy, &prod_yz)); // 20
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dag.get(total).unwrap(), 20);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_input_forms() {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let s = dag.add_task(task_fn(|_: ()| async { "hello".to_owned() }));
|
||||
|
||||
// Preferred form: &node
|
||||
let upper1 = dag
|
||||
.add_task(task_fn(|s: String| async move { s.to_uppercase() }))
|
||||
.depends_on(&s);
|
||||
|
||||
// Also allowed: (&node,)
|
||||
let upper2 = dag
|
||||
.add_task(task_fn(|s: String| async move { s.to_uppercase() }))
|
||||
.depends_on((&s,));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&dag.get(upper1).unwrap(), "HELLO");
|
||||
assert_eq!(&dag.get(upper2).unwrap(), "HELLO");
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
//! Integration tests for end-to-end functionality
|
||||
|
||||
use dagx::{DagResult, DagRunner, Task};
|
||||
use crate::common::task_fn;
|
||||
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| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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(|x: i32| async move { x + 5 }))
|
||||
.depends_on(step1);
|
||||
let step3 = dag.add_task(Multiply::new(3)).depends_on(step2);
|
||||
final_nodes.push(step3);
|
||||
}
|
||||
|
||||
dag.run(|fut| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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| { tokio::spawn(fut); }).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 |_: ()| async move {
|
||||
sleep(sleep_duration).await;
|
||||
i
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| { tokio::spawn(fut); }).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 |_: ()| async 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(move |x: i32| async move {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
})).depends_on(¶llel_tasks[0]);
|
||||
|
||||
let seq2 = dag.add_task(task_fn(move |x: i32| async move {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
})).depends_on(seq1);
|
||||
|
||||
let seq3 = dag.add_task(task_fn(move |x: i32| async move {
|
||||
sleep(sleep_duration).await;
|
||||
x + 1
|
||||
})).depends_on(seq2);
|
||||
|
||||
let start = Instant::now();
|
||||
dag.run(|fut| { tokio::spawn(fut); }).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(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Test module for execution tests
|
||||
|
||||
mod basic;
|
||||
mod examples;
|
||||
mod results;
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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(|_: ()| async { Ok::<i32, String>(42) }));
|
||||
let error_task = dag.add_task(task_fn(|_: ()| async {
|
||||
Err::<i32, String>("error".to_string())
|
||||
}));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 100 }));
|
||||
|
||||
// Test that DagResult<()> works with ? operator pattern
|
||||
async fn run_dag(dag: &DagRunner) -> DagResult<()> {
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 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(|_: ()| async { 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()"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_successful_execution_returns_ok() {
|
||||
let dag = DagRunner::new();
|
||||
let task = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
|
||||
// Should return Ok(())
|
||||
let result = dag
|
||||
.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 5 }));
|
||||
let counter = dag.add_task(Counter::new(10)).depends_on(&input);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.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(|_: ()| async { 5 }));
|
||||
let scaled = dag
|
||||
.add_task(task_fn(move |x: i32| async move { x * multiplier }))
|
||||
.depends_on(&base);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(dag.get(scaled).unwrap(), 35);
|
||||
}
|
||||
Reference in new issue
Block a user