Address soundness hole in cycle prevention

This commit is contained in:
TechnoPorg committed 2026-01-29 06:56:17 +01:00
1 parent 4ad88bcd68
commit f82205baf4
80 files changed
+730 -717

No files matched your search

+13 -13
View File
@@ -1,17 +1,17 @@
//! Tests for different error types and error handling
use crate::common::task_fn;
use dagx::{DagError, DagRunner};
use dagx::{DagError, DagRunner, TaskHandle};
use futures::FutureExt;
#[tokio::test]
async fn test_result_not_found_error() {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { 42 }));
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
// Try to get result before running
let result = dag.get(&t1);
let result = dag.get(t1);
assert!(matches!(result, Err(DagError::ResultNotFound { .. })));
// Run and verify it works after
@@ -19,7 +19,7 @@ async fn test_result_not_found_error() {
.await
.unwrap();
assert_eq!(dag.get(&t1).unwrap(), 42);
assert_eq!(dag.get(t1).unwrap(), 42);
}
#[tokio::test]
@@ -38,8 +38,8 @@ async fn test_nested_result_error_handling() {
.unwrap();
// Both tasks complete successfully (they return Results)
assert_eq!(dag.get(&task1).unwrap(), Err("Internal error".to_string()));
assert_eq!(dag.get(&task2).unwrap(), Ok(42));
assert_eq!(dag.get(task1).unwrap(), Err("Internal error".to_string()));
assert_eq!(dag.get(task2).unwrap(), Ok(42));
}
#[tokio::test]
@@ -53,7 +53,7 @@ async fn test_option_none_not_error() {
.unwrap();
// None is a valid result, not an error
assert_eq!(dag.get(&task).unwrap(), None);
assert_eq!(dag.get(task).unwrap(), None);
}
#[tokio::test]
@@ -82,12 +82,12 @@ async fn test_custom_error_types() {
.unwrap();
// Check custom errors are preserved
match dag.get(&task1).unwrap() {
match dag.get(task1).unwrap() {
Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"),
_ => panic!("Wrong error type"),
}
match dag.get(&task2).unwrap() {
match dag.get(task2).unwrap() {
Err(CustomError::ProcessingError { code, .. }) => assert_eq!(code, 500),
_ => panic!("Wrong error type"),
}
@@ -104,7 +104,7 @@ async fn test_error_in_tuple_dependency() {
let dependent = dag
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
.depends_on((&ok_task, &err_task));
.depends_on((ok_task, err_task));
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -127,8 +127,8 @@ async fn test_string_panic_vs_structured_panic() {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// Both should result in errors
assert!(dag.get(&string_panic).is_err());
assert!(dag.get(&formatted_panic).is_err());
assert!(dag.get(string_panic).is_err());
assert!(dag.get(formatted_panic).is_err());
}
#[tokio::test]
@@ -149,7 +149,7 @@ async fn test_anyhow_like_error_chain() {
.unwrap();
// Get the chained error
let result = dag.get(&task).unwrap();
let result = dag.get(task).unwrap();
assert!(result.is_err());
if let Err(e) = result {
+58 -48
View File
@@ -1,7 +1,7 @@
//! Tests for error propagation through the DAG
use crate::common::task_fn;
use dagx::DagRunner;
use dagx::{DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
@@ -10,12 +10,12 @@ use std::sync::Arc;
async fn test_linear_error_propagation() {
let dag = DagRunner::new();
let t1 = dag.add_task(task_fn(|_: ()| async { 1 }));
let t1: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let t2 = dag
.add_task(task_fn(|x: i32| async move {
panic!("Error at t2 with input {}", x);
}))
.depends_on(&t1);
.depends_on(t1);
let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(t2);
@@ -26,7 +26,7 @@ async fn test_linear_error_propagation() {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// t1 succeeds
assert_eq!(dag.get(&t1).unwrap(), 1);
assert_eq!(dag.get(t1).unwrap(), 1);
// t2 fails
assert!(dag.get(t2).is_err());
@@ -46,17 +46,17 @@ async fn test_diamond_error_propagation() {
// \ /
// sink
let source = dag.add_task(task_fn(|_: ()| async { 100 }));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
let left = dag
.add_task(task_fn(|x: i32| async move { x / 2 }))
.depends_on(&source);
.depends_on(source);
let right = dag
.add_task(task_fn(|x: i32| async move {
panic!("Right path fails with {}", x);
}))
.depends_on(&source);
.depends_on(source);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
@@ -64,7 +64,7 @@ async fn test_diamond_error_propagation() {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
assert_eq!(dag.get(&source).unwrap(), 100);
assert_eq!(dag.get(source).unwrap(), 100);
assert_eq!(dag.get(left).unwrap(), 50);
assert!(dag.get(right).is_err());
assert!(dag.get(sink).is_err()); // Fails due to right dependency
@@ -76,16 +76,18 @@ async fn test_error_stops_at_boundary() {
let execution_tracker = Arc::new(AtomicUsize::new(0));
// Branch with error
let error_source = dag.add_task(task_fn({
let tracker = execution_tracker.clone();
move |_: ()| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(1, Ordering::SeqCst);
panic!("Error source");
let error_source: TaskHandle<_> = dag
.add_task(task_fn({
let tracker = execution_tracker.clone();
move |_: ()| {
let tracker = tracker.clone();
async move {
tracker.fetch_add(1, Ordering::SeqCst);
panic!("Error source");
}
}
}
}));
}))
.into();
let error_dependent = dag
.add_task(task_fn({
@@ -98,7 +100,7 @@ async fn test_error_stops_at_boundary() {
}
}
}))
.depends_on(&error_source);
.depends_on(error_source);
// Independent branch
let independent = dag.add_task(task_fn({
@@ -115,11 +117,11 @@ async fn test_error_stops_at_boundary() {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// Error branch: source runs but dependent doesn't
assert!(dag.get(&error_source).is_err());
assert!(dag.get(error_source).is_err());
assert!(dag.get(error_dependent).is_err());
// Independent branch runs
assert_eq!(dag.get(&independent).unwrap(), 10);
assert_eq!(dag.get(independent).unwrap(), 10);
// Check execution: 1 (error_source) + 10 (independent) = 11
assert_eq!(execution_tracker.load(Ordering::SeqCst), 11);
@@ -130,15 +132,19 @@ async fn test_multiple_error_sources_convergence() {
let dag = DagRunner::new();
// Multiple error sources
let error1 = dag.add_task(task_fn(|_: ()| async {
panic!("Error source 1");
}));
let error1 = dag
.add_task(task_fn(|_: ()| async {
panic!("Error source 1");
}))
.into();
let error2 = dag.add_task(task_fn(|_: ()| async {
panic!("Error source 2");
}));
let error2 = dag
.add_task(task_fn(|_: ()| async {
panic!("Error source 2");
}))
.into();
let success = dag.add_task(task_fn(|_: ()| async { 42 }));
let success = dag.add_task(task_fn(|_: ()| async { 42 })).into();
// Convergence point depends on all three
let convergence = dag
@@ -148,11 +154,11 @@ async fn test_multiple_error_sources_convergence() {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// All error sources fail
assert!(dag.get(&error1).is_err());
assert!(dag.get(&error2).is_err());
assert!(dag.get(error1).is_err());
assert!(dag.get(error2).is_err());
// Success task works
assert_eq!(dag.get(&success).unwrap(), 42);
assert_eq!(dag.get(success).unwrap(), 42);
// Convergence fails due to any error dependency
assert!(dag.get(convergence).is_err());
@@ -162,22 +168,24 @@ async fn test_multiple_error_sources_convergence() {
async fn test_error_in_wide_fanout() {
let dag = DagRunner::new();
let source = dag.add_task(task_fn(|_: ()| async {
panic!("Source fails");
}));
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
panic!("Source fails");
}))
.into();
// 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 }))
.depends_on(&source)
.depends_on(source)
})
.collect();
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// Source fails
assert!(dag.get(&source).is_err());
assert!(dag.get(source).is_err());
// All dependents fail
for dependent in &dependents {
@@ -190,9 +198,11 @@ async fn test_selective_error_propagation() {
let dag = DagRunner::new();
// Source that produces a Result
let source = dag.add_task(task_fn(|_: ()| async {
Result::<i32, String>::Err("Source error".to_string())
}));
let source: TaskHandle<_> = dag
.add_task(task_fn(|_: ()| async {
Result::<i32, String>::Err("Source error".to_string())
}))
.into();
// Handler that processes the Result
let handler = dag
@@ -202,7 +212,7 @@ async fn test_selective_error_propagation() {
Err(_) => -1, // Default value on error
}
}))
.depends_on(&source);
.depends_on(source);
// Further processing
let final_task = dag
@@ -220,7 +230,7 @@ async fn test_selective_error_propagation() {
.unwrap();
// Source returns an Err variant (not a panic)
assert_eq!(dag.get(&source).unwrap(), Err("Source error".to_string()));
assert_eq!(dag.get(source).unwrap(), Err("Source error".to_string()));
// Handler processes it
assert_eq!(dag.get(handler).unwrap(), -1);
@@ -258,7 +268,7 @@ async fn test_error_propagation_timing() {
}
}
}))
.depends_on(&t1);
.depends_on(t1);
let _t3 = dag
.add_task(task_fn({
@@ -271,7 +281,7 @@ async fn test_error_propagation_timing() {
}
}
}))
.depends_on(&t2);
.depends_on(t2);
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
@@ -297,12 +307,12 @@ async fn test_partial_branch_failure_propagation() {
// \ | /
// sink
let root = dag.add_task(task_fn(|_: ()| async { 100 }));
let root: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
// Branch A
let a1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&root);
.depends_on(root);
let a2 = dag
.add_task(task_fn(|x: i32| async move { x + 2 }))
.depends_on(a1);
@@ -313,22 +323,22 @@ async fn test_partial_branch_failure_propagation() {
// Branch B (with error) - use Result instead of panic
let b1 = dag
.add_task(task_fn(|x: i32| async move { Ok(x * 2) }))
.depends_on(&root);
.depends_on(root);
let b2 = dag
.add_task(task_fn(|_x: Result<i32, &str>| async move {
Err("Branch B fails at b2")
}))
.depends_on(&b1);
.depends_on(b1);
let b3 = dag
.add_task(task_fn(
|x: Result<i32, &str>| async move { x.map(|v| v * 3) },
))
.depends_on(&b2);
.depends_on(b2);
// Branch C
let c1 = dag
.add_task(task_fn(|x: i32| async move { x - 1 }))
.depends_on(&root);
.depends_on(root);
let c2 = dag
.add_task(task_fn(|x: i32| async move { x - 2 }))
.depends_on(c1);
+17 -13
View File
@@ -27,10 +27,10 @@ async fn test_fallback_on_error() -> DagResult<()> {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// Primary should fail
assert!(dag.get(&primary).is_err());
assert!(dag.get(primary).is_err());
// Fallback should succeed
assert_eq!(dag.get(&fallback)?, 100);
assert_eq!(dag.get(fallback)?, 100);
Ok(())
}
@@ -63,7 +63,7 @@ async fn test_retry_pattern_simulation() -> DagResult<()> {
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Task completes with error on first "attempt"
assert_eq!(dag.get(&task)?, Err("Simulated failure"));
assert_eq!(dag.get(task)?, Err("Simulated failure"));
Ok(())
}
@@ -106,7 +106,7 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> {
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// First few tasks fail and open circuit
for task in &tasks[0..3] {
for task in tasks.into_iter().take(4) {
assert!(dag.get(task)?.is_err());
}
@@ -137,7 +137,7 @@ async fn test_partial_recovery_with_defaults() -> DagResult<()> {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
let result = dag.get(&aggregator)?;
let result = dag.get(aggregator)?;
assert_eq!(result.len(), 10);
Ok(())
@@ -149,7 +149,7 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
let errors = Arc::new(parking_lot::Mutex::new(Vec::new()));
// Tasks that may produce errors
let tasks: Vec<_> = (0..10)
let mut tasks: Vec<_> = (0..10)
.map(|i| {
let errors = errors.clone();
dag.add_task(task_fn(move |_: ()| {
@@ -171,9 +171,13 @@ async fn test_error_accumulation_pattern() -> DagResult<()> {
let error_list = errors.lock().clone();
assert_eq!(error_list.len(), 5); // Even numbered tasks
tasks.truncate(2);
let [first, second] = <[_; 2]>::try_from(tasks).map_err(|_e| ()).unwrap();
// Verify specific results
assert!(dag.get(&tasks[0])?.is_err());
assert_eq!(dag.get(&tasks[1])?, Ok(1));
assert!(dag.get(first)?.is_err());
assert_eq!(dag.get(second)?, Ok(1));
Ok(())
}
@@ -197,7 +201,7 @@ async fn test_graceful_degradation() -> DagResult<()> {
// Here just uses base
base.iter().sum::<i32>()
}))
.depends_on(&critical);
.depends_on(critical);
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -217,13 +221,13 @@ async fn test_error_boundary_isolation() -> DagResult<()> {
.add_task(task_fn(|_x: i32| async move {
Err::<i32, &str>("Group A error")
}))
.depends_on(&a1);
.depends_on(a1);
// Group B: should be isolated from Group A's error
let b1 = dag.add_task(task_fn(|_: ()| async { 20 }));
let b2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&b1);
.depends_on(b1);
// Group C: depends on B but not A
let c1 = dag
@@ -268,10 +272,10 @@ async fn test_compensation_action() -> DagResult<()> {
let _ = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await;
// Action failed
assert!(dag.get(&action).is_err());
assert!(dag.get(action).is_err());
// Compensation ran successfully
assert_eq!(dag.get(&compensation)?, "Compensation executed");
assert_eq!(dag.get(compensation)?, "Compensation executed");
assert!(compensation_triggered.load(Ordering::SeqCst));
Ok(())