diff --git a/.beads/dagx.db b/.beads/dagx.db index 73b4404..40156e1 100644 Binary files a/.beads/dagx.db and b/.beads/dagx.db differ diff --git a/examples/custom_types.rs b/examples/custom_types.rs new file mode 100644 index 0000000..8467bc4 --- /dev/null +++ b/examples/custom_types.rs @@ -0,0 +1,119 @@ +//! # Custom Types: Using Your Own Structs +//! +//! This example demonstrates that **ANY custom type** works in dagx as long as it implements +//! `Clone + Send + Sync + 'static`. You don't need to implement any special traits! +//! +//! ## What You'll Learn +//! - How to use custom structs as task outputs +//! - How custom types flow through the DAG automatically +//! - How one task's output can be consumed by multiple downstream tasks +//! - No trait implementations required (just derive Clone!) +//! +//! ## Key Insight +//! +//! **dagx works with ANY type automatically!** The `#[task]` macro generates type-specific +//! extraction logic, so you don't need to implement any special traits. Just make sure your +//! type implements: +//! - `Clone` (so it can be extracted from Arc) +//! - `Send + Sync` (required for async tasks) +//! - `'static` (no lifetime constraints) +//! +//! That's it! No special traits, no annotations, no boilerplate. +//! +//! ## Running This Example +//! ```bash +//! cargo run --example custom_types +//! ``` +//! +//! ## Expected Output +//! ```text +//! Creating custom types example... +//! +//! Creating user: Alice (age: 30) +//! Processing user Alice for greeting +//! Processing user Alice for validation +//! Greeting: Hello, Alice! +//! Validation: Alice is valid (age 30 >= 18) +//! ``` + +use dagx::{task, DagRunner}; + +// Custom type - just derive Clone! +#[derive(Clone, Debug)] +struct User { + name: String, + age: u32, +} + +// Task that produces a custom type +struct CreateUser { + name: String, + age: u32, +} + +#[task] +impl CreateUser { + async fn run(&self) -> User { + println!("Creating user: {} (age: {})", self.name, self.age); + User { + name: self.name.clone(), + age: self.age, + } + } +} + +// Task that consumes the custom type and produces a String +struct GreetUser; + +#[task] +impl GreetUser { + async fn run(user: &User) -> String { + println!("Processing user {} for greeting", user.name); + format!("Hello, {}!", user.name) + } +} + +// Another task that also consumes the same custom type +// This demonstrates fan-out: one output consumed by multiple tasks +struct ValidateUser; + +#[task] +impl ValidateUser { + async fn run(user: &User) -> String { + println!("Processing user {} for validation", user.name); + if user.age >= 18 { + format!("{} is valid (age {} >= 18)", user.name, user.age) + } else { + format!("{} is too young (age {} < 18)", user.name, user.age) + } + } +} + +#[tokio::main] +async fn main() { + println!("Creating custom types example...\n"); + + let dag = DagRunner::new(); + + // Create a task that produces a custom User type + let user_task = dag.add_task(CreateUser { + name: "Alice".to_string(), + age: 30, + }); + + // Two different tasks consume the same custom User type + // dagx automatically handles Arc wrapping, so both tasks get &User + let greeting = dag.add_task(GreetUser).depends_on(&user_task); + let validation = dag.add_task(ValidateUser).depends_on(&user_task); + + // Execute the DAG + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Print results + println!("Greeting: {}", dag.get(greeting).unwrap()); + println!("Validation: {}", dag.get(validation).unwrap()); +} diff --git a/src/deps/tests.rs b/src/deps/tests.rs index cf06c19..8ad0d1c 100644 --- a/src/deps/tests.rs +++ b/src/deps/tests.rs @@ -20,11 +20,11 @@ impl TestTask { } } +#[allow(dead_code)] struct TestTaskWithInput; #[crate::task] impl TestTaskWithInput { - #[allow(dead_code)] #[cfg(not(tarpaulin_include))] async fn run(input: &i32) -> i32 { input * 2 diff --git a/src/node/tests.rs b/src/node/tests.rs index e296f4b..88d2877 100644 --- a/src/node/tests.rs +++ b/src/node/tests.rs @@ -162,12 +162,12 @@ async fn test_execute_with_listeners_non_sink_node() { // Verify output was broadcast through announcement let listener = *listener - .downcast::>() + .downcast::>>() .unwrap(); let received = listener.listen().await; assert!(received.is_ok()); - assert_eq!(received.unwrap(), 42); + assert_eq!(*received.unwrap(), 42); // Also verify output was returned (Arc-wrapped) let output = result.unwrap(); @@ -195,11 +195,11 @@ async fn test_execute_with_multiple_listeners() { // Verify all three listeners receive the output for listener in [listener1, listener2, listener3] { let listener = *listener - .downcast::>() + .downcast::>>() .unwrap(); let received = listener.listen().await; assert!(received.is_ok()); - assert_eq!(received.unwrap(), 100); + assert_eq!(*received.unwrap(), 100); } } diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 287a9f0..93d85b7 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -301,3 +301,172 @@ async fn test_task_panic_in_multi_task_layer() { // This test mainly ensures we don't crash assert!(result.is_ok() || result.is_err()); } + +// ============================================================================ +// Coverage tests for DagRunner::get() type mismatch errors +// These tests target the error path at src/runner.rs:601-607 +// ============================================================================ + +#[tokio::test] +async fn test_get_type_mismatch_i32_to_vec() { + init_tracing(); + // Test type mismatch: i32 to Vec + + struct IntTask; + #[crate::task] + impl IntTask { + async fn run(&self) -> i32 { + 123 + } + } + + let dag = DagRunner::new(); + let int_task = dag.add_task(IntTask); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Try to get as Vec - TYPE MISMATCH! + let wrong_handle: TaskHandle> = TaskHandle { + id: int_task.id, + _phantom: std::marker::PhantomData, + }; + + let result = dag.get(wrong_handle); + assert!(result.is_err()); + match result.unwrap_err() { + DagError::TypeMismatch { expected, found } => { + assert_eq!(expected, std::any::type_name::>()); + assert_eq!(found, "unknown"); + } + _ => panic!("Expected TypeMismatch error"), + } +} + +#[tokio::test] +async fn test_get_type_mismatch_struct_to_hashmap() { + init_tracing(); + // Test type mismatch: custom struct to HashMap + + use std::collections::HashMap; + + #[derive(Clone)] + struct CustomOutput { + #[allow(dead_code)] + value: i32, + } + + struct CustomTask; + #[crate::task] + impl CustomTask { + async fn run(&self) -> CustomOutput { + CustomOutput { value: 42 } + } + } + + let dag = DagRunner::new(); + let task_handle = dag.add_task(CustomTask); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Try to get as HashMap - TYPE MISMATCH! + let wrong_handle: TaskHandle> = TaskHandle { + id: task_handle.id, + _phantom: std::marker::PhantomData, + }; + + let result = dag.get(wrong_handle); + assert!(result.is_err()); + match result.unwrap_err() { + DagError::TypeMismatch { expected, found } => { + assert!(expected.contains("HashMap")); + assert_eq!(found, "unknown"); + } + _ => panic!("Expected TypeMismatch error"), + } +} + +#[tokio::test] +async fn test_get_type_mismatch_vec_different_types() { + init_tracing(); + // Test type mismatch: Vec to Vec + + struct VecTask; + #[crate::task] + impl VecTask { + async fn run(&self) -> Vec { + vec![1, 2, 3] + } + } + + let dag = DagRunner::new(); + let vec_task = dag.add_task(VecTask); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Try to get as Vec - TYPE MISMATCH! + let wrong_handle: TaskHandle> = TaskHandle { + id: vec_task.id, + _phantom: std::marker::PhantomData, + }; + + let result = dag.get(wrong_handle); + assert!(result.is_err()); + match result.unwrap_err() { + DagError::TypeMismatch { expected, found } => { + assert_eq!(expected, std::any::type_name::>()); + assert_eq!(found, "unknown"); + } + _ => panic!("Expected TypeMismatch error"), + } +} + +#[tokio::test] +async fn test_get_type_mismatch_option_types() { + init_tracing(); + // Test type mismatch: Option to Option + + struct OptionTask; + #[crate::task] + impl OptionTask { + async fn run(&self) -> Option { + Some(42) + } + } + + let dag = DagRunner::new(); + let opt_task = dag.add_task(OptionTask); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Try to get as Option - TYPE MISMATCH! + let wrong_handle: TaskHandle> = TaskHandle { + id: opt_task.id, + _phantom: std::marker::PhantomData, + }; + + let result = dag.get(wrong_handle); + assert!(result.is_err()); + match result.unwrap_err() { + DagError::TypeMismatch { expected, found } => { + assert_eq!(expected, std::any::type_name::>()); + assert_eq!(found, "unknown"); + } + _ => panic!("Expected TypeMismatch error"), + } +} diff --git a/src/task/tests.rs b/src/task/tests.rs index 2f60539..90efb3d 100644 --- a/src/task/tests.rs +++ b/src/task/tests.rs @@ -1,72 +1,9 @@ //! Unit tests for task module -use crate::task::{task_fn, Task}; +use crate::task::Task; use std::future::Future; use std::pin::Pin; -// Test the task_fn function and TaskFn struct -#[tokio::test] -async fn test_task_fn_creation_and_execution() { - // Test line 111 in task.rs - Task implementation for TaskFn - - // Create a simple task function - let task = task_fn(|input: i32| async move { input * 2 }); - - // Test the Task trait implementation - let result = task.run(42).await; - assert_eq!(result, 84); -} - -#[tokio::test] -async fn test_task_fn_with_unit_input() { - // Test task_fn with unit input - let task = task_fn(|_: ()| async { 100 }); - - let result = task.run(()).await; - assert_eq!(result, 100); -} - -#[tokio::test] -async fn test_task_fn_with_tuple_input() { - // Test task_fn with tuple input - let task = task_fn(|(a, b): (i32, i32)| async move { a + b }); - - let result = task.run((10, 20)).await; - assert_eq!(result, 30); -} - -#[tokio::test] -async fn test_task_fn_with_string_input() { - // Test task_fn with String input - let task = task_fn(|input: String| { - let len = input.len(); - async move { len } - }); - - let test_string = String::from("hello"); - let result = task.run(test_string).await; - assert_eq!(result, 5); -} - -#[tokio::test] -async fn test_task_fn_with_complex_output() { - // Test task_fn with complex output type - let task = task_fn(|input: i32| async move { vec![input, input * 2, input * 3] }); - - let result = task.run(5).await; - assert_eq!(result, vec![5, 10, 15]); -} - -#[tokio::test] -async fn test_task_fn_stateful_closure() { - // Test that task_fn can capture state - let multiplier = 3; - let task = task_fn(move |input: i32| async move { input * multiplier }); - - let result = task.run(7).await; - assert_eq!(result, 21); -} - // Test custom Task implementation struct CustomTask { state: i32, @@ -111,55 +48,4 @@ fn assert_task_bounds() { fn test_task_trait_bounds() { // Verify that our test tasks meet the Task trait bounds assert_task_bounds::(); - - // We can't directly test TaskFn because it's a private type and needs a concrete closure type, - // but the fact that task_fn works in the tests above proves it's correct -} - -#[tokio::test] -async fn test_task_fn_single_call() { - // Test that task_fn works (can only call once due to ownership) - let task = task_fn(|x: i32| async move { x + 1 }); - - assert_eq!(task.run(1).await, 2); -} - -#[tokio::test] -async fn test_task_fn_with_result_type() { - // Test task_fn returning Result - let task = task_fn(|x: i32| async move { - if x > 0 { - Ok(x * 2) - } else { - Err("negative input") - } - }); - - assert_eq!(task.run(5).await, Ok(10)); -} - -#[tokio::test] -async fn test_task_fn_with_option_type() { - // Test task_fn returning Option - let task = task_fn(|x: i32| async move { - if x > 0 { - Some(x * 2) - } else { - None - } - }); - - assert_eq!(task.run(5).await, Some(10)); -} - -#[tokio::test] -async fn test_task_fn_explicit_trait_call() { - // Test explicit Task trait method call to cover line 121 - use crate::task::Task; - - let task = task_fn(|x: i32| async move { x * 3 }); - - // Explicitly call the Task trait's run method - let result = Task::run(task, 10).await; - assert_eq!(result, 30); } diff --git a/src/types/tests.rs b/src/types/tests.rs index 4471371..de05806 100644 --- a/src/types/tests.rs +++ b/src/types/tests.rs @@ -172,3 +172,158 @@ fn test_task_builder_ref_from_conversion() { let handle: TaskHandle = (&builder).into(); assert_eq!(handle.id, builder_id); } + +// ============================================================================ +// Additional TaskBuilder conversion coverage tests +// Port from commit sq (sqpzloqrlykp) - src/types/coverage_tests.rs +// ============================================================================ + +#[tokio::test] +async fn test_taskbuilder_conversion_with_multiple_tasks() { + // Test conversion with multiple unit-type tasks to ensure IDs are preserved + use crate::runner::DagRunner; + + struct FirstTask; + #[crate::task] + impl FirstTask { + async fn run(&self) -> String { + "first".to_string() + } + } + + struct SecondTask; + #[crate::task] + impl SecondTask { + async fn run(&self) -> i32 { + 100 + } + } + + struct ThirdTask; + #[crate::task] + impl ThirdTask { + async fn run(&self) -> bool { + true + } + } + + let dag = DagRunner::new(); + + // Create builders for all tasks + let first_builder = dag.add_task(FirstTask); + let second_builder = dag.add_task(SecondTask); + let third_builder = dag.add_task(ThirdTask); + + // Save IDs before conversion + let first_id = first_builder.id; + let second_id = second_builder.id; + let third_id = third_builder.id; + + // Convert each builder to handle explicitly + let first_handle: TaskHandle = first_builder.into(); + let second_handle: TaskHandle = second_builder.into(); + let third_handle: TaskHandle = third_builder.into(); + + // Verify IDs are preserved through conversion + assert_eq!(first_handle.id, first_id); + assert_eq!(second_handle.id, second_id); + assert_eq!(third_handle.id, third_id); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + // Verify all outputs are correct + assert_eq!(dag.get(first_handle).unwrap(), "first"); + assert_eq!(dag.get(second_handle).unwrap(), 100); + assert!(dag.get(third_handle).unwrap()); +} + +#[tokio::test] +async fn test_taskbuilder_conversion_with_stateful_task() { + // Test that stateful tasks (with fields) can have unit input and convert correctly + use crate::runner::DagRunner; + + struct StatefulTask { + value: i32, + } + + impl StatefulTask { + fn new(value: i32) -> Self { + Self { value } + } + } + + #[crate::task] + impl StatefulTask { + async fn run(&self) -> i32 { + self.value * 2 + } + } + + let dag = DagRunner::new(); + + // Create task with initial state + let builder = dag.add_task(StatefulTask::new(21)); + let builder_id = builder.id; + + // Explicit conversion - ensures From works for stateful tasks + let handle: TaskHandle = builder.into(); + assert_eq!(handle.id, builder_id); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + assert_eq!(dag.get(handle).unwrap(), 42); +} + +#[tokio::test] +async fn test_taskbuilder_conversion_with_complex_output_types() { + // Test conversion works with complex output types (Vec, Option, etc.) + use crate::runner::DagRunner; + + struct VecTask; + #[crate::task] + impl VecTask { + async fn run(&self) -> Vec { + vec![1, 2, 3] + } + } + + struct OptionTask; + #[crate::task] + impl OptionTask { + async fn run(&self) -> Option { + Some("value".to_string()) + } + } + + let dag = DagRunner::new(); + + let vec_builder = dag.add_task(VecTask); + let option_builder = dag.add_task(OptionTask); + + let vec_id = vec_builder.id; + let option_id = option_builder.id; + + // Convert to handles with complex types + let vec_handle: TaskHandle> = vec_builder.into(); + let option_handle: TaskHandle> = option_builder.into(); + + assert_eq!(vec_handle.id, vec_id); + assert_eq!(option_handle.id, option_id); + + dag.run(|fut| { + tokio::spawn(fut); + }) + .await + .unwrap(); + + assert_eq!(dag.get(vec_handle).unwrap(), vec![1, 2, 3]); + assert_eq!(dag.get(option_handle).unwrap(), Some("value".to_string())); +}