Skip to content

Commit 30f2907

Browse files
authored
Adding fuzz target for contraction (#1435)
* Adding fuzz target for contraction Added a fuzz target for cycle check validation and idempotency check (calling contract_nodes twice doesn't crash) * Fixing Lint issues * Updates based on comments
1 parent 217d58d commit 30f2907

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed

rustworkx-core/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ path = "fuzz_targets/test_fuzz_random_graph.rs"
4141
test = false
4242
doc = false
4343
bench = false
44+
45+
[[bin]]
46+
name = "test_fuzz_contraction"
47+
path = "fuzz_targets/test_fuzz_contraction.rs"
48+
test = false
49+
doc = false
50+
bench = false
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#![no_main]
2+
3+
use arbitrary::{Arbitrary, Unstructured};
4+
use libfuzzer_sys::fuzz_target;
5+
use rustworkx_core::err::ContractError;
6+
use rustworkx_core::graph_ext::*;
7+
use rustworkx_core::petgraph::graph::NodeIndex;
8+
use rustworkx_core::petgraph::prelude::*;
9+
10+
#[derive(Debug, Arbitrary)]
11+
struct ContractFuzzInput {
12+
edges: Vec<(usize, usize, usize)>,
13+
node_count: usize,
14+
contract_indices: Vec<usize>,
15+
replacement_weight: char,
16+
}
17+
18+
fuzz_target!(|data: &[u8]| {
19+
if let Ok(input) = ContractFuzzInput::arbitrary(&mut Unstructured::new(data)) {
20+
fuzz_contract_nodes(input);
21+
}
22+
});
23+
24+
fn fuzz_contract_nodes(input: ContractFuzzInput) {
25+
if input.node_count == 0 || input.node_count > 500 || input.edges.len() > 5000 {
26+
return;
27+
}
28+
29+
let mut graph: StableDiGraph<char, usize> = StableDiGraph::new();
30+
let mut nodes = Vec::with_capacity(input.node_count);
31+
for i in 0..input.node_count {
32+
let label = (b'a' + ((i % 26) as u8)) as char;
33+
nodes.push(graph.add_node(label));
34+
}
35+
36+
for (u, v, w) in input.edges {
37+
if u < input.node_count && v < input.node_count && w > 0 {
38+
graph.add_edge(nodes[u], nodes[v], w);
39+
}
40+
}
41+
42+
let to_contract: Vec<NodeIndex> = input
43+
.contract_indices
44+
.into_iter()
45+
.filter_map(|i| nodes.get(i).copied())
46+
.collect();
47+
48+
if to_contract.len() < 2 {
49+
return;
50+
}
51+
52+
let mut graph_no_check = graph.clone();
53+
54+
// Run contraction without cycle check (should never fail)
55+
let _ = graph_no_check.contract_nodes(to_contract.clone(), input.replacement_weight, false);
56+
57+
// Run contraction with cycle check, match on the result
58+
#[allow(unreachable_patterns)]
59+
match graph.contract_nodes(to_contract.clone(), input.replacement_weight, true) {
60+
Ok(_) => {
61+
// Idempotency: running again should not panic
62+
let _ = graph.contract_nodes(to_contract, input.replacement_weight, true);
63+
}
64+
Err(ContractError::DAGWouldCycle) => {
65+
// Expected error — no-op
66+
}
67+
Err(err) => {
68+
panic!("Unexpected error during node contraction: {:?}", err);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)