use ahash::AHashSet;
use crate::{errors::SqliteGraphError, graph::SqliteGraph, progress::ProgressCallback};
use super::graph_similarity::{SimilarityBounds, structural_similarity};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeDelta {
pub nodes_added: AHashSet<i64>,
pub nodes_removed: AHashSet<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdgeDelta {
pub edges_added: Vec<(i64, i64)>,
pub edges_removed: Vec<(i64, i64)>,
}
#[derive(Debug, Clone)]
pub struct GraphDiffResult {
pub nodes_added: AHashSet<i64>,
pub nodes_removed: AHashSet<i64>,
pub edges_added: Vec<(i64, i64)>,
pub edges_removed: Vec<(i64, i64)>,
pub similarity_score: f64,
pub is_isomorphic: bool,
pub graph_edit_distance: f64,
pub graph1_size: usize,
pub graph2_size: usize,
}
impl GraphDiffResult {
#[inline]
pub fn is_safe(&self) -> bool {
self.nodes_removed.is_empty() && self.similarity_score >= 0.8
}
#[inline]
pub fn has_breaking_changes(&self) -> bool {
!self.nodes_removed.is_empty() || self.similarity_score < 0.5
}
pub fn summary(&self) -> String {
format!(
"Added {} nodes, removed {} nodes. Added {} edges, removed {} edges. Similarity: {:.2}",
self.nodes_added.len(),
self.nodes_removed.len(),
self.edges_added.len(),
self.edges_removed.len(),
self.similarity_score
)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.nodes_added.is_empty()
&& self.nodes_removed.is_empty()
&& self.edges_added.is_empty()
&& self.edges_removed.is_empty()
}
#[inline]
pub fn total_changes(&self) -> usize {
self.nodes_added.len()
+ self.nodes_removed.len()
+ self.edges_added.len()
+ self.edges_removed.len()
}
}
fn compute_node_delta(
graph1: &SqliteGraph,
graph2: &SqliteGraph,
) -> Result<NodeDelta, SqliteGraphError> {
let nodes1: AHashSet<i64> = graph1.all_entity_ids()?.into_iter().collect();
let nodes2: AHashSet<i64> = graph2.all_entity_ids()?.into_iter().collect();
let nodes_added: AHashSet<i64> = nodes2.difference(&nodes1).copied().collect();
let nodes_removed: AHashSet<i64> = nodes1.difference(&nodes2).copied().collect();
Ok(NodeDelta {
nodes_added,
nodes_removed,
})
}
fn compute_edge_delta(
graph1: &SqliteGraph,
graph2: &SqliteGraph,
) -> Result<EdgeDelta, SqliteGraphError> {
let mut edges1: AHashSet<(i64, i64)> = AHashSet::new();
let mut edges2: AHashSet<(i64, i64)> = AHashSet::new();
for &from_id in &graph1.all_entity_ids()? {
if let Ok(outgoing) = graph1.fetch_outgoing(from_id) {
for &to_id in &outgoing {
edges1.insert((from_id, to_id));
}
}
}
for &from_id in &graph2.all_entity_ids()? {
if let Ok(outgoing) = graph2.fetch_outgoing(from_id) {
for &to_id in &outgoing {
edges2.insert((from_id, to_id));
}
}
}
let edges_added: Vec<(i64, i64)> = edges2.difference(&edges1).copied().collect();
let edges_removed: Vec<(i64, i64)> = edges1.difference(&edges2).copied().collect();
Ok(EdgeDelta {
edges_added,
edges_removed,
})
}
pub fn graph_diff(
graph1: &SqliteGraph,
graph2: &SqliteGraph,
) -> Result<GraphDiffResult, SqliteGraphError> {
let node_delta = compute_node_delta(graph1, graph2)?;
let edge_delta = compute_edge_delta(graph1, graph2)?;
let similarity = structural_similarity(graph1, graph2, SimilarityBounds::default())?;
let graph1_size = graph1.all_entity_ids()?.len();
let graph2_size = graph2.all_entity_ids()?.len();
Ok(GraphDiffResult {
nodes_added: node_delta.nodes_added,
nodes_removed: node_delta.nodes_removed,
edges_added: edge_delta.edges_added,
edges_removed: edge_delta.edges_removed,
similarity_score: similarity.mcs_similarity,
is_isomorphic: similarity.isomorphic,
graph_edit_distance: similarity.ged_distance,
graph1_size,
graph2_size,
})
}
pub fn graph_diff_with_progress<F>(
graph1: &SqliteGraph,
graph2: &SqliteGraph,
progress: &F,
) -> Result<GraphDiffResult, SqliteGraphError>
where
F: ProgressCallback,
{
progress.on_progress(0, Some(5), "Computing node delta...");
let node_delta = compute_node_delta(graph1, graph2)?;
progress.on_progress(
1,
Some(5),
&format!(
"Found {} nodes added, {} nodes removed",
node_delta.nodes_added.len(),
node_delta.nodes_removed.len()
),
);
progress.on_progress(2, Some(5), "Computing edge delta...");
let edge_delta = compute_edge_delta(graph1, graph2)?;
progress.on_progress(
3,
Some(5),
&format!(
"Found {} edges added, {} edges removed",
edge_delta.edges_added.len(),
edge_delta.edges_removed.len()
),
);
progress.on_progress(4, Some(5), "Computing structural similarity...");
let similarity =
structural_similarity_with_progress(graph1, graph2, SimilarityBounds::default(), progress)?;
let graph1_size = graph1.all_entity_ids()?.len();
let graph2_size = graph2.all_entity_ids()?.len();
progress.on_progress(
5,
Some(5),
&format!("Similarity score: {:.2}", similarity.mcs_similarity),
);
progress.on_complete();
Ok(GraphDiffResult {
nodes_added: node_delta.nodes_added,
nodes_removed: node_delta.nodes_removed,
edges_added: edge_delta.edges_added,
edges_removed: edge_delta.edges_removed,
similarity_score: similarity.mcs_similarity,
is_isomorphic: similarity.isomorphic,
graph_edit_distance: similarity.ged_distance,
graph1_size,
graph2_size,
})
}
use super::graph_similarity::structural_similarity_with_progress;
#[derive(Debug, Clone)]
pub struct RefactorValidation {
pub is_safe: bool,
pub breaking_changes: Vec<String>,
pub warnings: Vec<String>,
}
impl RefactorValidation {
#[inline]
pub fn is_clean(&self) -> bool {
self.breaking_changes.is_empty() && self.warnings.is_empty()
}
pub fn summary(&self) -> String {
if self.is_safe {
if self.warnings.is_empty() {
"Refactor is safe - no breaking changes".to_string()
} else {
format!(
"Refactor is safe with warnings:\n{}",
self.warnings
.iter()
.map(|w| format!(" - {}", w))
.collect::<Vec<_>>()
.join("\n")
)
}
} else {
format!(
"Refactor has breaking changes:\n{}\nWarnings:\n{}",
self.breaking_changes
.iter()
.map(|c| format!(" - {}", c))
.collect::<Vec<_>>()
.join("\n"),
self.warnings
.iter()
.map(|w| format!(" - {}", w))
.collect::<Vec<_>>()
.join("\n")
)
}
}
}
pub fn validate_refactor(diff: &GraphDiffResult) -> RefactorValidation {
let mut validation = RefactorValidation {
is_safe: true,
breaking_changes: Vec::new(),
warnings: Vec::new(),
};
if !diff.nodes_removed.is_empty() {
validation.breaking_changes.push(format!(
"Removed {} nodes - potentially breaking",
diff.nodes_removed.len()
));
validation.is_safe = false;
}
if diff.similarity_score < 0.5 {
validation.breaking_changes.push(format!(
"Low similarity score: {:.2} - significant structural changes",
diff.similarity_score
));
validation.is_safe = false;
} else if diff.similarity_score < 0.8 {
validation.warnings.push(format!(
"Moderate similarity: {:.2} - review recommended",
diff.similarity_score
));
}
if diff.is_isomorphic {
validation
.warnings
.push("Structure preserved (isomorphic)".to_string());
}
if !diff.edges_removed.is_empty() {
validation.warnings.push(format!(
"Removed {} edges - review control flow impact",
diff.edges_removed.len()
));
}
validation
}
#[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();
}
fn add_typed_edge(graph: &SqliteGraph, from_idx: i64, to_idx: i64, edge_type: &str) {
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_type.to_string(),
data: serde_json::json!({}),
};
graph.insert_edge(&edge).ok();
}
#[test]
fn test_graph_diff_identical() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.nodes_added.is_empty());
assert!(diff.nodes_removed.is_empty());
assert!(diff.edges_added.is_empty());
assert!(diff.edges_removed.is_empty());
assert_eq!(diff.similarity_score, 1.0);
assert!(diff.is_isomorphic);
assert!(diff.is_empty());
assert_eq!(diff.total_changes(), 0);
}
#[test]
fn test_graph_diff_no_changes() {
let graph1 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
let diff = graph_diff(&graph1, &graph1).unwrap();
assert!(diff.is_empty());
assert_eq!(diff.similarity_score, 1.0);
assert!(diff.is_isomorphic);
}
#[test]
fn test_graph_diff_empty_graphs() {
let graph1 = SqliteGraph::open_in_memory().expect("Failed to create graph");
let graph2 = SqliteGraph::open_in_memory().expect("Failed to create graph");
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.is_empty());
assert_eq!(diff.similarity_score, 1.0);
assert!(diff.is_isomorphic);
assert_eq!(diff.graph_edit_distance, 0.0);
}
#[test]
fn test_graph_diff_one_empty() {
let graph1 = SqliteGraph::open_in_memory().expect("Failed to create graph");
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert_eq!(diff.nodes_added.len(), 3);
assert!(diff.nodes_removed.is_empty());
assert_eq!(diff.similarity_score, 0.0);
assert!(!diff.is_isomorphic);
assert!(diff.has_breaking_changes());
}
#[test]
fn test_node_delta_added() {
let graph1 = create_test_graph_with_nodes(2);
let graph2 = create_test_graph_with_nodes(4);
add_edge(&graph1, 0, 1);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
add_edge(&graph2, 2, 3);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert_eq!(diff.nodes_added.len(), 2);
assert!(diff.nodes_removed.is_empty());
assert_eq!(diff.edges_added.len(), 2);
}
#[test]
fn test_node_delta_removed() {
let graph1 = create_test_graph_with_nodes(4);
let graph2 = create_test_graph_with_nodes(2);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 2, 3);
add_edge(&graph2, 0, 1);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.nodes_added.is_empty());
assert_eq!(diff.nodes_removed.len(), 2);
assert_eq!(diff.edges_removed.len(), 2);
assert!(diff.has_breaking_changes());
}
#[test]
fn test_node_delta_mixed() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(4);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.total_changes() > 0);
}
#[test]
fn test_edge_delta_added() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
add_edge(&graph2, 0, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.total_changes() > 0 || diff.edges_added.len() > 0);
}
#[test]
fn test_edge_delta_removed() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 0, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.total_changes() > 0);
}
#[test]
fn test_edge_delta_no_change() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.is_isomorphic);
assert_eq!(diff.similarity_score, 1.0);
}
#[test]
fn test_diff_with_similarity() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert_eq!(diff.similarity_score, 1.0);
assert_eq!(diff.graph_edit_distance, 0.0);
}
#[test]
fn test_diff_isomorphic_flag() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.is_isomorphic);
}
#[test]
fn test_diff_ged_distance() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let expected_ged = 1.0 - diff.similarity_score;
assert!((diff.graph_edit_distance - expected_ged).abs() < 0.01);
}
#[test]
fn test_graph_diff_with_progress() {
use crate::progress::NoProgress;
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let progress = NoProgress;
let diff = graph_diff_with_progress(&graph1, &graph2, &progress).unwrap();
assert!(diff.is_isomorphic);
assert_eq!(diff.similarity_score, 1.0);
}
#[test]
fn test_diff_large_graphs() {
let graph1 = create_test_graph_with_nodes(100);
let graph2 = create_test_graph_with_nodes(100);
for i in 0..99 {
add_edge(&graph1, i, i + 1);
add_edge(&graph2, i, i + 1);
}
let start = std::time::Instant::now();
let diff = graph_diff(&graph1, &graph2).unwrap();
let elapsed = start.elapsed();
assert!(diff.is_isomorphic);
assert!(elapsed.as_secs() < 10);
}
#[test]
fn test_disjoint_graphs() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 2, 0);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(!diff.is_isomorphic);
assert!(diff.similarity_score < 1.0);
}
#[test]
fn test_is_safe_method() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.is_safe());
assert!(!diff.has_breaking_changes());
}
#[test]
fn test_has_breaking_changes_method() {
let graph1 = create_test_graph_with_nodes(4);
let graph2 = create_test_graph_with_nodes(2);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 2, 3);
add_edge(&graph2, 0, 1);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert!(diff.has_breaking_changes());
assert!(!diff.is_safe());
}
#[test]
fn test_summary_method() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let summary = diff.summary();
assert!(summary.contains("Similarity"));
assert!(summary.contains("1.00"));
}
#[test]
fn test_graph_sizes_in_result() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(5);
let diff = graph_diff(&graph1, &graph2).unwrap();
assert_eq!(diff.graph1_size, 3);
assert_eq!(diff.graph2_size, 5);
}
#[test]
fn test_validate_refactor_safe() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.is_safe);
assert!(validation.breaking_changes.is_empty());
assert!(validation.warnings.iter().any(|w| w.contains("isomorphic")));
}
#[test]
fn test_validate_refactor_nodes_removed() {
let graph1 = create_test_graph_with_nodes(4);
let graph2 = create_test_graph_with_nodes(2);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 2, 3);
add_edge(&graph2, 0, 1);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
assert!(!validation.is_safe);
assert!(!validation.breaking_changes.is_empty());
assert!(
validation
.breaking_changes
.iter()
.any(|c| c.contains("Removed") && c.contains("nodes"))
);
}
#[test]
fn test_validate_refactor_low_similarity() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 2, 0);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
if diff.similarity_score < 0.5 {
assert!(!validation.is_safe);
assert!(
validation
.breaking_changes
.iter()
.any(|c| c.contains("similarity"))
);
}
}
#[test]
fn test_validate_refactor_isomorphic() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.is_safe);
assert!(validation.warnings.iter().any(|w| w.contains("isomorphic")));
}
#[test]
fn test_validate_refactor_moderate_similarity() {
let graph1 = create_test_graph_with_nodes(5);
let graph2 = create_test_graph_with_nodes(5);
for i in 0..4 {
add_edge(&graph1, i, i + 1);
}
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
if diff.similarity_score >= 0.5 && diff.similarity_score < 0.8 {
assert!(
validation
.warnings
.iter()
.any(|w| w.contains("Moderate similarity"))
);
}
}
#[test]
fn test_validate_refactor_edges_removed() {
let graph1 = create_test_graph_with_nodes(3);
let graph2 = create_test_graph_with_nodes(3);
add_edge(&graph1, 0, 1);
add_edge(&graph1, 1, 2);
add_edge(&graph1, 0, 2);
add_edge(&graph2, 0, 1);
add_edge(&graph2, 1, 2);
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.warnings.iter().any(|w| w.contains("edges")));
}
#[test]
fn test_refactor_validation_is_clean() {
let validation = RefactorValidation {
is_safe: true,
breaking_changes: vec![],
warnings: vec![],
};
assert!(validation.is_clean());
}
#[test]
fn test_refactor_validation_is_clean_with_warnings() {
let validation = RefactorValidation {
is_safe: true,
breaking_changes: vec![],
warnings: vec!["Some warning".to_string()],
};
assert!(!validation.is_clean());
}
#[test]
fn test_refactor_validation_summary() {
let validation = RefactorValidation {
is_safe: true,
breaking_changes: vec![],
warnings: vec![],
};
let summary = validation.summary();
assert!(summary.contains("safe"));
}
#[test]
fn test_refactor_validation_summary_with_warnings() {
let validation = RefactorValidation {
is_safe: true,
breaking_changes: vec![],
warnings: vec!["Warning 1".to_string(), "Warning 2".to_string()],
};
let summary = validation.summary();
assert!(summary.contains("safe"));
assert!(summary.contains("Warning 1") || summary.contains("Warning 2"));
}
#[test]
fn test_refactor_validation_summary_with_breaking() {
let validation = RefactorValidation {
is_safe: false,
breaking_changes: vec!["Breaking change".to_string()],
warnings: vec!["Warning".to_string()],
};
let summary = validation.summary();
assert!(summary.contains("Breaking change") || summary.contains("Warning"));
}
#[test]
fn test_refactor_validation_workflow() {
let original = create_test_graph_with_nodes(4);
add_edge(&original, 0, 1);
add_edge(&original, 1, 2);
add_edge(&original, 2, 3);
let optimized = create_test_graph_with_nodes(4);
add_edge(&optimized, 0, 1);
add_edge(&optimized, 1, 2);
add_edge(&optimized, 2, 3);
let diff = graph_diff(&original, &optimized).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.is_safe);
assert!(validation.breaking_changes.is_empty());
}
#[test]
fn test_refactor_validation_breaking_change() {
let v1 = create_test_graph_with_nodes(4);
add_edge(&v1, 0, 1);
add_edge(&v1, 1, 2);
add_edge(&v1, 2, 3);
let v2 = create_test_graph_with_nodes(2);
add_edge(&v2, 0, 1);
let diff = graph_diff(&v1, &v2).unwrap();
let validation = validate_refactor(&diff);
assert!(!validation.is_safe);
assert!(!validation.breaking_changes.is_empty());
}
#[test]
fn test_refactor_validation_optimizer_equivalence() {
let before = create_test_graph_with_nodes(5);
add_edge(&before, 0, 1);
add_edge(&before, 1, 2);
add_edge(&before, 2, 3);
add_edge(&before, 3, 4);
let after = create_test_graph_with_nodes(5);
add_edge(&after, 0, 1);
add_edge(&after, 1, 2);
add_edge(&after, 2, 3);
add_edge(&after, 3, 4);
let diff = graph_diff(&before, &after).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.is_safe);
}
#[test]
fn test_refactor_validation_empty_graphs() {
let graph1 = SqliteGraph::open_in_memory().expect("Failed to create graph");
let graph2 = SqliteGraph::open_in_memory().expect("Failed to create graph");
let diff = graph_diff(&graph1, &graph2).unwrap();
let validation = validate_refactor(&diff);
assert!(validation.is_safe);
}
}