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:
20 files changed
+6545
No files matched your search
Binary file not shown.
@@ -0,0 +1,142 @@
|
|||||||
|
# cargo-mutants configuration for the dagx crate
|
||||||
|
#
|
||||||
|
# This configuration optimizes mutation testing by:
|
||||||
|
# - Focusing on correctness-critical code
|
||||||
|
# - Excluding low-value mutations
|
||||||
|
# - Setting appropriate timeouts
|
||||||
|
# - Configuring parallel execution
|
||||||
|
|
||||||
|
# Overall timeout for each mutant test
|
||||||
|
# Increased from default to handle async tests
|
||||||
|
timeout = 300 # 5 minutes per mutant
|
||||||
|
|
||||||
|
# Number of parallel jobs
|
||||||
|
# Set to number of CPU cores for faster execution
|
||||||
|
# jobs = 8 # Uncomment and adjust for your system
|
||||||
|
|
||||||
|
# Test tool to use
|
||||||
|
# nextest is faster than cargo test
|
||||||
|
test_tool = "nextest"
|
||||||
|
|
||||||
|
# Additional arguments to pass to the test command
|
||||||
|
# --no-fail-fast ensures all tests run even if one fails
|
||||||
|
test_args = ["--no-fail-fast"]
|
||||||
|
|
||||||
|
# Exclude patterns for files that shouldn't be mutated
|
||||||
|
# These are low-value targets that produce noise
|
||||||
|
exclude_globs = [
|
||||||
|
# Test files - we test the tests, not mutate them
|
||||||
|
"tests/**",
|
||||||
|
"**/tests.rs",
|
||||||
|
"benches/**",
|
||||||
|
"examples/**",
|
||||||
|
|
||||||
|
# Proc macro crate
|
||||||
|
"dagx-macros/**",
|
||||||
|
|
||||||
|
# Build scripts
|
||||||
|
"build.rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Exclude specific functions/methods that shouldn't be mutated
|
||||||
|
# These are low-value mutations that don't test correctness
|
||||||
|
[[exclude_functions]]
|
||||||
|
# Display trait implementations - not correctness-critical
|
||||||
|
regex = ".*::fmt"
|
||||||
|
reason = "Display/Debug implementations don't affect correctness"
|
||||||
|
|
||||||
|
[[exclude_functions]]
|
||||||
|
# Clone implementations - trivial mutations
|
||||||
|
regex = ".*::clone"
|
||||||
|
reason = "Clone implementations are trivial"
|
||||||
|
|
||||||
|
[[exclude_functions]]
|
||||||
|
# Default implementations - trivial
|
||||||
|
regex = ".*::default"
|
||||||
|
reason = "Default implementations are trivial"
|
||||||
|
|
||||||
|
# Specific functions to focus on (include)
|
||||||
|
# These are the correctness-critical paths
|
||||||
|
[[examine_functions]]
|
||||||
|
# Core DAG execution logic
|
||||||
|
regex = "DagRunner::run"
|
||||||
|
reason = "Critical: Core DAG execution orchestration"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Task addition and dependency tracking
|
||||||
|
regex = "DagRunner::add_task"
|
||||||
|
reason = "Critical: Task registration and node creation"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Dependency management
|
||||||
|
regex = "TaskBuilder::depends_on"
|
||||||
|
reason = "Critical: Dependency edge creation"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Output extraction and type checking
|
||||||
|
regex = "DagRunner::get"
|
||||||
|
reason = "Critical: Type-safe output retrieval"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Node execution with listeners
|
||||||
|
regex = "ExecutableNode::execute_with_listeners"
|
||||||
|
reason = "Critical: Task execution and value propagation"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Listener extraction from announcements
|
||||||
|
regex = "ExtractFromListeners::extract"
|
||||||
|
reason = "Critical: Input extraction for dependent tasks"
|
||||||
|
|
||||||
|
[[examine_functions]]
|
||||||
|
# Dependency tuple conversion
|
||||||
|
regex = "DepsTuple::to_node_ids"
|
||||||
|
reason = "Critical: Dependency tracking correctness"
|
||||||
|
|
||||||
|
# Minimum test time threshold
|
||||||
|
# If tests complete faster than this, warn about insufficient testing
|
||||||
|
# minimum_test_time = 1.0 # Uncomment to enable
|
||||||
|
|
||||||
|
# Output formats
|
||||||
|
# Generate both text and JSON for tracking
|
||||||
|
# output_formats = ["text", "json"] # Uncomment to enable
|
||||||
|
|
||||||
|
# Baseline
|
||||||
|
# Compare mutations against a baseline to detect regressions
|
||||||
|
# baseline = "main" # Uncomment to enable baseline comparison
|
||||||
|
|
||||||
|
# Notes on configuration strategy:
|
||||||
|
#
|
||||||
|
# 1. FOCUS: We explicitly list critical functions to mutate
|
||||||
|
# - DAG execution: run(), execute_with_listeners()
|
||||||
|
# - Dependency tracking: add_task(), depends_on(), to_node_ids()
|
||||||
|
# - Type safety: get() with downcasting
|
||||||
|
# - Value propagation: extract() from listeners
|
||||||
|
#
|
||||||
|
# 2. EXCLUDE: We exclude low-value targets
|
||||||
|
# - Display/Debug impls: String formatting doesn't affect correctness
|
||||||
|
# - Clone/Default impls: Trivial, unlikely to have bugs
|
||||||
|
# - Tests themselves: We test production code, not tests
|
||||||
|
# - Proc macros: Generated code tested via integration
|
||||||
|
#
|
||||||
|
# 3. TIMEOUT: 5 minutes is generous
|
||||||
|
# - Most mutants complete in seconds
|
||||||
|
# - Allows for async tests with real execution
|
||||||
|
# - Catches actual deadlocks without false positives
|
||||||
|
#
|
||||||
|
# 4. NEXTEST: Faster test execution
|
||||||
|
# - Parallel test execution
|
||||||
|
# - Better output formatting
|
||||||
|
# - Improved reliability
|
||||||
|
#
|
||||||
|
# 5. NO-FAIL-FAST: See all failures
|
||||||
|
# - Don't stop at first test failure
|
||||||
|
# - Get complete picture of mutation impact
|
||||||
|
# - Better for debugging
|
||||||
|
#
|
||||||
|
# Expected mutation counts with this configuration:
|
||||||
|
# - Target: ~30-50 mutants for core logic
|
||||||
|
# - Score: 90%+ (high bar for DAG correctness)
|
||||||
|
#
|
||||||
|
# Runtime with this configuration:
|
||||||
|
# - Current: ~5-10 minutes
|
||||||
|
# - With nextest: ~30% faster
|
||||||
@@ -36,6 +36,8 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"
|
|||||||
async-std = { version = "1", features = ["attributes"] }
|
async-std = { version = "1", features = ["attributes"] }
|
||||||
smol = "2"
|
smol = "2"
|
||||||
criterion = "0.7"
|
criterion = "0.7"
|
||||||
|
# Property-based testing
|
||||||
|
proptest = "1"
|
||||||
# Quirky async runtimes for edge-case testing
|
# Quirky async runtimes for edge-case testing
|
||||||
async-executor = "1.13"
|
async-executor = "1.13"
|
||||||
pollster = "0.4"
|
pollster = "0.4"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "dagx-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.4"
|
||||||
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
|
[dependencies.dagx]
|
||||||
|
path = ".."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_dag_execution"
|
||||||
|
path = "fuzz_targets/fuzz_dag_execution.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_dependencies"
|
||||||
|
path = "fuzz_targets/fuzz_dependencies.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_type_safety"
|
||||||
|
path = "fuzz_targets/fuzz_type_safety.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_concurrent"
|
||||||
|
path = "fuzz_targets/fuzz_concurrent.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
# Fuzz Testing for dagx
|
||||||
|
|
||||||
|
This directory contains fuzz testing infrastructure using [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) and libFuzzer.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
We have 4 fuzz targets that test different aspects of the `dagx` library:
|
||||||
|
|
||||||
|
1. **fuzz_dag_execution** - Tests DAG execution with various task configurations
|
||||||
|
2. **fuzz_dependencies** - Tests dependency patterns (linear, diamond, fan-out, complex)
|
||||||
|
3. **fuzz_type_safety** - Tests type-safe output retrieval with various types
|
||||||
|
4. **fuzz_concurrent** - Tests concurrent task execution and thread safety
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Fuzzing requires nightly Rust and cargo-fuzz:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup install nightly
|
||||||
|
cargo install cargo-fuzz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Fuzz Tests
|
||||||
|
|
||||||
|
Run a single fuzz target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +nightly fuzz run fuzz_dag_execution
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with a time limit (recommended for CI):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +nightly fuzz run fuzz_dag_execution -- -max_total_time=60
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with a specific number of iterations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +nightly fuzz run fuzz_dag_execution -- -runs=10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Run all fuzz targets sequentially:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for target in fuzz_dag_execution fuzz_dependencies fuzz_type_safety fuzz_concurrent; do
|
||||||
|
echo "Running $target..."
|
||||||
|
cargo +nightly fuzz run $target -- -runs=10000
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Fuzzing
|
||||||
|
|
||||||
|
For deeper testing, run fuzz targets for extended periods:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run overnight (8 hours)
|
||||||
|
cargo +nightly fuzz run fuzz_concurrent -- -max_total_time=28800
|
||||||
|
|
||||||
|
# Run until crash (or Ctrl+C)
|
||||||
|
cargo +nightly fuzz run fuzz_dag_execution
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding Output
|
||||||
|
|
||||||
|
Fuzzer output includes:
|
||||||
|
|
||||||
|
- `cov: X` - Code coverage (number of edges covered)
|
||||||
|
- `ft: X` - Feature coverage (number of unique code paths)
|
||||||
|
- `corp: X/Yb` - Corpus size (X inputs, Y bytes total)
|
||||||
|
- `exec/s: X` - Executions per second
|
||||||
|
|
||||||
|
If a crash is found:
|
||||||
|
|
||||||
|
- Crash input is saved to `artifacts/FUZZ_TARGET/crash-*`
|
||||||
|
- Reproduce with: `cargo +nightly fuzz run FUZZ_TARGET artifacts/FUZZ_TARGET/crash-*`
|
||||||
|
|
||||||
|
## Fuzz Target Details
|
||||||
|
|
||||||
|
### fuzz_dag_execution
|
||||||
|
|
||||||
|
Tests basic DAG execution:
|
||||||
|
- Simple linear task chains
|
||||||
|
- Fan-out patterns (one task → multiple dependents)
|
||||||
|
- Various data types (i32, Vec<u8>, custom structs)
|
||||||
|
- DAG execution correctness
|
||||||
|
- Output value verification
|
||||||
|
|
||||||
|
### fuzz_dependencies
|
||||||
|
|
||||||
|
Tests dependency management:
|
||||||
|
- Linear dependency chains
|
||||||
|
- Diamond patterns (source → two paths → merge)
|
||||||
|
- Multiple independent sources
|
||||||
|
- Complex dependency graphs
|
||||||
|
- Dependency tracking correctness
|
||||||
|
|
||||||
|
### fuzz_type_safety
|
||||||
|
|
||||||
|
Tests type system correctness:
|
||||||
|
- Various output types (i32, String, Vec, custom structs)
|
||||||
|
- Correct type retrieval with dag.get()
|
||||||
|
- Option<T> and Result<T, E> handling
|
||||||
|
- Custom struct preservation
|
||||||
|
- Type mismatch error handling
|
||||||
|
|
||||||
|
### fuzz_concurrent
|
||||||
|
|
||||||
|
Tests thread safety and concurrency:
|
||||||
|
- Multiple independent concurrent tasks
|
||||||
|
- Concurrent dependent tasks (fan-out)
|
||||||
|
- Arc<DagRunner> sharing across threads
|
||||||
|
- Task timing variations
|
||||||
|
- Result access from multiple threads
|
||||||
|
|
||||||
|
## Adding New Fuzz Targets
|
||||||
|
|
||||||
|
1. Create a new file in `fuzz_targets/` (e.g., `fuzz_new_feature.rs`)
|
||||||
|
2. Use the template:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![no_main]
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
// Your fuzz test here
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add to `fuzz/Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_new_feature"
|
||||||
|
path = "fuzz_targets/fuzz_new_feature.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Build and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +nightly fuzz build
|
||||||
|
cargo +nightly fuzz run fuzz_new_feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI Integration
|
||||||
|
|
||||||
|
Fuzzing can run automatically in CI on every push/PR:
|
||||||
|
- Each target runs for 60-120 seconds
|
||||||
|
- Targets run 1,000-10,000 iterations minimum
|
||||||
|
- Any crashes fail the build
|
||||||
|
|
||||||
|
See your CI configuration for details.
|
||||||
|
|
||||||
|
## Corpus Management
|
||||||
|
|
||||||
|
The fuzzer builds a corpus of interesting inputs in `corpus/FUZZ_TARGET/`. These inputs:
|
||||||
|
- Are automatically discovered during fuzzing
|
||||||
|
- Can be committed to Git for regression testing
|
||||||
|
- Help future fuzzing sessions start with good coverage
|
||||||
|
|
||||||
|
To minimize the corpus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo +nightly fuzz cmin fuzz_dag_execution
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Start with short runs** - Use `-max_total_time=60` to catch obvious bugs quickly
|
||||||
|
2. **Run overnight** - For thorough testing, run for hours or days
|
||||||
|
3. **Check coverage** - Monitor `cov:` values to ensure good code coverage
|
||||||
|
4. **Save crashes** - Commit crash artifacts as regression tests
|
||||||
|
5. **Update regularly** - Re-run fuzzing when adding new features
|
||||||
|
6. **Limit data size** - Fuzz targets filter out oversized inputs to avoid timeouts
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- DAG execution involves async runtime overhead
|
||||||
|
- Each fuzz iteration creates a new Tokio runtime
|
||||||
|
- Typical execution rate: 100-1000 runs/second
|
||||||
|
- Adjust `-runs` or `-max_total_time` based on your needs
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [cargo-fuzz book](https://rust-fuzz.github.io/book/cargo-fuzz.html)
|
||||||
|
- [libFuzzer documentation](https://llvm.org/docs/LibFuzzer.html)
|
||||||
|
- [Fuzzing in Rust](https://rust-fuzz.github.io/book/)
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use dagx::{task, DagRunner};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if data.is_empty() || data.len() > 50 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
// Test concurrent task execution
|
||||||
|
struct SlowTask { val: i32, delay_ms: u64 }
|
||||||
|
#[task]
|
||||||
|
impl SlowTask {
|
||||||
|
async fn run(&self) -> i32 {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(self.delay_ms)).await;
|
||||||
|
self.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FastTask { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl FastTask {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test concurrent independent tasks
|
||||||
|
{
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
// Create multiple independent tasks
|
||||||
|
let num_tasks = (data[0] % 5 + 1) as usize;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..num_tasks {
|
||||||
|
let val = data.get(i).copied().unwrap_or(0) as i32;
|
||||||
|
let delay = (val.abs() as u64 % 10) + 1;
|
||||||
|
|
||||||
|
let task = dag.add_task(SlowTask { val, delay_ms: delay });
|
||||||
|
tasks.push((task, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
// Verify all tasks completed with correct values
|
||||||
|
for (handle, expected) in tasks {
|
||||||
|
if let Ok(result) = dag.get(handle) {
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test concurrent dependent tasks
|
||||||
|
{
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
let source = dag.add_task(FastTask { val: data[0] as i32 });
|
||||||
|
|
||||||
|
// Create multiple tasks that depend on source
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
let base = data[0] as i32;
|
||||||
|
if let Ok(r1) = dag.get(t1) {
|
||||||
|
assert_eq!(r1, base.wrapping_add(1));
|
||||||
|
}
|
||||||
|
if let Ok(r2) = dag.get(t2) {
|
||||||
|
assert_eq!(r2, base.wrapping_add(2));
|
||||||
|
}
|
||||||
|
if let Ok(r3) = dag.get(t3) {
|
||||||
|
assert_eq!(r3, base.wrapping_add(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Arc sharing across concurrent tasks
|
||||||
|
{
|
||||||
|
let dag = Arc::new(DagRunner::new());
|
||||||
|
|
||||||
|
struct SharedTask { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl SharedTask {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = dag.add_task(SharedTask { val: data[0] as i32 });
|
||||||
|
let task_copy = task;
|
||||||
|
|
||||||
|
let dag1 = Arc::clone(&dag);
|
||||||
|
let dag2 = Arc::clone(&dag);
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
// Multiple threads can access result
|
||||||
|
if let Ok(r1) = dag1.get(task_copy) {
|
||||||
|
if let Ok(r2) = dag2.get(task_copy) {
|
||||||
|
assert_eq!(r1, r2);
|
||||||
|
assert_eq!(r1, data[0] as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
// Only fuzz with reasonable data sizes to avoid timeouts
|
||||||
|
if data.is_empty() || data.len() > 1000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert bytes to values for tasks
|
||||||
|
let value1 = data.get(0).copied().unwrap_or(0) as i32;
|
||||||
|
let value2 = data.get(1).copied().unwrap_or(0) as i32;
|
||||||
|
let value3 = data.get(2).copied().unwrap_or(0) as i32;
|
||||||
|
|
||||||
|
// Test simple linear DAG execution
|
||||||
|
{
|
||||||
|
struct Source1 { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl Source1 {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Source2 { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl Source2 {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Compute;
|
||||||
|
#[task]
|
||||||
|
impl Compute {
|
||||||
|
async fn run(a: &i32, b: &i32) -> i32 {
|
||||||
|
a.wrapping_add(*b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let s1 = dag.add_task(Source1 { val: value1 });
|
||||||
|
let s2 = dag.add_task(Source2 { val: value2 });
|
||||||
|
let compute = dag.add_task(Compute).depends_on((&s1, &s2));
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
if let Ok(()) = rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}) {
|
||||||
|
// Verify output
|
||||||
|
if let Ok(result) = dag.get(compute) {
|
||||||
|
assert_eq!(result, value1.wrapping_add(value2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test fan-out pattern
|
||||||
|
{
|
||||||
|
struct Source { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl Source {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Double;
|
||||||
|
#[task]
|
||||||
|
impl Double {
|
||||||
|
async fn run(input: &i32) -> i32 {
|
||||||
|
input.wrapping_mul(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Triple;
|
||||||
|
#[task]
|
||||||
|
impl Triple {
|
||||||
|
async fn run(input: &i32) -> i32 {
|
||||||
|
input.wrapping_mul(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let source = dag.add_task(Source { val: value3 });
|
||||||
|
let double = dag.add_task(Double).depends_on(&source);
|
||||||
|
let triple = dag.add_task(Triple).depends_on(&source);
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
if let Ok(()) = rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}) {
|
||||||
|
// Verify outputs
|
||||||
|
if let (Ok(d), Ok(t)) = (dag.get(double), dag.get(triple)) {
|
||||||
|
assert_eq!(d, value3.wrapping_mul(2));
|
||||||
|
assert_eq!(t, value3.wrapping_mul(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test error-free execution with various data
|
||||||
|
if data.len() >= 4 {
|
||||||
|
struct DataTask { data: Vec<u8> }
|
||||||
|
#[task]
|
||||||
|
impl DataTask {
|
||||||
|
async fn run(&self) -> Vec<u8> {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let task = dag.add_task(DataTask {
|
||||||
|
data: data[..4.min(data.len())].to_vec()
|
||||||
|
});
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
if let Ok(()) = rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}) {
|
||||||
|
if let Ok(result) = dag.get(task) {
|
||||||
|
assert_eq!(result.len(), 4.min(data.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use dagx::{task, DagRunner};
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if data.is_empty() || data.len() > 100 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
// Test type safety with various output types
|
||||||
|
struct IntTask { val: i32 }
|
||||||
|
#[task]
|
||||||
|
impl IntTask {
|
||||||
|
async fn run(&self) -> i32 { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StringTask { val: String }
|
||||||
|
#[task]
|
||||||
|
impl StringTask {
|
||||||
|
async fn run(&self) -> String { self.val.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VecTask { data: Vec<u8> }
|
||||||
|
#[task]
|
||||||
|
impl VecTask {
|
||||||
|
async fn run(&self) -> Vec<u8> { self.data.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test correct type retrieval
|
||||||
|
{
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let int_task = dag.add_task(IntTask { val: data[0] as i32 });
|
||||||
|
|
||||||
|
if let Ok(string_data) = std::str::from_utf8(data) {
|
||||||
|
let string_task = dag.add_task(StringTask {
|
||||||
|
val: string_data.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
// Correct type should work
|
||||||
|
let _ = dag.get(int_task);
|
||||||
|
let _ = dag.get(string_task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Vec type
|
||||||
|
{
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let vec_task = dag.add_task(VecTask {
|
||||||
|
data: data.to_vec()
|
||||||
|
});
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
if let Ok(result) = dag.get(vec_task) {
|
||||||
|
assert_eq!(result, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom struct types
|
||||||
|
{
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct CustomData {
|
||||||
|
value: i32,
|
||||||
|
flag: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomTask { data: CustomData }
|
||||||
|
#[task]
|
||||||
|
impl CustomTask {
|
||||||
|
async fn run(&self) -> CustomData {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let custom = CustomData {
|
||||||
|
value: data[0] as i32,
|
||||||
|
flag: data.get(1).map(|&b| b % 2 == 0).unwrap_or(false),
|
||||||
|
};
|
||||||
|
let task = dag.add_task(CustomTask { data: custom.clone() });
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
if let Ok(result) = dag.get(task) {
|
||||||
|
assert_eq!(result, custom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Option and Result types
|
||||||
|
{
|
||||||
|
struct OptionTask { val: Option<i32> }
|
||||||
|
#[task]
|
||||||
|
impl OptionTask {
|
||||||
|
async fn run(&self) -> Option<i32> { self.val }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResultTask { val: Result<i32, String> }
|
||||||
|
#[task]
|
||||||
|
impl ResultTask {
|
||||||
|
async fn run(&self) -> Result<i32, String> { self.val.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
let opt_task = dag.add_task(OptionTask {
|
||||||
|
val: if data[0] % 2 == 0 {
|
||||||
|
Some(data[0] as i32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result_task = dag.add_task(ResultTask {
|
||||||
|
val: if data.get(1).map(|&b| b % 2 == 0).unwrap_or(false) {
|
||||||
|
Ok(data.get(1).copied().unwrap_or(0) as i32)
|
||||||
|
} else {
|
||||||
|
Err("error".to_string())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if rt.block_on(async {
|
||||||
|
dag.run(|fut| { tokio::spawn(fut); }).await
|
||||||
|
}).is_ok() {
|
||||||
|
let _ = dag.get(opt_task);
|
||||||
|
let _ = dag.get(result_task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -161,6 +161,51 @@ bench-compare:
|
|||||||
bench-suite suite:
|
bench-suite suite:
|
||||||
cargo bench --bench {{suite}}
|
cargo bench --bench {{suite}}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Fuzzing
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Run a single fuzz target for 60 seconds
|
||||||
|
fuzz target:
|
||||||
|
@echo "Fuzzing {{target}} for 60 seconds..."
|
||||||
|
cargo +nightly fuzz run {{target}} -- -max_total_time=60
|
||||||
|
|
||||||
|
# Run all fuzz targets sequentially for quick smoke test
|
||||||
|
fuzz-all:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
echo "🔬 Running all fuzz targets..."
|
||||||
|
for target in fuzz_dag_execution fuzz_dependencies fuzz_type_safety fuzz_concurrent; do
|
||||||
|
echo ""
|
||||||
|
echo "Running $target..."
|
||||||
|
cargo +nightly fuzz run $target -- -max_total_time=30 -runs=10000
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "✅ All fuzz targets completed"
|
||||||
|
|
||||||
|
# Run extended fuzzing session (for deeper testing)
|
||||||
|
fuzz-extended target:
|
||||||
|
@echo "Running extended fuzzing session for {{target}}..."
|
||||||
|
@echo "⚠️ This will run for 1 hour. Press Ctrl+C to stop early."
|
||||||
|
cargo +nightly fuzz run {{target}} -- -max_total_time=3600
|
||||||
|
|
||||||
|
# Build all fuzz targets
|
||||||
|
fuzz-build:
|
||||||
|
@echo "Building fuzz targets..."
|
||||||
|
cargo +nightly fuzz build
|
||||||
|
@echo "✅ All fuzz targets built"
|
||||||
|
|
||||||
|
# Minimize corpus for a fuzz target
|
||||||
|
fuzz-cmin target:
|
||||||
|
@echo "Minimizing corpus for {{target}}..."
|
||||||
|
cargo +nightly fuzz cmin {{target}}
|
||||||
|
|
||||||
|
# Run property-based tests
|
||||||
|
proptest:
|
||||||
|
@echo "Running property-based tests..."
|
||||||
|
cargo nextest run property_tests --all-features
|
||||||
|
@echo "✅ Property tests passed"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Documentation & Utilities
|
# Documentation & Utilities
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
|||||||
|
0.038627406s DEBUG cargo_mutants::copy_tree: /home/swaits/.local/share/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-mutants-25.3.1/src/copy_tree.rs:69: walk_builder=WalkBuilder { paths: ["/home/swaits/Code/dagx"], ig_builder: IgnoreBuilder { dir: "", overrides: Override(Gitignore { set: GlobSet { len: 0, strats: [] }, root: "", globs: [], num_ignores: 0, num_whitelists: 0, matches: None }), types: Types { defs: [], selections: [], has_selected: false, glob_to_selection: [], set: GlobSet { len: 0, strats: [] }, matches: Pool(Pool { stacks: [CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. }), CacheLine(Mutex { data: [], poisoned: false, .. })], owner: 0, owner_val: UnsafeCell { .. } }) }, explicit_ignores: [], custom_ignore_filenames: [], opts: IgnoreOptions { hidden: false, ignore: false, parents: true, git_global: false, git_ignore: false, git_exclude: false, ignore_case_insensitive: false, require_git: true } }, max_depth: None, max_filesize: None, follow_links: false, threads: 0, skip: None }
|
||||||
|
0.041495573s DEBUG cargo_mutants::copy_tree: /home/swaits/.local/share/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-mutants-25.3.1/src/copy_tree.rs:115: Copied source tree total_bytes=967004 total_files=153 temp_dir="/tmp/cargo-mutants-dagx-o2Mc3y.tmp"
|
||||||
|
0.041758047s DEBUG cargo_mutants::lab: /home/swaits/.local/share/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-mutants-25.3.1/src/lab.rs:55: starting jobserver n_tasks=16
|
||||||
|
0.041839390s DEBUG cargo_mutants::lab: /home/swaits/.local/share/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-mutants-25.3.1/src/lab.rs:267: test_packages=Explicit([Package { name: "dagx", version: "0.3.0", relative_dir: "", top_sources: ["src/lib.rs"] }])
|
||||||
|
0.041870839s DEBUG run{phase=Build}: cargo_mutants::process: /home/swaits/.local/share/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cargo-mutants-25.3.1/src/process.rs:81: start process quoted_argv=/home/swaits/.local/share/rustup/toolchains/1.90.0-x86_64-unknown-linux-gnu/bin/cargo nextest run --no-run --verbose --package=dagx@0.3.0 --all-features
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"cargo_mutants_version": "25.3.1",
|
||||||
|
"start_time": "2025-10-14T16:44:19.225601093Z",
|
||||||
|
"hostname": "swaits-fw13",
|
||||||
|
"username": "swaits"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
Whitespace-only changes.
File diff suppressed because it is too large.
Load diff
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,533 @@
|
|||||||
|
//! 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user