Implements dagx-11: Add comprehensive testing infrastructure including fuzzing, property-based testing, and mutation testing configuration. **Fuzzing Infrastructure:** - Created fuzz/ directory with cargo-fuzz setup - 4 fuzz targets covering different scenarios: - fuzz_dag_execution: Basic DAG execution with various patterns - fuzz_dependencies: Dependency management (linear, diamond, complex) - fuzz_type_safety: Type-safe output retrieval - fuzz_concurrent: Concurrent execution and thread safety - Comprehensive README with usage instructions - Integrated with justfile (fuzz, fuzz-all, fuzz-extended, fuzz-build) **Property-Based Testing:** - Added proptest dependency to Cargo.toml - Created tests/property_tests.rs with 14 property tests: - Value preservation through DAG - Linear chain correctness - Fan-out pattern verification - Diamond pattern correctness - String, Vec, Option, Result handling - HashMap and custom struct preservation - DAG isolation - Concurrent execution scaling - Integrated with justfile (proptest recipe) **Mutation Testing:** - Created .cargo-mutants.toml configuration - Focused on critical functions: - DagRunner::run and ::get - TaskBuilder::depends_on - ExecutableNode::execute_with_listeners - ExtractFromListeners::extract - DepsTuple::to_node_ids - Excluded low-value mutations (fmt, clone, default) - Configured for nextest and parallel execution - Already integrated via existing justfile mutants recipe **Justfile Updates:** - Added fuzzing section with 5 new recipes - Added proptest recipe for property-based tests - All recipes follow existing patterns and naming conventions Similar setup to ../announcement project but tailored for DAG-specific testing.
534 lines
15 KiB
Rust
534 lines
15 KiB
Rust
//! 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<Value = String> {
|
|
prop::string::string_regex("[a-zA-Z0-9]{0,100}").unwrap()
|
|
}
|
|
|
|
fn arb_vec() -> impl Strategy<Value = Vec<i32>> {
|
|
prop::collection::vec(any::<i32>(), 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<i32> with transformations
|
|
#[test]
|
|
fn prop_vec_operations_correct(vec in arb_vec()) {
|
|
struct VecSource { data: Vec<i32> }
|
|
#[task]
|
|
impl VecSource {
|
|
async fn run(&self) -> Vec<i32> { self.data.clone() }
|
|
}
|
|
|
|
struct Sum;
|
|
#[task]
|
|
impl Sum {
|
|
async fn run(input: &Vec<i32>) -> i32 {
|
|
input.iter().fold(0i32, |acc, &x| acc.wrapping_add(x))
|
|
}
|
|
}
|
|
|
|
struct Len;
|
|
#[task]
|
|
impl Len {
|
|
async fn run(input: &Vec<i32>) -> 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<T> values preserved correctly
|
|
///
|
|
/// Verifies: Both Some and None variants handled properly
|
|
/// Strategy: Random Option<i32> values
|
|
#[test]
|
|
fn prop_option_handling(opt in prop::option::of(any::<i32>())) {
|
|
struct OptionSource { val: Option<i32> }
|
|
#[task]
|
|
impl OptionSource {
|
|
async fn run(&self) -> Option<i32> { self.val }
|
|
}
|
|
|
|
struct MapOption;
|
|
#[task]
|
|
impl MapOption {
|
|
async fn run(input: &Option<i32>) -> Option<i32> {
|
|
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<T, E> values preserved correctly
|
|
///
|
|
/// Verifies: Both Ok and Err variants handled properly
|
|
/// Strategy: Random Result<i32, String> values
|
|
#[test]
|
|
fn prop_result_handling(
|
|
result in prop::result::maybe_ok(any::<i32>(), arb_string())
|
|
) {
|
|
struct ResultSource { val: Result<i32, String> }
|
|
#[task]
|
|
impl ResultSource {
|
|
async fn run(&self) -> Result<i32, String> { self.val.clone() }
|
|
}
|
|
|
|
struct MapResult;
|
|
#[task]
|
|
impl MapResult {
|
|
async fn run(input: &Result<i32, String>) -> Result<i32, String> {
|
|
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::<i32>(), 0..20)
|
|
) {
|
|
struct MapSource { data: HashMap<String, i32> }
|
|
#[task]
|
|
impl MapSource {
|
|
async fn run(&self) -> HashMap<String, i32> { self.data.clone() }
|
|
}
|
|
|
|
struct MapLen;
|
|
#[task]
|
|
impl MapLen {
|
|
async fn run(input: &HashMap<String, i32>) -> 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<i32>,
|
|
}
|
|
|
|
// Strategy for generating CustomData
|
|
fn arb_custom_data() -> impl Strategy<Value = CustomData> {
|
|
(any::<i32>(), 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);
|
|
}
|
|
}
|
|
}
|