refactor: consolidate some tests/examples/benchmarks

This commit is contained in:
TechnoPorg committed 2026-02-12 23:31:19 +01:00
1 parent 49be497964
commit b8204c4ed9
83 files changed
+569 -10068

No files matched your search

+1
View File
@@ -121,6 +121,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
/// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap();
/// # };
/// ```
#[allow(private_bounds)]
pub fn depends_on<D>(self, deps: D) -> TaskHandle<Tk::Output>
where
D: DepsTuple<Tk::Input>,
-12
View File
@@ -57,18 +57,6 @@ fn test_task_builder_from_conversion() {
assert_eq!(handle.id.0, 0); // First task should have ID 0
}
#[test]
fn test_task_builder_depends_on_returns_handle() {
init_tracing();
let dag = DagRunner::new();
let source = dag.add_task(TestTask { value: 10 });
let dependent = dag.add_task(TestTaskWithInput).depends_on(source);
// depends_on should return a TaskHandle
let _handle: TaskHandle<_> = dependent;
}
#[test]
fn test_task_builder_chain() {
let dag = DagRunner::new();
-82
View File
@@ -1,82 +0,0 @@
//! # Compile-Time Cycle Prevention
//!
//! This module documents how the type system prevents cycles in the DAG.
//! **Cycles are impossible via the public API** due to the type-state pattern.
//!
//! ## Why Cycles Can't Happen
//!
//! 1. [`crate::builder::TaskBuilder::depends_on`] **consumes** the builder (takes `self` by value)
//! 2. It returns a [`crate::types::TaskHandle`] - an immutable, opaque reference
//! 3. [`crate::types::TaskHandle`] has **no methods** to add or modify dependencies
//! 4. You can only wire dependencies **once** per task
//!
//! ## The Catch-22
//!
//! To create a cycle A→B→A, you would need:
//! - Task A to depend on Task B's handle (requires B finalized)
//! - Task B to depend on Task A's handle (requires A finalized)
//!
//! But you can't finalize both before wiring them together!
//!
//! ## Proof by Compilation Failure
//!
//! The following examples demonstrate that cycles fail to compile:
//!
//! ### Proof 1: Self-Loop Prevention via Move Semantics
//!
//! ```compile_fail
//! use dagx::*;
//!
//! struct TaskA;
//! #[dagx::task]
//! impl TaskA {
//! async fn run(&self, input: &i32) -> i32 {
//! input + 1
//! }
//! }
//!
//! async fn self_loop() {
//! let dag = DagRunner::new();
//! let a_builder = dag.add_task(TaskA);
//!
//! // ERROR: a_builder moved in the call to depends_on
//! // Can't use 'a_builder' as both the builder and the dependency
//! let a = a_builder.depends_on(&a_builder);
//! }
//! ```
//!
//! ### Proof 2: TaskHandle is Immutable
//!
//! ```compile_fail,E0599
//! use dagx::*;
//!
//! struct NoInput;
//! #[dagx::task]
//! impl NoInput {
//! async fn run(&self) -> i32 { 42 }
//! }
//!
//! async fn unit_cycle() {
//! let dag = DagRunner::new();
//! let a_builder = dag.add_task(NoInput);
//! let b_builder = dag.add_task(NoInput);
//!
//! // We can convert to handles (unit input = no dependencies required)
//! let a_handle: TaskHandle<i32> = a_builder.into();
//! let b_handle: TaskHandle<i32> = b_builder.into();
//!
//! // But TaskHandle has no methods to add dependencies!
//! // ERROR: no method named `depends_on` found for type `TaskHandle<i32>`
//! let _ = a_handle.depends_on(&b_handle);
//! }
//! ```
//!
//! ## Implications
//!
//! Because cycles are impossible at compile-time:
//! - No runtime cycle detection is needed
//! - Topological sort will always succeed
//! - The DAG structure is guaranteed by the type system
//!
//! This is an example of **zero-cost abstraction** - the type system
//! provides safety guarantees without runtime overhead.
-11
View File
@@ -17,17 +17,6 @@ impl TestTask {
}
}
struct TestTaskWithInput;
#[crate::task]
impl TestTaskWithInput {
#[allow(dead_code)]
#[cfg(not(tarpaulin_include))]
async fn run(input: &i32) -> i32 {
input * 2
}
}
#[test]
fn test_deps_tuple_unit() {
// Test lines 20-22 in deps.rs - unit type implementation
+1
View File
@@ -4,6 +4,7 @@
/// Errors that can occur during DAG construction and execution
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DagError {
/// Invalid dependency: task does not exist
InvalidDependency { task_id: u32 },
+1 -50
View File
@@ -1,6 +1,6 @@
//! Unit tests for error module
use crate::error::{DagError, DagResult};
use crate::error::DagError;
#[test]
fn test_dag_error_display_invalid_dependency() {
@@ -74,52 +74,3 @@ fn test_dag_error_std_error_impl() {
let _ = err_ref.to_string();
assert!(err_ref.source().is_none()); // DagError doesn't chain errors
}
#[test]
fn test_dag_error_equality() {
let err1 = DagError::InvalidDependency { task_id: 42 };
let err2 = DagError::InvalidDependency { task_id: 42 };
let err3 = DagError::InvalidDependency { task_id: 99 };
assert_eq!(err1, err2);
assert_ne!(err1, err3);
}
#[test]
fn test_dag_error_clone() {
let err = DagError::TaskPanicked {
task_id: 10,
panic_message: "test panic".to_string(),
};
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn test_dag_error_debug() {
let err = DagError::TypeMismatch {
expected: "i32",
found: "bool",
};
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("TypeMismatch"));
assert!(debug_str.contains("expected"));
assert!(debug_str.contains("found"));
}
#[test]
fn test_dag_result_type_alias() {
// Test that DagResult works as expected
fn returns_dag_result() -> DagResult<i32> {
Ok(42)
}
fn returns_dag_error() -> DagResult<String> {
Err(DagError::ResultNotFound { task_id: 1 })
}
assert!(returns_dag_result().is_ok());
assert!(returns_dag_error().is_err());
}
-1
View File
@@ -40,7 +40,6 @@ use crate::TaskInput;
///
/// Type erasure occurs only at the ExecutableNode trait boundary - by the time
/// we're in ExtractInput, we know the concrete type and can safely downcast.
#[allow(dead_code)]
pub trait ExtractInput: Sized + Clone {
type Input: Send;
type Retv<'input>;
+2 -10
View File
@@ -6,7 +6,7 @@
//! # Features
//!
//! - **Compile-time cycle prevention**: The type system makes cycles **impossible**—no runtime
//! cycle detection needed! See [`cycle_prevention`] module for detailed explanation and proof.
//! cycle detection needed! See [the repository documentation](https://github.com/swaits/dagx/blob/main/docs/CYCLE_PREVENTION.md) for a detailed explanation.
//! - **Compile-time type safety**: Dependencies are validated at compile time through the type
//! system. The public API is fully type-safe with no runtime type errors. Internal execution
//! uses type erasure for heterogeneous task storage, but this is never exposed to users.
@@ -482,8 +482,7 @@
//! ```
//!
//! **No trait implementations needed!** Works with nested structs, collections, enums, and any
//! other custom type. See [`examples/custom_types.rs`](https://github.com/swaits/dagx/blob/main/examples/custom_types.rs)
//! for a complete example with complex nested types.
//! other custom type.
//!
//! # Runtime Agnostic
//!
@@ -764,18 +763,11 @@
//!
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
//! Rust async libraries.
//!
//! See [`examples/tracing_example.rs`](https://github.com/swaits/dagx/blob/main/examples/tracing_example.rs)
//! for a complete working example.
//!
#![allow(private_bounds, private_interfaces)]
extern crate self as dagx;
// Module declarations
mod builder;
pub mod cycle_prevention;
mod deps;
mod error;
mod extract;
+1 -1
View File
@@ -68,7 +68,7 @@ async fn test_execute_with_deps_sink_node() {
#[tokio::test]
async fn test_execute_with_deps_non_sink_node() {
// Test that non-sink nodes send output via channels
// Test that non-sink nodes return their output
let node = TypedNode::new(TestTask { value: 42 });
// Execute with senders (non-sink node)
-7
View File
@@ -277,11 +277,6 @@ impl DagRunner {
/// - Returns `DagError::ConcurrentExecution` if the DAG is already executing
/// - Returns `DagError::TaskPanicked` if any task panics during execution
///
/// # Note on Cycles
///
/// Cycles are impossible via the public API due to compile-time prevention.
/// See the [`crate::cycle_prevention`] module for details.
///
/// # Examples
///
/// ```
@@ -674,13 +669,11 @@ impl DagRunner {
}
if !current_layer.is_empty() {
current_layer.sort(); // Deterministic ordering
layers.push(current_layer);
}
}
// Cycle detection removed: cycles are impossible via the public API.
// See src/cycle_prevention.rs for proof that the type system prevents cycles.
//
// The type-state pattern enforces acyclic structure at compile time through:
// 1. TaskBuilder::depends_on() consumes the builder (move semantics)
+1 -20
View File
@@ -168,23 +168,6 @@ async fn test_concurrent_run_protection() {
}
}
#[test]
fn test_add_task_increments_id() {
let dag = DagRunner::new();
let handle1 = dag.add_task(TestTask { value: 1 });
let handle2 = dag.add_task(TestTask { value: 2 });
let handle3 = dag.add_task(TestTask { value: 3 });
// Node IDs should be sequential
assert_eq!(handle1.id.0, 0);
assert_eq!(handle2.id.0, 1);
assert_eq!(handle3.id.0, 2);
// Check that nodes were actually added
assert_eq!(dag.nodes.lock().len(), 3);
}
#[tokio::test]
async fn test_invalid_node_id_in_get() {
init_tracing();
@@ -255,7 +238,5 @@ async fn test_task_panic_in_multi_task_layer() {
.await;
// The run should fail due to the panic
// Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime
// This test mainly ensures we don't crash
assert!(result.is_ok() || result.is_err());
assert!(result.is_err());
}
+4 -183
View File
@@ -1,99 +1,9 @@
//! Unit tests for task module
use crate::task::{task_fn, Task, TaskInput};
use crate::task::{Task, TaskInput};
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
// 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::<i32, _, _>(|input: &i32| input * 2);
// Test the Task trait implementation
let result = task
.run(TaskInput::new(
[Arc::new(42) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.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::<(), _, _>(|_: ()| 100);
let result = task
.run(TaskInput::new(
[Arc::new(()) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.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::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b);
let result = task
.run(TaskInput::new(
[
Arc::new(10) as Arc<dyn Any + Send + Sync + 'static>,
Arc::new(20) as Arc<dyn Any + Send + Sync + 'static>,
]
.iter(),
))
.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::<String, _, _>(|input: &String| input.len());
let test_string = String::from("hello");
let result = task
.run(TaskInput::new(
[Arc::new(test_string) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.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::<i32, _, _>(|&input: &i32| vec![input, input * 2, input * 3]);
let result = task
.run(TaskInput::new(
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.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::<i32, _, _>(move |input: &i32| input * multiplier);
let result = task
.run(TaskInput::new(
[Arc::new(7) as Arc<dyn Any + Send + Sync + 'static>].iter(),
))
.await;
assert_eq!(result, 21);
}
// Test custom Task implementation
struct CustomTask {
state: i32,
@@ -103,15 +13,9 @@ impl Task for CustomTask {
type Input = (i32,);
type Output = i32;
#[allow(refining_impl_trait)]
fn run(
mut self,
input: TaskInput<'_, Self::Input>,
) -> Pin<Box<dyn Future<Output = Self::Output> + Send + '_>> {
Box::pin(async move {
self.state += input.next().0;
self.state
})
async fn run(mut self, input: TaskInput<'_, Self::Input>) -> i32 {
self.state += input.next().0;
self.state
}
}
@@ -129,86 +33,3 @@ async fn test_custom_task_implementation() {
// Can't call run again since task was consumed
// This demonstrates the ownership model
}
// Test that Task trait bounds are correct
fn assert_task_bounds<T: Task>() {
// This function just checks that T implements Task with the right bounds
fn assert_send<T: Send>() {}
// These should compile if bounds are correct
assert_send::<T>();
assert_send::<T::Input>();
assert_send::<T::Output>();
}
#[test]
fn test_task_trait_bounds() {
// Verify that our test tasks meet the Task trait bounds
assert_task_bounds::<CustomTask>();
// 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::<i32, _, _>(|x: &i32| x + 1);
assert_eq!(
task.run(TaskInput::new(
[Arc::new(1) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
2
);
}
#[tokio::test]
async fn test_task_fn_with_result_type() {
// Test task_fn returning Result
let task = task_fn::<i32, _, _>(|&x: &i32| {
if x > 0 {
Ok(x * 2)
} else {
Err("negative input")
}
});
assert_eq!(
task.run(TaskInput::new(
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Ok(10)
);
}
#[tokio::test]
async fn test_task_fn_with_option_type() {
// Test task_fn returning Option
let task = task_fn::<i32, _, _>(|&x: &i32| if x > 0 { Some(x * 2) } else { None });
assert_eq!(
task.run(TaskInput::new(
[Arc::new(5) as Arc<dyn Any + Send + Sync + 'static>].iter()
))
.await,
Some(10)
);
}
#[tokio::test]
async fn test_task_fn_explicit_trait_call() {
// Test explicit Task trait method call to cover line 121
let task = task_fn::<i32, _, _>(|x: &i32| x * 3);
// Explicitly call the Task trait's run method
let result = Task::run(
task,
TaskInput::new([Arc::new(10) as Arc<dyn Any + Send + Sync + 'static>].iter()),
)
.await;
assert_eq!(result, 30);
}
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::builder::TaskBuilder;
use crate::task::Task;
/// Opaque node identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct NodeId(pub u32);
/// Opaque, typed token for a node's output.
-201
View File
@@ -4,90 +4,6 @@
use crate::{task, DagRunner, TaskHandle};
#[tokio::test]
async fn test_unit_type_taskbuilder_to_handle_conversion() {
// Test that explicitly uses the From<TaskBuilder> conversion for unit-type tasks
// This covers line 71 in types.rs
// Unit struct (no fields) - its Input type is ()
struct UnitTask;
#[task]
impl UnitTask {
async fn run(&self) -> i32 {
42
}
}
let dag = DagRunner::new();
// This creates a TaskBuilder<UnitTask, Pending>
// Since UnitTask has Input = (), it can be converted to TaskHandle
let builder = dag.add_task(UnitTask);
// Explicitly convert to TaskHandle using From implementation
// This triggers the From<TaskBuilder> implementation at line 69-74,
// specifically covering line 71
let handle: TaskHandle<_> = builder.into();
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
.await
.unwrap();
// Verify the task ran and we can get its output
assert_eq!(dag.get(handle).unwrap(), 42);
}
#[tokio::test]
async fn test_unit_type_conversion_with_multiple_tasks() {
// Test conversion with multiple unit-type tasks
struct FirstTask;
#[task]
impl FirstTask {
async fn run(&self) -> String {
"first".to_string()
}
}
struct SecondTask;
#[task]
impl SecondTask {
async fn run(&self) -> i32 {
100
}
}
struct ThirdTask;
#[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);
// Convert each builder to handle explicitly
let first_handle: TaskHandle<String> = first_builder.into();
let second_handle: TaskHandle<_> = second_builder.into();
let third_handle: TaskHandle<bool> = third_builder.into();
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
.await
.unwrap();
// Verify all outputs
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_unit_struct_with_state() {
// Even structs with fields can have unit input type
@@ -124,75 +40,9 @@ async fn test_unit_struct_with_state() {
assert_eq!(dag.get(handle).unwrap(), 42);
}
#[tokio::test]
async fn test_taskbuilder_conversion_assignment() {
// Test the conversion through assignment (different from .into())
struct SimpleTask;
#[task]
impl SimpleTask {
async fn run(&self) -> Vec<i32> {
vec![1, 2, 3]
}
}
let dag = DagRunner::new();
let builder = dag.add_task(SimpleTask);
// Conversion through type annotation and assignment
// This also uses From<TaskBuilder> implementation
let handle: TaskHandle<Vec<i32>> = builder.into();
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
.await
.unwrap();
let result = dag.get(handle).unwrap();
assert_eq!(result, vec![1, 2, 3]);
}
#[tokio::test]
async fn test_mixed_unit_and_dependent_tasks() {
// Test that unit-type tasks work alongside tasks with dependencies
struct Source;
#[task]
impl Source {
async fn run(&self) -> i32 {
10
}
}
struct Doubler;
#[task]
impl Doubler {
async fn run(&self, input: &i32) -> i32 {
input * 2
}
}
let dag = DagRunner::new();
// Source is unit-type, can be converted
let source_builder = dag.add_task(Source);
let source_handle: TaskHandle<_> = source_builder.into(); // Covers line 71
// Doubler needs dependencies, so we use it differently
let doubler = dag.add_task(Doubler).depends_on(source_handle);
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
.await
.unwrap();
assert_eq!(dag.get(source_handle).unwrap(), 10);
assert_eq!(dag.get(doubler).unwrap(), 20);
}
#[test]
fn test_taskbuilder_conversion_without_running() {
// Test that conversion works even without running the DAG
// This ensures the conversion is purely a type-level operation
struct TestTask;
#[task]
impl TestTask {
@@ -211,54 +61,3 @@ fn test_taskbuilder_conversion_without_running() {
// Verify the ID was transferred correctly
assert_eq!(handle.id, builder_id);
}
#[test]
fn test_multiple_conversions_preserve_ids() {
// Ensure that multiple conversions maintain correct IDs
struct Task1;
#[task]
impl Task1 {
async fn run(&self) -> i32 {
1
}
}
struct Task2;
#[task]
impl Task2 {
async fn run(&self) -> i32 {
2
}
}
struct Task3;
#[task]
impl Task3 {
async fn run(&self) -> i32 {
3
}
}
let dag = DagRunner::new();
let builder1 = dag.add_task(Task1);
let builder2 = dag.add_task(Task2);
let builder3 = dag.add_task(Task3);
let id1 = builder1.id;
let id2 = builder2.id;
let id3 = builder3.id;
// Convert all builders
let handle1: TaskHandle<_> = builder1.into();
let handle2: TaskHandle<_> = builder2.into();
let handle3: TaskHandle<_> = builder3.into();
// Verify IDs are preserved and sequential
assert_eq!(handle1.id, id1);
assert_eq!(handle2.id, id2);
assert_eq!(handle3.id, id3);
assert_eq!(handle1.id.0 + 1, handle2.id.0);
assert_eq!(handle2.id.0 + 1, handle3.id.0);
}
+5 -104
View File
@@ -4,54 +4,19 @@ use crate::types::{NodeId, TaskHandle};
use std::marker::PhantomData;
#[test]
fn test_node_id_operations() {
let id1 = NodeId(1);
let id2 = NodeId(2);
let id3 = NodeId(1);
// Test equality
assert_eq!(id1, id3);
assert_ne!(id1, id2);
// Test ordering
assert!(id1 < id2);
assert!(id2 > id1);
// Test Debug
let debug_str = format!("{:?}", id1);
assert!(debug_str.contains("NodeId"));
assert!(debug_str.contains("1"));
// Test Clone
let cloned = id1;
assert_eq!(id1, cloned);
// Test Copy
let copied = id1;
assert_eq!(id1, copied);
// Test Hash (compile-time check that it implements Hash)
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(id1);
set.insert(id2);
assert_eq!(set.len(), 2);
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_task_handle_clone() {
let handle: TaskHandle<(i32,)> = TaskHandle {
id: NodeId(42),
_phantom: PhantomData,
};
// Test Clone implementation (lines 44-46 in types.rs)
let cloned = handle;
// Test Clone implementation
let cloned = handle.clone();
assert_eq!(handle.id.0, cloned.id.0);
// Test that clone is actually a copy (same memory semantics)
let handle2 = handle;
let handle3 = handle2;
let handle2 = handle.clone();
let handle3 = handle2.clone();
assert_eq!(handle3.id.0, 42);
}
@@ -69,70 +34,6 @@ fn test_task_handle_copy() {
assert_eq!(copied2.id.0, 99);
}
#[test]
fn test_task_handle_with_different_types() {
// Test that TaskHandle works with various types
let int_handle: TaskHandle<i32> = TaskHandle {
id: NodeId(1),
_phantom: PhantomData,
};
let string_handle: TaskHandle<String> = TaskHandle {
id: NodeId(2),
_phantom: PhantomData,
};
let vec_handle: TaskHandle<Vec<u8>> = TaskHandle {
id: NodeId(3),
_phantom: PhantomData,
};
// Clone and copy should work for all
let _ = int_handle;
let _ = string_handle;
let _ = vec_handle;
let _ = int_handle;
let _ = string_handle;
let _ = vec_handle;
}
#[test]
fn test_task_handle_explicit_clone() {
// Test explicit .clone() call to cover line 47-48
let handle: TaskHandle<i32> = TaskHandle {
id: NodeId(42),
_phantom: PhantomData,
};
#[allow(clippy::clone_on_copy)]
let cloned = handle.clone();
assert_eq!(handle.id.0, cloned.id.0);
assert_eq!(cloned.id.0, 42);
}
#[test]
fn test_task_builder_from_conversion() {
use crate::runner::DagRunner;
// Test From<TaskBuilder> for TaskHandle (line 69-74)
struct SimpleTask;
#[crate::task]
impl SimpleTask {
async fn run(&self) -> i32 {
42
}
}
let dag = DagRunner::new();
let builder = dag.add_task(SimpleTask);
let builder_id = builder.id;
// This conversion uses the From implementation
let handle: TaskHandle<_> = builder.into();
assert_eq!(handle.id, builder_id);
}
#[test]
fn test_task_handle_ref_from_conversion() {
// Test From<&TaskHandle<T>> for TaskHandle<T> (line 90-93)