use crate::model::{State, Statechart};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Difference {
pub path: String,
pub kind: DiffKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffKind {
Changed {
old: String,
new: String,
},
Added {
value: String,
},
Removed {
value: String,
},
}
pub fn diff(a: &Statechart, b: &Statechart) -> Vec<Difference> {
let mut diffs = Vec::new();
if a.initial != b.initial {
diffs.push(Difference {
path: "initial".into(),
kind: DiffKind::Changed {
old: a.initial.to_string(),
new: b.initial.to_string(),
},
});
}
if a.name != b.name {
diffs.push(Difference {
path: "name".into(),
kind: DiffKind::Changed {
old: format!("{:?}", a.name),
new: format!("{:?}", b.name),
},
});
}
let limit = crate::max_depth();
diff_states(&a.states, &b.states, "states", 0, &mut diffs, limit);
diffs
}
fn diff_states(
a: &[State],
b: &[State],
prefix: &str,
depth: usize,
diffs: &mut Vec<Difference>,
limit: usize,
) {
if depth > limit {
return;
}
let a_map: std::collections::BTreeMap<&str, &State> =
a.iter().map(|s| (s.id.as_str(), s)).collect();
let b_map: std::collections::BTreeMap<&str, &State> =
b.iter().map(|s| (s.id.as_str(), s)).collect();
for (id, sa) in &a_map {
if let Some(sb) = b_map.get(id) {
diff_state(sa, sb, &format!("{prefix}.{id}"), depth, diffs, limit);
} else {
diffs.push(Difference {
path: format!("{prefix}.{id}"),
kind: DiffKind::Removed {
value: id.to_string(),
},
});
}
}
for id in b_map.keys() {
if !a_map.contains_key(id) {
diffs.push(Difference {
path: format!("{prefix}.{id}"),
kind: DiffKind::Added {
value: id.to_string(),
},
});
}
}
}
fn diff_state(
a: &State,
b: &State,
prefix: &str,
depth: usize,
diffs: &mut Vec<Difference>,
limit: usize,
) {
if a.kind != b.kind {
diffs.push(Difference {
path: format!("{prefix}.kind"),
kind: DiffKind::Changed {
old: format!("{:?}", a.kind),
new: format!("{:?}", b.kind),
},
});
}
if a.transitions.len() != b.transitions.len() {
diffs.push(Difference {
path: format!("{prefix}.transitions.len"),
kind: DiffKind::Changed {
old: a.transitions.len().to_string(),
new: b.transitions.len().to_string(),
},
});
} else {
for (j, (ta, tb)) in a.transitions.iter().zip(b.transitions.iter()).enumerate() {
let tp = format!("{prefix}.transitions[{j}]");
if ta.event != tb.event {
diffs.push(Difference {
path: format!("{tp}.event"),
kind: DiffKind::Changed {
old: format!("{:?}", ta.event),
new: format!("{:?}", tb.event),
},
});
}
if ta.guard != tb.guard {
diffs.push(Difference {
path: format!("{tp}.guard"),
kind: DiffKind::Changed {
old: format!("{:?}", ta.guard),
new: format!("{:?}", tb.guard),
},
});
}
if ta.targets != tb.targets {
diffs.push(Difference {
path: format!("{tp}.targets"),
kind: DiffKind::Changed {
old: format!("{:?}", ta.targets),
new: format!("{:?}", tb.targets),
},
});
}
if ta.delay != tb.delay {
diffs.push(Difference {
path: format!("{tp}.delay"),
kind: DiffKind::Changed {
old: format!("{:?}", ta.delay),
new: format!("{:?}", tb.delay),
},
});
}
if ta.quorum != tb.quorum {
diffs.push(Difference {
path: format!("{tp}.quorum"),
kind: DiffKind::Changed {
old: format!("{:?}", ta.quorum),
new: format!("{:?}", tb.quorum),
},
});
}
}
}
diff_states(
&a.children,
&b.children,
&format!("{prefix}.children"),
depth + 1,
diffs,
limit,
);
}
pub fn is_equivalent(a: &Statechart, b: &Statechart) -> bool {
diff(a, b).is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Transition;
fn simple_chart() -> Statechart {
Statechart::new(
"a",
vec![
{
let mut s = State::atomic("a");
s.transitions.push(Transition::new("go", "b"));
s
},
State::final_state("b"),
],
)
}
#[test]
fn identical_charts_no_diff() {
let a = simple_chart();
let b = simple_chart();
assert!(is_equivalent(&a, &b));
}
#[test]
fn different_initial() {
let a = simple_chart();
let mut b = simple_chart();
b.initial = "b".into();
let diffs = diff(&a, &b);
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].path, "initial");
}
#[test]
fn added_state() {
let a = simple_chart();
let mut b = simple_chart();
b.states.push(State::atomic("c"));
let diffs = diff(&a, &b);
assert!(diffs.iter().any(|d| d.path == "states.c"
&& matches!(&d.kind, DiffKind::Added { value } if value == "c")));
}
#[test]
fn changed_guard() {
let a = simple_chart();
let mut b = simple_chart();
b.states[0].transitions[0] = Transition::new("go", "b").with_guard("new_guard");
let diffs = diff(&a, &b);
assert!(diffs.iter().any(|d| d.path.contains("guard")));
}
#[test]
fn roundtrip_is_equivalent() {
let xml = r#"
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="a">
<state id="a"><transition event="go" target="b"/></state>
<final id="b"/>
</scxml>
"#;
let chart = crate::parse_xml(xml).unwrap();
let exported = crate::export::xml::to_xml(&chart);
let chart2 = crate::parse_xml(&exported).unwrap();
assert!(is_equivalent(&chart, &chart2));
}
}