Update tests, examples, benches

This commit is contained in:
TechnoPorg committed 2026-02-11 22:01:50 +01:00
1 parent 0137296fb4
commit 86787b3a0d
71 files changed
+1791 -3547

No files matched your search

+16 -16
View File
@@ -8,7 +8,7 @@ use futures::FutureExt;
async fn test_result_not_found_error() {
let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Try to get result before running
let result = dag.get(t1);
@@ -27,11 +27,11 @@ async fn test_nested_result_error_handling() {
let dag = DagRunner::new();
// Task that returns a Result
let task1 = dag.add_task(task_fn(|_: ()| async {
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, String>::Err("Internal error".to_string())
}));
let task2 = dag.add_task(task_fn(|_: ()| async { Result::<i32, String>::Ok(42) }));
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::<i32, String>::Ok(42)));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -46,7 +46,7 @@ async fn test_nested_result_error_handling() {
async fn test_option_none_not_error() {
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async { None::<i32> }));
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::<i32>));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
.await
@@ -66,11 +66,11 @@ async fn test_custom_error_types() {
let dag = DagRunner::new();
let task1 = dag.add_task(task_fn(|_: ()| async {
let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, CustomError>::Err(CustomError::ValidationError("Invalid input".to_string()))
}));
let task2 = dag.add_task(task_fn(|_: ()| async {
let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, CustomError>::Err(CustomError::ProcessingError {
code: 500,
details: "Processing failed".to_string(),
@@ -82,13 +82,13 @@ async fn test_custom_error_types() {
.unwrap();
// Check custom errors are preserved
match dag.get(task1).unwrap() {
match dag.get(task1).unwrap().as_ref() {
Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
_ => panic!("Wrong error type"),
}
match dag.get(task2).unwrap() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500),
match dag.get(task2).unwrap().as_ref() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500),
_ => panic!("Wrong error type"),
}
}
@@ -97,13 +97,13 @@ async fn test_custom_error_types() {
async fn test_error_in_tuple_dependency() {
let dag = DagRunner::new();
let ok_task = dag.add_task(task_fn(|_: ()| async { 10 }));
let err_task = dag.add_task(task_fn(|_: ()| async {
let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error in dependency");
}));
let dependent = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((ok_task, err_task));
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -116,11 +116,11 @@ async fn test_error_in_tuple_dependency() {
async fn test_string_panic_vs_structured_panic() {
let dag = DagRunner::new();
let string_panic = dag.add_task(task_fn(|_: ()| async {
let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("String panic message");
}));
let formatted_panic = dag.add_task(task_fn(|_: ()| async {
let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Formatted panic: value={}, code={}", 42, "ERROR");
}));
@@ -136,7 +136,7 @@ async fn test_anyhow_like_error_chain() {
// Test error chains with simple string-based errors
let dag = DagRunner::new();
let task = dag.add_task(task_fn(|_: ()| async {
let task = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Simulate an error chain with nested Results
let _inner: Result<i32, String> = Err("Inner error".to_string());
let outer: Result<Result<i32, String>, String> = Err("Outer error".to_string());
@@ -152,7 +152,7 @@ async fn test_anyhow_like_error_chain() {
let result = dag.get(task).unwrap();
assert!(result.is_err());
if let Err(e) = result {
if let Err(e) = result.as_ref() {
assert_eq!(e, "Outer error");
}
}
+92 -86
View File
@@ -1,26 +1,28 @@
//! Tests for error propagation through the DAG
use crate::common::task_fn;
use dagx::{DagRunner, TaskHandle};
use dagx::{task, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn test_linear_error_propagation() {
let dag = DagRunner::new();
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
let t2 = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| -> i32 {
panic!("Error at t2 with input {}", x);
}))
.depends_on(t1);
let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(t2);
let t4 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(t3);
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -46,20 +48,20 @@ async fn test_diamond_error_propagation() {
// \ /
// sink
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x / 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x / 2))
.depends_on(source);
let right = dag
.add_task(task_fn(|x: i32| async move {
.add_task(task_fn::<i32, _, _>(|&x: &i32| {
panic!("Right path fails with {}", x);
}))
.depends_on(source);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r))
.depends_on((&left, &right));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -77,40 +79,34 @@ async fn test_error_stops_at_boundary() {
// Branch with error
let error_source: TaskHandle<_> = dag
.add_task(task_fn({
.add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone();
move |_: ()| {
move |_: ()| -> i32 {
let tracker = tracker.clone();
async move {
tracker.fetch_add(1, Ordering::SeqCst);
panic!("Error source");
}
tracker.fetch_add(1, Ordering::SeqCst);
panic!("Error source");
}
}))
.into();
let error_dependent = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let tracker = execution_tracker.clone();
move |_: i32| {
move |_: &i32| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
42
}
tracker.fetch_add(100, Ordering::SeqCst); // Should not execute
42
}
}))
.depends_on(error_source);
// Independent branch
let independent = dag.add_task(task_fn({
let independent = dag.add_task(task_fn::<(), _, _>({
let tracker = execution_tracker.clone();
move |_: ()| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(10, Ordering::SeqCst);
10
}
tracker.fetch_add(10, Ordering::SeqCst);
10
}
}));
@@ -133,22 +129,24 @@ async fn test_multiple_error_sources_convergence() {
// Multiple error sources
let error1 = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 1");
}))
.into();
let error2 = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Error source 2");
}))
.into();
let success = dag.add_task(task_fn(|_: ()| async { 42 })).into();
let success = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into();
// Convergence point depends on all three
let convergence = dag
.add_task(task_fn(|(_, _, _): (i32, i32, i32)| async { 100 }))
.add_task(task_fn::<(i32, i32, i32), _, _>(
|(_, _, _): (&i32, &i32, &i32)| 100,
))
.depends_on((&error1, &error2, &success));
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -169,7 +167,7 @@ async fn test_error_in_wide_fanout() {
let dag = DagRunner::new();
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| -> i32 {
panic!("Source fails");
}))
.into();
@@ -177,7 +175,7 @@ async fn test_error_in_wide_fanout() {
// Create wide fanout from failing source
let dependents: Vec<_> = (0..20)
.map(|i| {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source)
})
.collect();
@@ -199,24 +197,26 @@ async fn test_selective_error_propagation() {
// Source that produces a Result
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
.add_task(task_fn::<(), _, _>(|_: ()| {
Result::<i32, String>::Err("Source error".to_string())
}))
.into();
// Handler that processes the Result
let handler = dag
.add_task(task_fn(|result: Result<i32, String>| async move {
match result {
Ok(val) => val * 2,
Err(_) => -1, // Default value on error
}
}))
.add_task(task_fn::<Result<_, _>, _, _>(
|result: &Result<i32, String>| {
match result {
Ok(val) => val * 2,
Err(_) => -1, // Default value on error
}
},
))
.depends_on(source);
// Further processing
let final_task = dag
.add_task(task_fn(|val: i32| async move {
.add_task(task_fn::<i32, _, _>(|&val: &i32| {
if val < 0 {
"Handled error case".to_string()
} else {
@@ -244,41 +244,49 @@ async fn test_error_propagation_timing() {
let dag = DagRunner::new();
let propagation_order = Arc::new(parking_lot::Mutex::new(Vec::new()));
let t1 = dag.add_task(task_fn({
let order = propagation_order.clone();
move |_: ()| {
let order = order.clone();
async move {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
order.lock().push("t1_complete");
1
}
struct T1Task {
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[task]
impl T1Task {
async fn run(&self) -> i32 {
sleep(Duration::from_millis(10)).await;
self.order.lock().push("t1_complete");
1
}
}));
}
let t1 = dag.add_task(T1Task {
order: propagation_order.clone(),
});
struct T2Task {
order: Arc<parking_lot::Mutex<Vec<&'static str>>>,
}
#[task]
impl T2Task {
async fn run(&self, x: &i32) -> i32 {
self.order.lock().push("t2_start");
sleep(Duration::from_millis(10)).await;
panic!("t2 fails with {x}");
}
}
let t2 = dag
.add_task(task_fn({
let order = propagation_order.clone();
move |x: i32| {
let order = order.clone();
async move {
order.lock().push("t2_start");
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
panic!("t2 fails with {}", x);
}
}
}))
.add_task(T2Task {
order: propagation_order.clone(),
})
.depends_on(t1);
let _t3 = dag
.add_task(task_fn({
.add_task(task_fn::<i32, _, _>({
let order = propagation_order.clone();
move |x: i32| {
move |x: &i32| {
let order = order.clone();
async move {
order.lock().push("t3_should_not_run");
x + 1
}
order.lock().push("t3_should_not_run");
x + 1
}
}))
.depends_on(t2);
@@ -307,53 +315,51 @@ async fn test_partial_branch_failure_propagation() {
// \ | /
// sink
let root: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
// Branch A
let a1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 1))
.depends_on(root);
let a2 = dag
.add_task(task_fn(|x: i32| async move { x + 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 2))
.depends_on(a1);
let a3 = dag
.add_task(task_fn(|x: i32| async move { x + 3 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 3))
.depends_on(a2);
// Branch B (with error) - use Result instead of panic
let b1 = dag
.add_task(task_fn(|x: i32| async move { Ok(x * 2) }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| Ok::<i32, &str>(x * 2)))
.depends_on(root);
let b2 = dag
.add_task(task_fn(|_x: Result<i32, &str>| async move {
Err("Branch B fails at b2")
.add_task(task_fn::<Result<_, _>, _, _>(|_x: &Result<i32, &str>| {
Err::<i32, &str>("Branch B fails at b2")
}))
.depends_on(b1);
let b3 = dag
.add_task(task_fn(
|x: Result<i32, &str>| async move { x.map(|v| v * 3) },
))
.add_task(task_fn::<Result<_, _>, _, _>(|x: &Result<i32, &str>| {
x.map(|v| v * 3)
}))
.depends_on(b2);
// Branch C
let c1 = dag
.add_task(task_fn(|x: i32| async move { x - 1 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 1))
.depends_on(root);
let c2 = dag
.add_task(task_fn(|x: i32| async move { x - 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 2))
.depends_on(c1);
let c3 = dag
.add_task(task_fn(|x: i32| async move { x - 3 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x - 3))
.depends_on(c2);
// Sink depends on all branches - handle Result from B
let sink = dag
.add_task(task_fn(
|(a, b, c): (i32, Result<i32, &str>, i32)| async move {
match b {
Ok(b_val) => Ok(a + b_val + c),
Err(e) => Err(e),
}
.add_task(task_fn::<(i32, Result<_, _>, _), _, _>(
|(a, b, c): (&i32, &Result<i32, &str>, &i32)| match b {
Ok(b_val) => Ok(a + b_val + c),
Err(e) => Err(*e),
},
))
.depends_on((&a3, &b3, &c3));
+46 -54
View File
@@ -11,17 +11,17 @@ async fn test_fallback_on_error() -> DagResult<()> {
let dag = DagRunner::new();
// Primary task that fails
let primary = dag.add_task(task_fn(|_: ()| async {
let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| {
panic!("Primary task failed");
#[allow(unreachable_code)]
42
}));
// Fallback task
let fallback = dag.add_task(task_fn(|_: ()| async { 100 }));
let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
// Selector that uses fallback if primary fails
let _selector = dag.add_task(task_fn(|_: ()| async {}));
let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {}));
// Run DAG
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -41,22 +41,20 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
let attempt_counter = Arc::new(AtomicUsize::new(0));
// Task that would fail on first attempts (simulated)
let task = dag.add_task(task_fn({
let task = dag.add_task(task_fn::<(), _, _>({
let counter = attempt_counter.clone();
move |_: ()| {
let counter = counter.clone();
async move {
let attempt = counter.fetch_add(1, Ordering::SeqCst);
let attempt = counter.fetch_add(1, Ordering::SeqCst);
// In real retry, this would be retried
// Here we simulate by not panicking
if attempt == 0 {
// Would normally panic here
return Err::<i32, &str>("Simulated failure");
}
Ok(42)
// In real retry, this would be retried
// Here we simulate by not panicking
if attempt == 0 {
// Would normally panic here
return Err::<i32, &str>("Simulated failure");
}
Ok(42)
}
}));
@@ -79,26 +77,24 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
.map(|i| {
let failures = failure_count.clone();
let circuit = circuit_open.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let failures = failures.clone();
let circuit = circuit.clone();
async move {
// Check if circuit is open
if circuit.load(Ordering::SeqCst) {
return Err("Circuit breaker open");
}
// Simulate some failures
if i < 3 {
let count = failures.fetch_add(1, Ordering::SeqCst);
if count >= 2 {
circuit.store(true, Ordering::SeqCst);
}
return Err("Task failed");
}
Ok(i)
// Check if circuit is open
if circuit.load(Ordering::SeqCst) {
return Err("Circuit breaker open");
}
// Simulate some failures
if i < 3 {
let count = failures.fetch_add(1, Ordering::SeqCst);
if count >= 2 {
circuit.store(true, Ordering::SeqCst);
}
return Err("Task failed");
}
Ok(i)
}))
})
.collect();
@@ -120,7 +116,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
// Some tasks that might fail
let _tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
if i % 3 == 0 {
panic!("Task {} failed", i);
}
@@ -130,7 +126,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
.collect();
// Aggregator that handles failures gracefully
let aggregator = dag.add_task(task_fn(|_: ()| async {
let aggregator = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// In real scenario, would check task results and use defaults
vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks
}));
@@ -152,15 +148,13 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
let mut tasks: Vec<_> = (0..10)
.map(|i| {
let errors = errors.clone();
dag.add_task(task_fn(move |_: ()| {
dag.add_task(task_fn::<(), _, _>(move |_: ()| {
let errors = errors.clone();
async move {
if i % 2 == 0 {
errors.lock().push(format!("Error from task {}", i));
return Err(format!("Task {} error", i));
}
Ok(i)
if i % 2 == 0 {
errors.lock().push(format!("Error from task {}", i));
return Err(format!("Task {} error", i));
}
Ok(i)
}))
})
.collect();
@@ -187,16 +181,16 @@ async fn test_graceful_degradation() -> DagResult<()> {
let dag = DagRunner::new();
// Critical task
let critical = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
let critical = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5]));
// Optional enhancement that fails - returns Result
let _enhancement = dag.add_task(task_fn(|_: ()| async {
let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| {
Err::<Vec<i32>, &str>("Enhancement failed")
}));
// Final task that works with what's available
let final_task = dag
.add_task(task_fn(|base: Vec<i32>| async move {
.add_task(task_fn::<Vec<_>, _, _>(|base: &Vec<i32>| {
// Would combine with enhancement if available
// Here just uses base
base.iter().sum::<i32>()
@@ -216,22 +210,22 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
let dag = DagRunner::new();
// Group A: contains an error - use Result
let a1 = dag.add_task(task_fn(|_: ()| async { 10 }));
let a1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10));
let a2 = dag
.add_task(task_fn(|_x: i32| async move {
.add_task(task_fn::<i32, _, _>(|_x: &i32| {
Err::<i32, &str>("Group A error")
}))
.depends_on(a1);
// Group B: should be isolated from Group A's error
let b1 = dag.add_task(task_fn(|_: ()| async { 20 }));
let b1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20));
let b2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x * 2))
.depends_on(b1);
// Group C: depends on B but not A
let c1 = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.add_task(task_fn::<i32, _, _>(|&x: &i32| x + 5))
.depends_on(b2);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -252,20 +246,18 @@ async fn test_compensation_action() -> DagResult<()> {
let compensation_triggered = Arc::new(AtomicBool::new(false));
// Action that fails
let action = dag.add_task(task_fn(|_: ()| async {
let action = dag.add_task(task_fn::<(), _, _>(|_: ()| {
// Perform some action that fails
panic!("Action failed");
}));
// Compensation task (runs independently)
let compensation = dag.add_task(task_fn({
let compensation = dag.add_task(task_fn::<(), _, _>({
let triggered = compensation_triggered.clone();
move |_: ()| {
let triggered = triggered.clone();
async move {
triggered.store(true, Ordering::SeqCst);
"Compensation executed"
}
triggered.store(true, Ordering::SeqCst);
"Compensation executed"
}
}));