test: add comprehensive testing infrastructure (fuzzing, proptest, mutation)

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.
This commit is contained in:
swaits committed 2025-10-14 10:44:30 -06:00
1 parent 10af81ade7
commit 5839b35979
20 files changed
+6545

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use dagx::{task, DagRunner};
fuzz_target!(|data: &[u8]| {
// Only test with reasonable sizes
if data.is_empty() || data.len() > 100 {
return;
}
// Test various dependency patterns based on input data
let pattern = data[0] % 4;
struct SimpleTask { val: i32 }
#[task]
impl SimpleTask {
async fn run(&self) -> i32 { self.val }
}
struct DependentTask;
#[task]
impl DependentTask {
async fn run(input: &i32) -> i32 {
input.wrapping_add(1)
}
}
let rt = tokio::runtime::Runtime::new().unwrap();
match pattern {
0 => {
// Linear dependency chain
let dag = DagRunner::new();
let t1 = dag.add_task(SimpleTask { val: data[0] as i32 });
let t2 = dag.add_task(DependentTask).depends_on(&t1);
let t3 = dag.add_task(DependentTask).depends_on(&t2);
if rt.block_on(async {
dag.run(|fut| { tokio::spawn(fut); }).await
}).is_ok() {
if let Ok(result) = dag.get(t3) {
assert_eq!(result, (data[0] as i32).wrapping_add(2));
}
}
}
1 => {
// Diamond pattern
let dag = DagRunner::new();
let source = dag.add_task(SimpleTask { val: data[0] as i32 });
let left = dag.add_task(DependentTask).depends_on(&source);
let right = dag.add_task(DependentTask).depends_on(&source);
struct MergeTask;
#[task]
impl MergeTask {
async fn run(a: &i32, b: &i32) -> i32 {
a.wrapping_add(*b)
}
}
let merge = dag.add_task(MergeTask).depends_on((&left, &right));
if rt.block_on(async {
dag.run(|fut| { tokio::spawn(fut); }).await
}).is_ok() {
if let Ok(result) = dag.get(merge) {
let expected = (data[0] as i32).wrapping_add(1);
assert_eq!(result, expected.wrapping_add(expected));
}
}
}
2 => {
// Multiple independent sources
let dag = DagRunner::new();
let t1 = dag.add_task(SimpleTask { val: data[0] as i32 });
let t2 = dag.add_task(SimpleTask {
val: data.get(1).copied().unwrap_or(0) as i32
});
let t3 = dag.add_task(SimpleTask {
val: data.get(2).copied().unwrap_or(0) as i32
});
if rt.block_on(async {
dag.run(|fut| { tokio::spawn(fut); }).await
}).is_ok() {
if let (Ok(r1), Ok(r2), Ok(r3)) = (dag.get(t1), dag.get(t2), dag.get(t3)) {
assert_eq!(r1, data[0] as i32);
assert_eq!(r2, data.get(1).copied().unwrap_or(0) as i32);
assert_eq!(r3, data.get(2).copied().unwrap_or(0) as i32);
}
}
}
_ => {
// Complex dependency graph
let dag = DagRunner::new();
let s1 = dag.add_task(SimpleTask { val: data[0] as i32 });
let s2 = dag.add_task(SimpleTask {
val: data.get(1).copied().unwrap_or(0) as i32
});
let d1 = dag.add_task(DependentTask).depends_on(&s1);
let d2 = dag.add_task(DependentTask).depends_on(&s2);
let d3 = dag.add_task(DependentTask).depends_on(&d1);
if rt.block_on(async {
dag.run(|fut| { tokio::spawn(fut); }).await
}).is_ok() {
let _ = dag.get(d1);
let _ = dag.get(d2);
let _ = dag.get(d3);
}
}
}
});