281 lines
7.5 KiB
Rust
281 lines
7.5 KiB
Rust
//! Unit tests for runner module
|
|
|
|
use crate::error::DagError;
|
|
use crate::runner::DagRunner;
|
|
use crate::types::TaskHandle;
|
|
|
|
use futures::FutureExt;
|
|
|
|
// Initialize tracing subscriber for tests (idempotent)
|
|
#[cfg(feature = "tracing")]
|
|
fn init_tracing() {
|
|
use std::sync::Once;
|
|
static INIT: Once = Once::new();
|
|
INIT.call_once(|| {
|
|
tracing_subscriber::fmt()
|
|
.with_test_writer()
|
|
.with_max_level(tracing::Level::TRACE)
|
|
.try_init()
|
|
.ok();
|
|
});
|
|
}
|
|
|
|
#[cfg(not(feature = "tracing"))]
|
|
fn init_tracing() {
|
|
// No-op when tracing is disabled
|
|
}
|
|
|
|
struct TestTask {
|
|
value: i32,
|
|
}
|
|
|
|
#[crate::task]
|
|
impl TestTask {
|
|
async fn run(&self) -> i32 {
|
|
self.value
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_dag_runner_new() {
|
|
// Test that DagRunner::new() creates a new instance
|
|
let dag = DagRunner::new();
|
|
|
|
// Initial state should be empty
|
|
assert_eq!(dag.nodes.lock().len(), 0);
|
|
assert_eq!(dag.edges.lock().len(), 0);
|
|
assert_eq!(dag.dependents.lock().len(), 0);
|
|
|
|
// Adding a task works
|
|
dag.add_task(TestTask { value: 42 });
|
|
assert_eq!(dag.nodes.lock().len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dag_runner_default() {
|
|
// Test lines 105-106 in runner.rs - Default implementation
|
|
let dag = DagRunner::default();
|
|
|
|
// Should behave the same as new()
|
|
assert_eq!(dag.nodes.lock().len(), 0);
|
|
assert_eq!(dag.edges.lock().len(), 0);
|
|
assert_eq!(dag.dependents.lock().len(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_wrong_type() {
|
|
init_tracing();
|
|
// Test getting with wrong type - this should return TypeMismatch
|
|
let dag = DagRunner::new();
|
|
let handle = dag.add_task(TestTask { value: 42 });
|
|
|
|
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Try to get with wrong type - downcast will fail
|
|
let fake_handle: TaskHandle<String> = TaskHandle {
|
|
id: handle.id,
|
|
_phantom: std::marker::PhantomData,
|
|
};
|
|
|
|
let result = dag.get(fake_handle);
|
|
assert!(result.is_err());
|
|
|
|
// When downcast fails, we get TypeMismatch
|
|
match result.unwrap_err() {
|
|
DagError::TypeMismatch { expected, .. } => {
|
|
assert_eq!(expected, std::any::type_name::<String>());
|
|
}
|
|
_ => panic!("Expected TypeMismatch error"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_result_not_found() {
|
|
init_tracing();
|
|
// Test getting a result before running the DAG
|
|
let dag = DagRunner::new();
|
|
let handle = dag.add_task(TestTask { value: 42 });
|
|
let handle_id = handle.id.0; // Save ID before moving handle
|
|
|
|
let result = dag.get(handle);
|
|
assert!(result.is_err());
|
|
match result.unwrap_err() {
|
|
DagError::ResultNotFound { task_id } => {
|
|
assert_eq!(task_id, handle_id);
|
|
}
|
|
_ => panic!("Expected ResultNotFound error"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_run_protection() {
|
|
init_tracing();
|
|
// Test that run_lock prevents concurrent runs
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
|
|
let dag = DagRunner::new();
|
|
|
|
// Add a task that takes some time
|
|
struct SlowTask;
|
|
#[crate::task]
|
|
impl SlowTask {
|
|
async fn run(&self) -> i32 {
|
|
sleep(Duration::from_millis(100)).await;
|
|
42
|
|
}
|
|
}
|
|
|
|
dag.add_task(SlowTask);
|
|
|
|
// Wrap in Arc for sharing between tasks
|
|
let dag = Arc::new(dag);
|
|
let dag1 = Arc::clone(&dag);
|
|
let dag2 = Arc::clone(&dag);
|
|
|
|
// Use a barrier to ensure both runs start at exactly the same time
|
|
let barrier = Arc::new(tokio::sync::Barrier::new(2));
|
|
let barrier1 = barrier.clone();
|
|
let barrier2 = barrier.clone();
|
|
|
|
// Start two runs concurrently
|
|
let handle1 = tokio::spawn(async move {
|
|
barrier1.wait().await;
|
|
dag1.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await
|
|
});
|
|
|
|
let handle2 = tokio::spawn(async move {
|
|
barrier2.wait().await;
|
|
dag2.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await
|
|
});
|
|
|
|
// One should succeed, one should fail (concurrent execution not supported)
|
|
let result1 = handle1.await.unwrap();
|
|
let result2 = handle2.await.unwrap();
|
|
|
|
// Exactly one should be ok, one should be err
|
|
assert!(result1.is_ok() != result2.is_ok());
|
|
|
|
// The error should be about concurrent execution
|
|
if let Err(e) = result1 {
|
|
assert!(matches!(e, crate::error::DagError::ConcurrentExecution));
|
|
}
|
|
if let Err(e) = result2 {
|
|
assert!(matches!(e, crate::error::DagError::ConcurrentExecution));
|
|
}
|
|
}
|
|
|
|
#[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_multiple_get_calls() {
|
|
init_tracing();
|
|
// Test that get() can be called multiple times for the same handle
|
|
let dag = DagRunner::new();
|
|
let handle = dag.add_task(TestTask { value: 100 });
|
|
|
|
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Multiple get calls should all return the same value
|
|
let result1 = dag.get(&handle).unwrap();
|
|
let result2 = dag.get(&handle).unwrap();
|
|
let result3 = dag.get(&handle).unwrap();
|
|
|
|
assert_eq!(result1, 100);
|
|
assert_eq!(result2, 100);
|
|
assert_eq!(result3, 100);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invalid_node_id_in_get() {
|
|
init_tracing();
|
|
// Test getting with an invalid node ID
|
|
let dag = DagRunner::new();
|
|
|
|
// Create a handle with an ID that doesn't exist
|
|
let invalid_handle: TaskHandle<i32> = TaskHandle {
|
|
id: crate::types::NodeId(999),
|
|
_phantom: std::marker::PhantomData,
|
|
};
|
|
|
|
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
|
|
.await
|
|
.unwrap();
|
|
|
|
let result = dag.get(invalid_handle);
|
|
assert!(result.is_err());
|
|
|
|
// With centralized output storage, we get ResultNotFound for invalid node IDs
|
|
match result.unwrap_err() {
|
|
DagError::ResultNotFound { task_id } => {
|
|
assert_eq!(task_id, 999);
|
|
}
|
|
_ => panic!("Expected ResultNotFound error"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_task_panic_in_multi_task_layer() {
|
|
init_tracing();
|
|
// Test that a panic in one task of a multi-task layer is caught and reported
|
|
|
|
struct Source;
|
|
#[crate::task]
|
|
impl Source {
|
|
async fn run(&self) -> i32 {
|
|
42
|
|
}
|
|
}
|
|
|
|
struct PanicTask;
|
|
#[crate::task]
|
|
impl PanicTask {
|
|
async fn run(&self, _input: &i32) -> i32 {
|
|
panic!("This task panics!");
|
|
}
|
|
}
|
|
|
|
struct GoodTask;
|
|
#[crate::task]
|
|
impl GoodTask {
|
|
async fn run(&self, input: &i32) -> i32 {
|
|
input * 2
|
|
}
|
|
}
|
|
|
|
let dag = DagRunner::new();
|
|
let source = dag.add_task(Source);
|
|
|
|
// Create two tasks in the same layer - one panics, one doesn't
|
|
let _panic_task = dag.add_task(PanicTask).depends_on(&source);
|
|
let _good_task = dag.add_task(GoodTask).depends_on(&source);
|
|
|
|
// Run the DAG - the panic should be caught
|
|
let result = dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).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());
|
|
}
|