//! Property-based testing for the dagx crate. //! Uses proptest to generate random test cases and verify invariants. use dagx::DagRunner; use proptest::prelude::*; use std::collections::HashMap; // Re-export task macro for use in tests use dagx::task; // Strategy for generating various types fn arb_string() -> impl Strategy { prop::string::string_regex("[a-zA-Z0-9]{0,100}").unwrap() } fn arb_vec() -> impl Strategy> { prop::collection::vec(any::(), 0..100) } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Single task execution preserves value /// /// Verifies: Output matches input for identity tasks /// Strategy: Random i32 values #[test] fn prop_single_task_preserves_value(value: i32) { struct IdentityTask { val: i32 } #[task] impl IdentityTask { async fn run(&self) -> i32 { self.val } } let dag = DagRunner::new(); let task = dag.add_task(IdentityTask { val: value }); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); assert_eq!(dag.get(task).unwrap(), value); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Linear dependency chain maintains computation /// /// Verifies: Values flow correctly through chain /// Strategy: Random starting value with fixed operations #[test] fn prop_linear_chain_correctness(start: i32) { struct Source { val: i32 } #[task] impl Source { async fn run(&self) -> i32 { self.val } } struct AddOne; #[task] impl AddOne { async fn run(input: &i32) -> i32 { input.wrapping_add(1) } } struct Double; #[task] impl Double { async fn run(input: &i32) -> i32 { input.wrapping_mul(2) } } let dag = DagRunner::new(); let source = dag.add_task(Source { val: start }); let add_one = dag.add_task(AddOne).depends_on(&source); let double = dag.add_task(Double).depends_on(&add_one); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let expected = start.wrapping_add(1).wrapping_mul(2); assert_eq!(dag.get(double).unwrap(), expected); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Fan-out delivers same value to all dependents /// /// Verifies: All dependent tasks receive identical input /// Strategy: Random source value with multiple computations #[test] fn prop_fan_out_delivers_same_value(value: i32) { struct Source { val: i32 } #[task] impl Source { async fn run(&self) -> i32 { self.val } } struct AddOne; #[task] impl AddOne { async fn run(input: &i32) -> i32 { input.wrapping_add(1) } } struct AddTwo; #[task] impl AddTwo { async fn run(input: &i32) -> i32 { input.wrapping_add(2) } } struct AddThree; #[task] impl AddThree { async fn run(input: &i32) -> i32 { input.wrapping_add(3) } } let dag = DagRunner::new(); let source = dag.add_task(Source { val: value }); let t1 = dag.add_task(AddOne).depends_on(&source); let t2 = dag.add_task(AddTwo).depends_on(&source); let t3 = dag.add_task(AddThree).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); assert_eq!(dag.get(t1).unwrap(), value.wrapping_add(1)); assert_eq!(dag.get(t2).unwrap(), value.wrapping_add(2)); assert_eq!(dag.get(t3).unwrap(), value.wrapping_add(3)); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Diamond pattern computes correctly /// /// Verifies: Fork and join operations maintain correctness /// Strategy: Random base value through diamond computation #[test] fn prop_diamond_pattern_correctness(base: i32) { struct Source { val: i32 } #[task] impl Source { async fn run(&self) -> i32 { self.val } } struct Left; #[task] impl Left { async fn run(input: &i32) -> i32 { input.wrapping_mul(2) } } struct Right; #[task] impl Right { async fn run(input: &i32) -> i32 { input.wrapping_mul(3) } } struct Merge; #[task] impl Merge { async fn run(left: &i32, right: &i32) -> i32 { left.wrapping_add(*right) } } let dag = DagRunner::new(); let source = dag.add_task(Source { val: base }); let left = dag.add_task(Left).depends_on(&source); let right = dag.add_task(Right).depends_on(&source); let merge = dag.add_task(Merge).depends_on((&left, &right)); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let expected = base.wrapping_mul(2).wrapping_add(base.wrapping_mul(3)); assert_eq!(dag.get(merge).unwrap(), expected); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: String data preserved through DAG /// /// Verifies: Non-trivial types flow correctly /// Strategy: Random strings with transformations #[test] fn prop_string_data_preserved(s in arb_string()) { struct StringSource { val: String } #[task] impl StringSource { async fn run(&self) -> String { self.val.clone() } } struct Uppercase; #[task] impl Uppercase { async fn run(input: &String) -> String { input.to_uppercase() } } struct Lowercase; #[task] impl Lowercase { async fn run(input: &String) -> String { input.to_lowercase() } } let dag = DagRunner::new(); let source = dag.add_task(StringSource { val: s.clone() }); let upper = dag.add_task(Uppercase).depends_on(&source); let lower = dag.add_task(Lowercase).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); assert_eq!(dag.get(upper).unwrap(), s.to_uppercase()); assert_eq!(dag.get(lower).unwrap(), s.to_lowercase()); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Vec operations maintain length and contents /// /// Verifies: Collection types work correctly in DAG /// Strategy: Random Vec with transformations #[test] fn prop_vec_operations_correct(vec in arb_vec()) { struct VecSource { data: Vec } #[task] impl VecSource { async fn run(&self) -> Vec { self.data.clone() } } struct Sum; #[task] impl Sum { async fn run(input: &Vec) -> i32 { input.iter().fold(0i32, |acc, &x| acc.wrapping_add(x)) } } struct Len; #[task] impl Len { async fn run(input: &Vec) -> usize { input.len() } } let dag = DagRunner::new(); let source = dag.add_task(VecSource { data: vec.clone() }); let sum = dag.add_task(Sum).depends_on(&source); let len = dag.add_task(Len).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let expected_sum = vec.iter().fold(0i32, |acc, &x| acc.wrapping_add(x)); assert_eq!(dag.get(sum).unwrap(), expected_sum); assert_eq!(dag.get(len).unwrap(), vec.len()); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Option values preserved correctly /// /// Verifies: Both Some and None variants handled properly /// Strategy: Random Option values #[test] fn prop_option_handling(opt in prop::option::of(any::())) { struct OptionSource { val: Option } #[task] impl OptionSource { async fn run(&self) -> Option { self.val } } struct MapOption; #[task] impl MapOption { async fn run(input: &Option) -> Option { input.map(|x| x.wrapping_mul(2)) } } let dag = DagRunner::new(); let source = dag.add_task(OptionSource { val: opt }); let mapped = dag.add_task(MapOption).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let expected = opt.map(|x| x.wrapping_mul(2)); assert_eq!(dag.get(mapped).unwrap(), expected); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Result values preserved correctly /// /// Verifies: Both Ok and Err variants handled properly /// Strategy: Random Result values #[test] fn prop_result_handling( result in prop::result::maybe_ok(any::(), arb_string()) ) { struct ResultSource { val: Result } #[task] impl ResultSource { async fn run(&self) -> Result { self.val.clone() } } struct MapResult; #[task] impl MapResult { async fn run(input: &Result) -> Result { input.as_ref().map(|&x| x.wrapping_add(1)).map_err(|e| e.clone()) } } let dag = DagRunner::new(); let source = dag.add_task(ResultSource { val: result.clone() }); let mapped = dag.add_task(MapResult).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let expected = result.map(|x| x.wrapping_add(1)); assert_eq!(dag.get(mapped).unwrap(), expected); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: HashMap structure preserved through DAG /// /// Verifies: Complex types maintain structure /// Strategy: Random HashMap with string keys and i32 values #[test] fn prop_hashmap_preservation( data in prop::collection::hash_map(arb_string(), any::(), 0..20) ) { struct MapSource { data: HashMap } #[task] impl MapSource { async fn run(&self) -> HashMap { self.data.clone() } } struct MapLen; #[task] impl MapLen { async fn run(input: &HashMap) -> usize { input.len() } } let dag = DagRunner::new(); let source = dag.add_task(MapSource { data: data.clone() }); let len = dag.add_task(MapLen).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let result_map = dag.get(source).unwrap(); assert_eq!(result_map.len(), data.len()); for (k, v) in &data { assert_eq!(result_map.get(k), Some(v)); } assert_eq!(dag.get(len).unwrap(), data.len()); } } // Custom struct for testing #[derive(Clone, Debug, PartialEq)] struct CustomData { id: i32, name: String, values: Vec, } // Strategy for generating CustomData fn arb_custom_data() -> impl Strategy { (any::(), arb_string(), arb_vec()) .prop_map(|(id, name, values)| CustomData { id, name, values }) } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Custom structs preserved through DAG /// /// Verifies: User-defined types work correctly /// Strategy: Random custom struct instances #[test] fn prop_custom_struct_preservation(custom in arb_custom_data()) { struct CustomSource { data: CustomData } #[task] impl CustomSource { async fn run(&self) -> CustomData { self.data.clone() } } struct ExtractId; #[task] impl ExtractId { async fn run(input: &CustomData) -> i32 { input.id } } let dag = DagRunner::new(); let source = dag.add_task(CustomSource { data: custom.clone() }); let id = dag.add_task(ExtractId).depends_on(&source); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); let result = dag.get(source).unwrap(); assert_eq!(result, custom); assert_eq!(dag.get(id).unwrap(), custom.id); } } proptest! { #![proptest_config(ProptestConfig::with_cases(256))] /// Property: Multiple independent DAGs are isolated /// /// Verifies: DAG instances don't interfere with each other /// Strategy: Two random values in separate DAGs #[test] fn prop_dag_isolation(value1: i32, value2: i32) { struct Source { val: i32 } #[task] impl Source { async fn run(&self) -> i32 { self.val } } let dag1 = DagRunner::new(); let dag2 = DagRunner::new(); let task1 = dag1.add_task(Source { val: value1 }); let task2 = dag2.add_task(Source { val: value2 }); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag1.run(|fut| { tokio::spawn(fut); }).await.unwrap(); dag2.run(|fut| { tokio::spawn(fut); }).await.unwrap(); }); assert_eq!(dag1.get(task1).unwrap(), value1); assert_eq!(dag2.get(task2).unwrap(), value2); } } proptest! { #![proptest_config(ProptestConfig::with_cases(128))] /// Property: N independent tasks all execute correctly /// /// Verifies: Concurrent execution scales correctly /// Strategy: Variable task count (1-20) with unique values #[test] fn prop_concurrent_independent_tasks(count in 1usize..20) { struct IndependentTask { val: i32 } #[task] impl IndependentTask { async fn run(&self) -> i32 { self.val } } let dag = DagRunner::new(); let mut tasks = Vec::new(); for i in 0..count { let task = dag.add_task(IndependentTask { val: i as i32 }); tasks.push((task, i as i32)); } let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { dag.run(|fut| { tokio::spawn(fut); }).await }).unwrap(); for (handle, expected) in tasks { assert_eq!(dag.get(handle).unwrap(), expected); } } }