364 lines
11 KiB
Rust
364 lines
11 KiB
Rust
//! Integration tests for end-to-end functionality
|
|
|
|
use dagx::{DagResult, DagRunner};
|
|
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(())
|
|
} |