use std::collections::HashSet;
use ahash::{AHashMap, AHashSet};
use crate::{errors::SqliteGraphError, graph::SqliteGraph, progress::ProgressCallback};
use super::scc::{SccResult, strongly_connected_components};
#[derive(Debug, Clone, Default)]
pub struct CycleBasisBounds {
pub max_cycles: Option<usize>,
pub max_cycle_length: Option<usize>,
pub max_per_scc: Option<usize>,
}
impl CycleBasisBounds {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn with_max_cycles(mut self, max: usize) -> Self {
self.max_cycles = Some(max);
self
}
#[inline]
pub fn with_max_cycle_length(mut self, max: usize) -> Self {
self.max_cycle_length = Some(max);
self
}
#[inline]
pub fn with_max_per_scc(mut self, max: usize) -> Self {
self.max_per_scc = Some(max);
self
}
#[inline]
pub fn is_bounded(&self) -> bool {
self.max_cycles.is_some() || self.max_cycle_length.is_some() || self.max_per_scc.is_some()
}
}
#[derive(Debug, Clone)]
pub struct CycleBasisResult {
pub cycles: Vec<Vec<i64>>,
pub scc_decomposition: SccResult,
pub cycles_skipped: usize,
pub bounds_applied: CycleBasisBounds,
}
impl CycleBasisResult {
pub fn cyclic_nodes(&self) -> AHashSet<i64> {
let mut nodes: AHashSet<i64> = self
.cycles
.iter()
.flat_map(|cycle| cycle.iter().copied())
.collect();
for component in &self.scc_decomposition.components {
if component.len() > 1 {
for &node in component {
nodes.insert(node);
}
}
}
nodes
}
pub fn is_cyclic(&self, node: i64) -> bool {
self.cycles.iter().any(|cycle| cycle.contains(&node))
}
pub fn cycles_containing(&self, node: i64) -> Vec<&[i64]> {
self.cycles
.iter()
.filter(|cycle| cycle.contains(&node))
.map(|cycle| cycle.as_slice())
.collect()
}
pub fn explain_cycle(&self, node: i64) -> String {
let cycles = self.cycles_containing(node);
if cycles.is_empty() {
format!("Node {} is not part of any cycle", node)
} else {
format!(
"Node {} is in {} cycle(s):\n{}",
node,
cycles.len(),
cycles
.iter()
.enumerate()
.map(|(i, cyc)| format!(" {}. {:?}", i + 1, cyc))
.collect::<Vec<_>>()
.join("\n")
)
}
}
pub fn cyclic_scc_count(&self) -> usize {
self.scc_decomposition.non_trivial_count()
}
pub fn has_cycles(&self) -> bool {
!self.cycles.is_empty()
}
}
pub fn cycle_basis(graph: &SqliteGraph) -> Result<CycleBasisResult, SqliteGraphError> {
cycle_basis_bounded(graph, CycleBasisBounds::default())
}
pub fn cycle_basis_bounded(
graph: &SqliteGraph,
bounds: CycleBasisBounds,
) -> Result<CycleBasisResult, SqliteGraphError> {
let scc = strongly_connected_components(graph)?;
let mut cycles: Vec<Vec<i64>> = Vec::new();
let mut cycles_skipped = 0usize;
for component in &scc.components {
if component.len() == 1 {
let node = *component.iter().next().unwrap();
if let Ok(outgoing) = graph.fetch_outgoing(node) {
if outgoing.contains(&node) {
if let Some(max_len) = bounds.max_cycle_length {
if 2 > max_len {
cycles_skipped += 1;
} else {
cycles.push(vec![node, node]);
}
} else {
cycles.push(vec![node, node]);
}
}
}
}
}
if let Some(max_cycles) = bounds.max_cycles {
if cycles.len() > max_cycles {
cycles_skipped += cycles.len() - max_cycles;
cycles.truncate(max_cycles);
}
if cycles.len() >= max_cycles {
return Ok(CycleBasisResult {
cycles: deduplicate_cycles(cycles),
scc_decomposition: scc,
cycles_skipped,
bounds_applied: bounds,
});
}
}
let non_trivial_sccs: Vec<&HashSet<i64>> =
scc.components.iter().filter(|c| c.len() > 1).collect();
if non_trivial_sccs.is_empty() {
return Ok(CycleBasisResult {
cycles: deduplicate_cycles(cycles),
scc_decomposition: scc,
cycles_skipped,
bounds_applied: bounds,
});
}
for scc_nodes in non_trivial_sccs {
let scc_cycles = paton_cycles_in_scc(graph, scc_nodes, &bounds, &mut cycles_skipped)?;
if let Some(max_per_scc) = bounds.max_per_scc {
if scc_cycles.len() > max_per_scc {
cycles_skipped += scc_cycles.len() - max_per_scc;
cycles.extend(scc_cycles.into_iter().take(max_per_scc));
} else {
cycles.extend(scc_cycles);
}
} else {
cycles.extend(scc_cycles);
}
if let Some(max_cycles) = bounds.max_cycles {
if cycles.len() >= max_cycles {
cycles_skipped += cycles.len() - max_cycles;
cycles.truncate(max_cycles);
break;
}
}
}
let deduped = deduplicate_cycles(cycles);
Ok(CycleBasisResult {
cycles: deduped,
scc_decomposition: scc,
cycles_skipped,
bounds_applied: bounds,
})
}
pub fn cycle_basis_with_progress<F>(
graph: &SqliteGraph,
bounds: CycleBasisBounds,
progress: &F,
) -> Result<CycleBasisResult, SqliteGraphError>
where
F: ProgressCallback,
{
progress.on_progress(0, Some(4), "Computing SCC decomposition");
let scc = strongly_connected_components(graph)?;
progress.on_progress(1, Some(4), "SCC decomposition complete");
let non_trivial_sccs: Vec<&HashSet<i64>> =
scc.components.iter().filter(|c| c.len() > 1).collect();
let non_trivial_count = non_trivial_sccs.len();
if non_trivial_count == 0 {
progress.on_complete();
return Ok(CycleBasisResult {
cycles: Vec::new(),
scc_decomposition: scc,
cycles_skipped: 0,
bounds_applied: bounds,
});
}
progress.on_progress(
2,
Some(4),
&format!("Finding cycles in {} SCCs", non_trivial_count),
);
let mut all_cycles: Vec<Vec<i64>> = Vec::new();
let mut cycles_skipped = 0usize;
for (idx, scc_nodes) in non_trivial_sccs.iter().enumerate() {
progress.on_progress(
idx,
Some(non_trivial_count),
&format!("Processing SCC {}/{}", idx + 1, non_trivial_count),
);
let scc_cycles = paton_cycles_in_scc(graph, scc_nodes, &bounds, &mut cycles_skipped)?;
if let Some(max_per_scc) = bounds.max_per_scc {
if scc_cycles.len() > max_per_scc {
cycles_skipped += scc_cycles.len() - max_per_scc;
all_cycles.extend(scc_cycles.into_iter().take(max_per_scc));
} else {
all_cycles.extend(scc_cycles);
}
} else {
all_cycles.extend(scc_cycles);
}
if let Some(max_cycles) = bounds.max_cycles {
if all_cycles.len() >= max_cycles {
cycles_skipped += all_cycles.len() - max_cycles;
all_cycles.truncate(max_cycles);
break;
}
}
}
progress.on_progress(3, Some(4), "Deduplicating cycles");
let deduped = deduplicate_cycles(all_cycles);
progress.on_complete();
Ok(CycleBasisResult {
cycles: deduped,
scc_decomposition: scc,
cycles_skipped,
bounds_applied: bounds,
})
}
fn paton_cycles_in_scc(
graph: &SqliteGraph,
scc: &HashSet<i64>,
bounds: &CycleBasisBounds,
cycles_skipped: &mut usize,
) -> Result<Vec<Vec<i64>>, SqliteGraphError> {
let mut cycles: Vec<Vec<i64>> = Vec::new();
let mut visited: AHashSet<i64> = AHashSet::new();
let mut parent: AHashMap<i64, i64> = AHashMap::new();
let mut depth: AHashMap<i64, usize> = AHashMap::new();
let mut rec_stack: AHashSet<i64> = AHashSet::new();
for &node in scc {
if !visited.contains(&node) {
dfs_cycle_search(
graph,
node,
scc,
&mut visited,
&mut parent,
&mut depth,
&mut rec_stack,
&mut cycles,
bounds,
cycles_skipped,
)?;
}
}
Ok(cycles)
}
fn dfs_cycle_search(
graph: &SqliteGraph,
node: i64,
scc: &HashSet<i64>,
visited: &mut AHashSet<i64>,
parent: &mut AHashMap<i64, i64>,
depth: &mut AHashMap<i64, usize>,
rec_stack: &mut AHashSet<i64>,
cycles: &mut Vec<Vec<i64>>,
bounds: &CycleBasisBounds,
cycles_skipped: &mut usize,
) -> Result<(), SqliteGraphError> {
visited.insert(node);
rec_stack.insert(node);
let current_depth = depth.get(&node).copied().unwrap_or(0);
for &neighbor in &graph.fetch_outgoing(node)? {
if !scc.contains(&neighbor) {
continue;
}
if !visited.contains(&neighbor) {
parent.insert(neighbor, node);
depth.insert(neighbor, current_depth + 1);
dfs_cycle_search(
graph,
neighbor,
scc,
visited,
parent,
depth,
rec_stack,
cycles,
bounds,
cycles_skipped,
)?;
} else if rec_stack.contains(&neighbor) {
if let Some(cycle) = extract_cycle_from_back_edge(node, neighbor, parent, depth) {
if let Some(max_len) = bounds.max_cycle_length {
if cycle.len() > max_len {
*cycles_skipped += 1;
continue;
}
}
let canonical = canonicalize_cycle(cycle);
cycles.push(canonical);
}
}
}
rec_stack.remove(&node);
Ok(())
}
fn extract_cycle_from_back_edge(
u: i64,
v: i64,
parent: &AHashMap<i64, i64>,
_depth: &AHashMap<i64, usize>,
) -> Option<Vec<i64>> {
if u == v {
return Some(vec![u, u]);
}
let mut path_u: Vec<i64> = Vec::new();
let mut current = u;
path_u.push(current);
while let Some(&p) = parent.get(¤t) {
path_u.push(p);
current = p;
}
let mut path_v: Vec<i64> = Vec::new();
current = v;
path_v.push(current);
while let Some(&p) = parent.get(¤t) {
path_v.push(p);
current = p;
}
let mut lca = None;
let path_u_set: AHashSet<i64> = path_u.iter().copied().collect();
for &node in path_v.iter().rev() {
if path_u_set.contains(&node) {
lca = Some(node);
break;
}
}
let lca = lca?;
let mut path_lca_to_u: Vec<i64> = Vec::new();
current = u;
while current != lca {
path_lca_to_u.push(current);
current = *parent.get(¤t)?;
}
path_lca_to_u.push(lca); path_lca_to_u.reverse();
let mut path_lca_to_v: Vec<i64> = Vec::new();
current = v;
while current != lca {
path_lca_to_v.push(current);
current = *parent.get(¤t)?;
}
path_lca_to_v.reverse();
let mut cycle = Vec::new();
cycle.extend(&path_lca_to_u); cycle.extend(&path_lca_to_v); cycle.push(lca);
Some(cycle)
}
fn canonicalize_cycle(mut cycle: Vec<i64>) -> Vec<i64> {
if cycle.is_empty() {
return cycle;
}
if cycle.len() == 2 && cycle[0] == cycle[1] {
return cycle; }
if cycle.len() > 1 && cycle[0] == cycle[cycle.len() - 1] {
cycle.pop();
}
if cycle.is_empty() {
return cycle;
}
let min_node = *cycle.iter().min().unwrap();
let min_pos = cycle.iter().position(|&x| x == min_node).unwrap();
cycle.rotate_left(min_pos);
cycle.push(cycle[0]);
cycle
}
fn deduplicate_cycles(cycles: Vec<Vec<i64>>) -> Vec<Vec<i64>> {
let mut seen: AHashSet<Vec<i64>> = AHashSet::new();
let mut result: Vec<Vec<i64>> = Vec::new();
for cycle in cycles {
let canonical = canonicalize_cycle(cycle);
if seen.insert(canonical.clone()) {
result.push(canonical);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GraphEdge, GraphEntity};
fn create_test_graph_with_nodes(count: usize) -> SqliteGraph {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
for i in 0..count {
let entity = GraphEntity {
id: 0,
kind: "test".to_string(),
name: format!("test_{}", i),
file_path: Some(format!("test_{}.rs", i)),
data: serde_json::json!({"index": i}),
};
graph
.insert_entity(&entity)
.expect("Failed to insert entity");
}
graph
}
fn get_entity_ids(graph: &SqliteGraph, count: usize) -> Vec<i64> {
graph
.all_entity_ids()
.expect("Failed to get IDs")
.into_iter()
.take(count)
.collect()
}
fn add_edge(graph: &SqliteGraph, from_idx: i64, to_idx: i64) {
let ids: Vec<i64> = graph.all_entity_ids().expect("Failed to get IDs");
let edge = GraphEdge {
id: 0,
from_id: ids[from_idx as usize],
to_id: ids[to_idx as usize],
edge_type: "edge".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
#[test]
fn test_simple_cycle() {
let graph = create_test_graph_with_nodes(3);
let ids = get_entity_ids(&graph, 3);
for (from, to) in &[(0, 1), (1, 2), (2, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
let result = cycle_basis(&graph).unwrap();
assert!(
result.cycles.len() >= 1,
"Expected at least 1 cycle, found {:?}",
result.cycles
);
let cyclic = result.cyclic_nodes();
assert!(
cyclic.contains(&ids[0]),
"ids[0]={} not in cyclic {:?}",
ids[0],
cyclic
);
assert!(
cyclic.contains(&ids[1]),
"ids[1]={} not in cyclic {:?}",
ids[1],
cyclic
);
assert!(
cyclic.contains(&ids[2]),
"ids[2]={} not in cyclic {:?}",
ids[2],
cyclic
);
}
#[test]
fn test_two_cycles_sharing_edge() {
let graph = create_test_graph_with_nodes(4);
let ids = get_entity_ids(&graph, 4);
let edges = [(0, 1), (1, 2), (2, 0), (0, 2), (2, 3), (3, 0)];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "edge".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
assert!(result.cycles.len() >= 1);
assert!(result.has_cycles());
assert_eq!(result.cyclic_nodes().len(), 4);
}
#[test]
fn test_mutual_recursion() {
let graph = create_test_graph_with_nodes(2);
let ids = get_entity_ids(&graph, 2);
for (from, to) in &[(0, 1), (1, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "recursion".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
let result = cycle_basis(&graph).unwrap();
assert!(
result.cycles.len() >= 1,
"Expected at least 1 cycle, found {:?}",
result.cycles
);
assert!(
result.is_cyclic(ids[0]),
"ids[0]={} should be cyclic",
ids[0]
);
assert!(
result.is_cyclic(ids[1]),
"ids[1]={} should be cyclic",
ids[1]
);
}
#[test]
fn test_multiple_sccs() {
let graph = create_test_graph_with_nodes(5);
let ids = get_entity_ids(&graph, 5);
for (from, to) in &[(0, 1), (1, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "scc1".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
for (from, to) in &[(2, 3), (3, 4), (4, 2)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "scc2".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).expect("Failed to insert edge");
}
let result = cycle_basis(&graph).unwrap();
assert!(
result.cycles.len() >= 1,
"Expected at least 1 cycle, found {:?}",
result.cycles
);
assert_eq!(result.cyclic_scc_count(), 2);
let cyclic = result.cyclic_nodes();
assert_eq!(
cyclic.len(),
5,
"Expected 5 cyclic nodes, found {:?}",
cyclic
);
}
#[test]
fn test_dag_no_cycles() {
let graph = create_test_graph_with_nodes(4);
let ids = get_entity_ids(&graph, 4);
for (from, to) in &[(0, 1), (1, 2), (2, 3)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "chain".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
assert_eq!(result.cycles.len(), 0);
assert!(!result.has_cycles());
assert_eq!(result.cyclic_nodes().len(), 0);
assert_eq!(result.cyclic_scc_count(), 0);
}
#[test]
fn test_complex_interlocking_cycles() {
let graph = create_test_graph_with_nodes(5);
let ids = get_entity_ids(&graph, 5);
let edges = [
(0, 1),
(1, 2),
(2, 0), (0, 3),
(3, 4),
(4, 1), ];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "complex".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
assert!(result.cycles.len() >= 1);
assert_eq!(result.cyclic_scc_count(), 1);
}
#[test]
fn test_self_loop() {
let graph = create_test_graph_with_nodes(1);
let ids = get_entity_ids(&graph, 1);
let edge = GraphEdge {
id: 0,
from_id: ids[0],
to_id: ids[0],
edge_type: "self".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
let result = cycle_basis(&graph).unwrap();
assert_eq!(result.cycles.len(), 1);
assert_eq!(result.cycles[0].len(), 2);
assert_eq!(result.cycles[0][0], result.cycles[0][1]);
}
#[test]
fn test_bounded_max_cycles() {
let graph = create_test_graph_with_nodes(6);
let ids = get_entity_ids(&graph, 6);
for (from, to) in &[(0, 1), (1, 0), (2, 3), (3, 2), (4, 5), (5, 4)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let bounds = CycleBasisBounds {
max_cycles: Some(2),
..Default::default()
};
let result = cycle_basis_bounded(&graph, bounds).unwrap();
assert!(result.cycles.len() <= 2);
}
#[test]
fn test_bounded_max_cycle_length() {
let graph = create_test_graph_with_nodes(5);
let ids = get_entity_ids(&graph, 5);
let edges = [(0, 1), (1, 2), (2, 3), (3, 0)];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "long".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let bounds = CycleBasisBounds {
max_cycle_length: Some(3), ..Default::default()
};
let result = cycle_basis_bounded(&graph, bounds).unwrap();
assert_eq!(result.cycles.len(), 0);
assert!(result.cycles_skipped > 0 || result.cycles.is_empty());
}
#[test]
fn test_bounded_max_per_scc() {
let graph = create_test_graph_with_nodes(5);
let ids = get_entity_ids(&graph, 5);
let edges = [(0, 1), (1, 2), (2, 0), (0, 2), (1, 0)];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "dense".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let bounds = CycleBasisBounds {
max_per_scc: Some(1),
..Default::default()
};
let result = cycle_basis_bounded(&graph, bounds).unwrap();
assert!(result.cycles.len() <= 1);
}
#[test]
fn test_helper_cyclic_nodes() {
let graph = create_test_graph_with_nodes(4);
let ids = get_entity_ids(&graph, 4);
for (from, to) in &[(0, 1), (1, 0), (2, 3), (3, 2)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
let cyclic = result.cyclic_nodes();
assert_eq!(cyclic.len(), 4);
assert!(cyclic.contains(&ids[0]));
assert!(cyclic.contains(&ids[1]));
assert!(cyclic.contains(&ids[2]));
assert!(cyclic.contains(&ids[3]));
}
#[test]
fn test_helper_is_cyclic() {
let graph = create_test_graph_with_nodes(3);
let ids = get_entity_ids(&graph, 3);
for (from, to) in &[(0, 1), (1, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
assert!(result.is_cyclic(ids[0]));
assert!(result.is_cyclic(ids[1]));
assert!(!result.is_cyclic(ids[2])); }
#[test]
fn test_helper_cycles_containing() {
let graph = create_test_graph_with_nodes(4);
let ids = get_entity_ids(&graph, 4);
let edges = [(0, 1), (1, 0), (0, 2), (2, 3), (3, 0)];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
let cycles_with_0 = result.cycles_containing(ids[0]);
assert!(cycles_with_0.len() >= 1);
let cycles_with_1 = result.cycles_containing(ids[1]);
assert!(cycles_with_1.len() >= 1);
}
#[test]
fn test_helper_explain_cycle() {
let graph = create_test_graph_with_nodes(2);
let ids = get_entity_ids(&graph, 2);
for (from, to) in &[(0, 1), (1, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
let explanation = result.explain_cycle(ids[0]);
assert!(explanation.contains(&format!("{}", ids[0])));
assert!(explanation.contains("cycle"));
let no_cycle_explanation = result.explain_cycle(999);
assert!(no_cycle_explanation.contains("not part of any cycle"));
}
#[test]
fn test_cycle_deduplication() {
let graph = create_test_graph_with_nodes(3);
let ids = get_entity_ids(&graph, 3);
let edges = [(0, 1), (1, 2), (2, 0), (0, 2)];
for (from, to) in &edges {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let result = cycle_basis(&graph).unwrap();
assert!(result.cycles.len() <= 2);
}
#[test]
fn test_empty_graph() {
let graph = SqliteGraph::open_in_memory().expect("Failed to create graph");
let result = cycle_basis(&graph).unwrap();
assert_eq!(result.cycles.len(), 0);
assert!(!result.has_cycles());
assert_eq!(result.cycles_skipped, 0);
}
#[test]
fn test_single_node_no_edges() {
let graph = create_test_graph_with_nodes(1);
let result = cycle_basis(&graph).unwrap();
assert_eq!(result.cycles.len(), 0);
assert!(!result.has_cycles());
}
#[test]
fn test_cycle_basis_with_progress() {
use crate::progress::NoProgress;
let graph = create_test_graph_with_nodes(3);
let ids = get_entity_ids(&graph, 3);
for (from, to) in &[(0, 1), (1, 2), (2, 0)] {
let edge = GraphEdge {
id: 0,
from_id: ids[*from],
to_id: ids[*to],
edge_type: "cycle".to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
let progress = NoProgress;
let result =
cycle_basis_with_progress(&graph, CycleBasisBounds::default(), &progress).unwrap();
assert!(result.has_cycles());
}
#[test]
fn test_bounds_builder() {
let bounds = CycleBasisBounds::new()
.with_max_cycles(100)
.with_max_cycle_length(10)
.with_max_per_scc(5);
assert_eq!(bounds.max_cycles, Some(100));
assert_eq!(bounds.max_cycle_length, Some(10));
assert_eq!(bounds.max_per_scc, Some(5));
assert!(bounds.is_bounded());
}
#[test]
fn test_bounds_unbounded() {
let bounds = CycleBasisBounds::default();
assert_eq!(bounds.max_cycles, None);
assert_eq!(bounds.max_cycle_length, None);
assert_eq!(bounds.max_per_scc, None);
assert!(!bounds.is_bounded());
}
}