26 KiB
SPARC Phase 4: Refinement - Test-Driven Development Plan
Overview
This phase details the Test-Driven Development (TDD) strategy for implementing the subpolynomial-time dynamic minimum cut algorithm. We follow a rigorous red-green-refactor cycle with comprehensive test coverage.
1. TDD Strategy
1.1 Development Cycles
Cycle Structure:
- RED: Write failing test that specifies desired behavior
- GREEN: Write minimal code to make test pass
- REFACTOR: Improve code quality without changing behavior
- VALIDATE: Run full test suite + benchmarks
1.2 Test-First Order
Implement modules in dependency order:
error.rs- Error types (foundation)graph/representation.rs- Basic graph structurelinkcut/node.rs- Link-cut tree nodeslinkcut/operations.rs- LCT operationstree/decomposition.rs- Hierarchical treealgorithm/insert.rs- Edge insertionalgorithm/delete.rs- Edge deletionalgorithm/query.rs- Cut queriessparsify/sampler.rs- Sparsificationmonitoring/callbacks.rs- Monitoring system
2. Unit Tests
2.1 Error Handling Tests
File: tests/unit/error_tests.rs
#[cfg(test)]
mod error_tests {
use super::*;
#[test]
fn test_invalid_vertex_error() {
let err = MinCutError::InvalidVertex(999);
assert_eq!(err.to_string(), "Invalid vertex ID: 999");
}
#[test]
fn test_edge_not_found_error() {
let err = MinCutError::EdgeNotFound(1, 2);
assert!(err.to_string().contains("Edge (1, 2) does not exist"));
}
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MinCutError>();
}
}
2.2 Graph Representation Tests
File: tests/unit/graph_tests.rs
#[cfg(test)]
mod graph_tests {
use ruvector_mincut::graph::*;
#[test]
fn test_empty_graph() {
let graph = DynamicGraph::new(0);
assert_eq!(graph.vertex_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_add_edge() {
let mut graph = DynamicGraph::new(3);
assert!(graph.add_edge(0, 1).is_ok());
assert_eq!(graph.edge_count(), 1);
assert!(graph.has_edge(0, 1));
assert!(graph.has_edge(1, 0)); // Undirected
}
#[test]
fn test_add_duplicate_edge() {
let mut graph = DynamicGraph::new(3);
graph.add_edge(0, 1).unwrap();
let result = graph.add_edge(0, 1);
assert!(result.is_err());
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_remove_edge() {
let mut graph = DynamicGraph::new(3);
graph.add_edge(0, 1).unwrap();
graph.add_edge(1, 2).unwrap();
assert!(graph.remove_edge(0, 1).is_ok());
assert_eq!(graph.edge_count(), 1);
assert!(!graph.has_edge(0, 1));
}
#[test]
fn test_remove_nonexistent_edge() {
let mut graph = DynamicGraph::new(3);
let result = graph.remove_edge(0, 1);
assert!(result.is_err());
}
#[test]
fn test_neighbors() {
let mut graph = DynamicGraph::new(4);
graph.add_edge(0, 1).unwrap();
graph.add_edge(0, 2).unwrap();
graph.add_edge(0, 3).unwrap();
let neighbors: Vec<_> = graph.neighbors(0).collect();
assert_eq!(neighbors.len(), 3);
assert!(neighbors.contains(&1));
assert!(neighbors.contains(&2));
assert!(neighbors.contains(&3));
}
#[test]
fn test_degree() {
let mut graph = DynamicGraph::new(4);
graph.add_edge(0, 1).unwrap();
graph.add_edge(0, 2).unwrap();
assert_eq!(graph.degree(0), 2);
assert_eq!(graph.degree(1), 1);
assert_eq!(graph.degree(2), 1);
assert_eq!(graph.degree(3), 0);
}
}
2.3 Link-Cut Tree Tests
File: tests/unit/linkcut_tests.rs
#[cfg(test)]
mod linkcut_tests {
use ruvector_mincut::linkcut::*;
#[test]
fn test_make_tree() {
let mut lct = LinkCutTree::new();
lct.make_tree(0);
lct.make_tree(1);
assert!(!lct.connected(0, 1));
}
#[test]
fn test_link() {
let mut lct = LinkCutTree::new();
lct.make_tree(0);
lct.make_tree(1);
lct.link(0, 1);
assert!(lct.connected(0, 1));
}
#[test]
fn test_cut() {
let mut lct = LinkCutTree::new();
lct.make_tree(0);
lct.make_tree(1);
lct.link(0, 1);
lct.cut(0, 1);
assert!(!lct.connected(0, 1));
}
#[test]
fn test_connected_transitive() {
let mut lct = LinkCutTree::new();
for i in 0..5 {
lct.make_tree(i);
}
// Build path: 0-1-2-3-4
lct.link(0, 1);
lct.link(1, 2);
lct.link(2, 3);
lct.link(3, 4);
assert!(lct.connected(0, 4));
assert!(lct.connected(1, 3));
}
#[test]
fn test_lca() {
let mut lct = LinkCutTree::new();
for i in 0..7 {
lct.make_tree(i);
}
// Build tree:
// 0
// / \
// 1 2
// / \
// 3 4
// / \
// 5 6
lct.link(1, 0);
lct.link(2, 0);
lct.link(3, 1);
lct.link(4, 1);
lct.link(5, 4);
lct.link(6, 4);
assert_eq!(lct.lca(3, 4), 1);
assert_eq!(lct.lca(3, 2), 0);
assert_eq!(lct.lca(5, 6), 4);
}
#[test]
fn test_path_aggregate() {
let mut lct = LinkCutTree::new();
for i in 0..4 {
lct.make_tree(i);
}
lct.link(0, 1);
lct.link(1, 2);
lct.link(2, 3);
// Test aggregate queries on path
let path_size = lct.path_aggregate(0, 3, |agg| agg.size);
assert_eq!(path_size, 4);
}
}
2.4 Decomposition Tree Tests
File: tests/unit/decomposition_tests.rs
#[cfg(test)]
mod decomposition_tests {
use ruvector_mincut::tree::*;
#[test]
fn test_empty_tree() {
let tree = DecompositionTree::new();
assert_eq!(tree.height(), 0);
}
#[test]
fn test_build_from_graph() {
let mut graph = DynamicGraph::new(4);
graph.add_edge(0, 1).unwrap();
graph.add_edge(1, 2).unwrap();
graph.add_edge(2, 3).unwrap();
graph.add_edge(3, 0).unwrap();
let tree = DecompositionTree::from_graph(&graph);
assert_eq!(tree.height(), 2); // log_2(4) = 2
assert_eq!(tree.leaf_count(), 4);
}
#[test]
fn test_find_leaf() {
let mut graph = DynamicGraph::new(4);
graph.add_edge(0, 1).unwrap();
graph.add_edge(2, 3).unwrap();
let tree = DecompositionTree::from_graph(&graph);
let leaf_0 = tree.find_leaf(0);
assert!(leaf_0.is_some());
assert_eq!(leaf_0.unwrap().vertices(), &[0]);
}
#[test]
fn test_local_cut_values() {
// Create complete graph K4
let mut graph = DynamicGraph::new(4);
for i in 0..4 {
for j in i+1..4 {
graph.add_edge(i, j).unwrap();
}
}
let tree = DecompositionTree::from_graph(&graph);
// In K4, minimum cut is 3 (any single vertex)
let root = tree.root();
assert_eq!(root.local_cut(), 3);
}
}
3. Integration Tests
3.1 End-to-End Dynamic Updates
File: tests/integration/dynamic_updates_test.rs
#[cfg(test)]
mod dynamic_updates_tests {
use ruvector_mincut::*;
#[test]
fn test_insert_edges_sequential() {
let mut mincut = DynamicMinCut::new(MinCutConfig::default());
// Build path graph: 0-1-2-3
mincut.insert_edge(0, 1).unwrap();
assert_eq!(mincut.min_cut_value(), 1);
mincut.insert_edge(1, 2).unwrap();
assert_eq!(mincut.min_cut_value(), 1);
mincut.insert_edge(2, 3).unwrap();
assert_eq!(mincut.min_cut_value(), 1);
}
#[test]
fn test_delete_edges_creates_bottleneck() {
let mut mincut = DynamicMinCut::new(MinCutConfig::default());
// Build K4
for i in 0..4 {
for j in i+1..4 {
mincut.insert_edge(i, j).unwrap();
}
}
assert_eq!(mincut.min_cut_value(), 3);
// Remove edges to create bottleneck
mincut.delete_edge(0, 2).unwrap();
mincut.delete_edge(0, 3).unwrap();
mincut.delete_edge(1, 2).unwrap();
mincut.delete_edge(1, 3).unwrap();
// Now min cut is 1 (between {0,1} and {2,3})
assert_eq!(mincut.min_cut_value(), 1);
}
#[test]
fn test_partition_correctness() {
let mut mincut = DynamicMinCut::new(MinCutConfig::default());
// Build dumbbell graph: K3 - single edge - K3
// Left clique
mincut.insert_edge(0, 1).unwrap();
mincut.insert_edge(1, 2).unwrap();
mincut.insert_edge(2, 0).unwrap();
// Bridge
mincut.insert_edge(2, 3).unwrap();
// Right clique
mincut.insert_edge(3, 4).unwrap();
mincut.insert_edge(4, 5).unwrap();
mincut.insert_edge(5, 3).unwrap();
let result = mincut.min_cut();
assert_eq!(result.value, 1);
assert_eq!(result.cut_edges.len(), 1);
assert!(result.cut_edges.contains(&(2, 3)) ||
result.cut_edges.contains(&(3, 2)));
// Verify partition sizes
assert_eq!(result.partition_a.len(), 3);
assert_eq!(result.partition_b.len(), 3);
}
}
3.2 Correctness Verification
File: tests/integration/correctness_test.rs
#[cfg(test)]
mod correctness_tests {
use ruvector_mincut::*;
use ruvector_mincut::testing::*;
#[test]
fn test_against_stoer_wagner() {
// Verify our algorithm matches Stoer-Wagner on various graphs
for size in [10, 20, 50, 100] {
for density in [0.1, 0.3, 0.5, 0.7] {
let graph = generate_random_graph(size, density);
let mut mincut = DynamicMinCut::from_graph(&graph, Default::default());
let our_result = mincut.min_cut_value();
let stoer_wagner = brute_force_mincut(&graph);
assert_eq!(our_result, stoer_wagner,
"Mismatch for n={}, density={}", size, density);
}
}
}
#[test]
fn test_random_update_sequences() {
use rand::Rng;
let mut rng = rand::thread_rng();
for trial in 0..100 {
let mut mincut = DynamicMinCut::new(MinCutConfig::default());
let n = 20;
// Random sequence of 100 updates
for _ in 0..100 {
let u = rng.gen_range(0..n);
let v = rng.gen_range(0..n);
if u == v { continue; }
if rng.gen_bool(0.5) {
// Insert
mincut.insert_edge(u, v).ok();
} else {
// Delete
mincut.delete_edge(u, v).ok();
}
// Verify invariants
#[cfg(debug_assertions)]
mincut.validate().unwrap();
}
// Final verification against ground truth
let computed = mincut.min_cut_value();
let actual = brute_force_mincut(&mincut.graph);
assert_eq!(computed, actual, "Trial {} failed", trial);
}
}
}
3.3 Sparsification Tests
File: tests/integration/sparsification_test.rs
#[cfg(test)]
mod sparsification_tests {
use ruvector_mincut::*;
#[test]
fn test_approximate_within_epsilon() {
let epsilon = 0.1;
let config = MinCutConfig {
epsilon,
use_sparsification: true,
..Default::default()
};
for size in [100, 500, 1000] {
let graph = generate_random_graph(size, 0.1);
let mut mincut = DynamicMinCut::from_graph(&graph, config);
let approximate = mincut.min_cut_value() as f64;
let exact = brute_force_mincut(&graph) as f64;
let ratio = approximate / exact;
assert!(ratio >= 1.0 - epsilon && ratio <= 1.0 + epsilon,
"Approximation ratio {} outside bounds for n={}", ratio, size);
}
}
#[test]
fn test_sparse_graph_size() {
let epsilon = 0.05;
let config = MinCutConfig {
epsilon,
use_sparsification: true,
..Default::default()
};
let n = 1000;
let graph = generate_random_graph(n, 0.5); // Dense graph
let mut mincut = DynamicMinCut::from_graph(&graph, config);
// Sparse graph should have O(n log n / ε²) edges
let sparse_size = mincut.sparse_graph_size();
let expected_max = ((n as f64) * (n as f64).ln() / (epsilon * epsilon)) as usize;
assert!(sparse_size < expected_max * 2,
"Sparse graph too large: {} > {}", sparse_size, expected_max);
}
}
4. Performance Tests
4.1 Benchmark Suite
File: benches/mincut_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use ruvector_mincut::*;
fn bench_insert_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("insert_operations");
for size in [100, 1000, 10000].iter() {
group.bench_with_input(
BenchmarkId::new("insert_edge", size),
size,
|b, &n| {
b.iter_batched(
|| {
let mut mincut = DynamicMinCut::new(Default::default());
let updates = generate_random_insertions(n, 0.1);
(mincut, updates)
},
|(mut mincut, updates)| {
for (u, v) in updates {
black_box(mincut.insert_edge(u, v).ok());
}
},
criterion::BatchSize::SmallInput
);
}
);
}
group.finish();
}
fn bench_delete_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("delete_operations");
for size in [100, 1000, 10000].iter() {
group.bench_with_input(
BenchmarkId::new("delete_edge", size),
size,
|b, &n| {
b.iter_batched(
|| {
let graph = generate_random_graph(n, 0.1);
let mut mincut = DynamicMinCut::from_graph(&graph, Default::default());
let edges: Vec<_> = graph.edges().collect();
(mincut, edges)
},
|(mut mincut, edges)| {
for (u, v) in edges.iter().take(100) {
black_box(mincut.delete_edge(*u, *v).ok());
}
},
criterion::BatchSize::SmallInput
);
}
);
}
group.finish();
}
fn bench_query_operations(c: &mut Criterion) {
let mut group = c.benchmark_group("query_operations");
for size in [100, 1000, 10000].iter() {
group.bench_with_input(
BenchmarkId::new("min_cut_value", size),
size,
|b, &n| {
let graph = generate_random_graph(n, 0.1);
let mincut = DynamicMinCut::from_graph(&graph, Default::default());
b.iter(|| {
black_box(mincut.min_cut_value());
});
}
);
group.bench_with_input(
BenchmarkId::new("min_cut_partition", size),
size,
|b, &n| {
let graph = generate_random_graph(n, 0.1);
let mincut = DynamicMinCut::from_graph(&graph, Default::default());
b.iter(|| {
black_box(mincut.min_cut());
});
}
);
}
group.finish();
}
fn bench_mixed_workload(c: &mut Criterion) {
let mut group = c.benchmark_group("mixed_workload");
for size in [100, 1000, 10000].iter() {
group.bench_with_input(
BenchmarkId::new("realistic_workload", size),
size,
|b, &n| {
b.iter_batched(
|| {
let graph = generate_random_graph(n, 0.05);
let mut mincut = DynamicMinCut::from_graph(&graph, Default::default());
let ops = generate_mixed_operations(1000, n);
(mincut, ops)
},
|(mut mincut, ops)| {
for op in ops {
match op {
Op::Insert(u, v) => {
black_box(mincut.insert_edge(u, v).ok());
},
Op::Delete(u, v) => {
black_box(mincut.delete_edge(u, v).ok());
},
Op::Query => {
black_box(mincut.min_cut_value());
}
}
}
},
criterion::BatchSize::SmallInput
);
}
);
}
group.finish();
}
criterion_group!(
benches,
bench_insert_operations,
bench_delete_operations,
bench_query_operations,
bench_mixed_workload
);
criterion_main!(benches);
4.2 Performance Regression Tests
File: tests/performance/regression_test.rs
#[cfg(test)]
mod performance_regression_tests {
use ruvector_mincut::*;
use std::time::Instant;
#[test]
#[ignore] // Run with: cargo test --release -- --ignored
fn test_update_time_subpolynomial() {
let n = 10_000;
let num_updates = 10_000;
let mut mincut = DynamicMinCut::new(Default::default());
let updates = generate_random_mixed_operations(num_updates, n);
let start = Instant::now();
for op in updates {
match op {
Op::Insert(u, v) => mincut.insert_edge(u, v).ok(),
Op::Delete(u, v) => mincut.delete_edge(u, v).ok(),
_ => continue,
};
}
let duration = start.elapsed();
let avg_ns = duration.as_nanos() / num_updates as u128;
let avg_ms = avg_ns as f64 / 1_000_000.0;
println!("Average update time: {:.3} ms", avg_ms);
// Target: <10ms per update for n=10,000
assert!(avg_ms < 10.0,
"Update time {} ms exceeds target of 10 ms", avg_ms);
}
#[test]
#[ignore]
fn test_query_time_constant() {
let n = 10_000;
let graph = generate_random_graph(n, 0.1);
let mincut = DynamicMinCut::from_graph(&graph, Default::default());
let num_queries = 100_000;
let start = Instant::now();
for _ in 0..num_queries {
black_box(mincut.min_cut_value());
}
let duration = start.elapsed();
let avg_ns = duration.as_nanos() / num_queries;
println!("Average query time: {} ns", avg_ns);
// Target: <100ns per query (essentially O(1))
assert!(avg_ns < 100,
"Query time {} ns exceeds target of 100 ns", avg_ns);
}
#[test]
#[ignore]
fn test_throughput_target() {
let n = 10_000;
let mut mincut = DynamicMinCut::new(Default::default());
let updates = generate_random_mixed_operations(10_000, n);
let start = Instant::now();
for op in updates {
match op {
Op::Insert(u, v) => mincut.insert_edge(u, v).ok(),
Op::Delete(u, v) => mincut.delete_edge(u, v).ok(),
_ => continue,
};
}
let duration = start.elapsed();
let throughput = 10_000.0 / duration.as_secs_f64();
println!("Throughput: {:.0} updates/second", throughput);
// Target: >1,000 updates/second for n=10,000
assert!(throughput > 1000.0,
"Throughput {} ops/s below target of 1000 ops/s", throughput);
}
}
5. Property-Based Testing
File: tests/property/quickcheck_tests.rs
use quickcheck::{quickcheck, TestResult};
use ruvector_mincut::*;
#[cfg(test)]
mod property_tests {
use super::*;
#[quickcheck]
fn prop_cut_value_nonnegative(ops: Vec<GraphOp>) -> TestResult {
if ops.len() > 1000 {
return TestResult::discard();
}
let mut mincut = DynamicMinCut::new(Default::default());
for op in ops {
apply_operation(&mut mincut, op);
}
TestResult::from_bool(mincut.min_cut_value() >= 0)
}
#[quickcheck]
fn prop_cut_bounded_by_min_degree(ops: Vec<GraphOp>) -> TestResult {
if ops.len() > 500 {
return TestResult::discard();
}
let mut mincut = DynamicMinCut::new(Default::default());
for op in ops {
apply_operation(&mut mincut, op);
}
let cut_value = mincut.min_cut_value();
let min_degree = compute_min_degree(&mincut.graph);
TestResult::from_bool(cut_value <= min_degree)
}
#[quickcheck]
fn prop_insert_delete_inverse(u: u32, v: u32) -> TestResult {
if u == v || u > 100 || v > 100 {
return TestResult::discard();
}
let mut mincut = DynamicMinCut::new(Default::default());
let cut_before = mincut.min_cut_value();
mincut.insert_edge(u, v).ok();
let cut_after_insert = mincut.min_cut_value();
mincut.delete_edge(u, v).ok();
let cut_after_delete = mincut.min_cut_value();
TestResult::from_bool(cut_before == cut_after_delete)
}
}
6. Fuzzing
File: fuzz/fuzz_targets/mincut_fuzz.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use ruvector_mincut::*;
fuzz_target!(|data: &[u8]| {
if data.len() < 10 {
return;
}
let mut mincut = DynamicMinCut::new(Default::default());
let n = 100;
let mut i = 0;
while i + 2 < data.len() {
let u = (data[i] as usize) % n;
let v = (data[i+1] as usize) % n;
let op_type = data[i+2] % 3;
match op_type {
0 => {
// Insert
mincut.insert_edge(u, v).ok();
},
1 => {
// Delete
mincut.delete_edge(u, v).ok();
},
2 => {
// Query
let _ = mincut.min_cut_value();
},
_ => unreachable!()
}
// Verify invariants
#[cfg(debug_assertions)]
mincut.validate().expect("Invariant violated");
i += 3;
}
});
7. Test Coverage Goals
7.1 Coverage Targets
- Unit tests: >90% line coverage
- Integration tests: >80% branch coverage
- Property tests: >100 successful runs per property
- Fuzzing: >1 million iterations without crash
7.2 Coverage Measurement
# Install coverage tool
cargo install cargo-tarpaulin
# Run coverage
cargo tarpaulin --out Html --output-dir coverage/
# View coverage report
open coverage/index.html
8. Continuous Integration
8.1 CI Pipeline
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run unit tests
run: cargo test --lib
- name: Run integration tests
run: cargo test --test '*'
- name: Run benchmarks (sanity check)
run: cargo bench --no-run
- name: Check code coverage
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --out Lcov
- name: Upload coverage
uses: codecov/codecov-action@v2
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run performance tests
run: cargo test --release -- --ignored
9. Test Data Generation
File: tests/fixtures/test_graphs.rs
use rand::Rng;
pub fn generate_random_graph(n: usize, density: f64) -> DynamicGraph {
let mut graph = DynamicGraph::new(n);
let mut rng = rand::thread_rng();
for u in 0..n {
for v in u+1..n {
if rng.gen_bool(density) {
graph.add_edge(u, v).unwrap();
}
}
}
graph
}
pub fn generate_complete_graph(n: usize) -> DynamicGraph {
let mut graph = DynamicGraph::new(n);
for u in 0..n {
for v in u+1..n {
graph.add_edge(u, v).unwrap();
}
}
graph
}
pub fn generate_path_graph(n: usize) -> DynamicGraph {
let mut graph = DynamicGraph::new(n);
for i in 0..n-1 {
graph.add_edge(i, i+1).unwrap();
}
graph
}
pub fn generate_cycle_graph(n: usize) -> DynamicGraph {
let mut graph = DynamicGraph::new(n);
for i in 0..n {
graph.add_edge(i, (i+1) % n).unwrap();
}
graph
}
pub fn generate_dumbbell_graph(clique_size: usize) -> DynamicGraph {
let n = clique_size * 2;
let mut graph = DynamicGraph::new(n);
// Left clique
for u in 0..clique_size {
for v in u+1..clique_size {
graph.add_edge(u, v).unwrap();
}
}
// Bridge
graph.add_edge(clique_size-1, clique_size).unwrap();
// Right clique
for u in clique_size..n {
for v in u+1..n {
graph.add_edge(u, v).unwrap();
}
}
graph
}
10. Validation & Debugging
10.1 Invariant Checking
impl DynamicMinCut {
#[cfg(debug_assertions)]
pub fn validate(&self) -> Result<()> {
// Check tree structure
self.tree.validate()?;
// Check LCT consistency
self.lct.validate()?;
// Verify cut value
let computed = self.current_cut;
let actual = self.compute_min_cut_brute_force();
if computed != actual {
return Err(MinCutError::InvariantViolation(
format!("Cut value mismatch: {} != {}", computed, actual)
));
}
Ok(())
}
}
Next Phase: Proceed to 05-completion.md for integration, deployment, and documentation.