refactor: consolidate some tests/examples/benchmarks
This commit is contained in:
1 parent
49be497964
commit
b8204c4ed9
83 files changed
+569
-10068
No files matched your search
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user