use scxml::{flatten, parse_xml, stats, validate};
#[cfg(feature = "xstate")]
use scxml::xstate::parse_xstate;
fn load_example(name: &str) -> String {
std::fs::read_to_string(format!("examples/{name}")).unwrap()
}
#[test]
fn example_new_product_approval() {
let xml = load_example("new_product_approval.scxml");
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let s = stats(&chart);
assert_eq!(s.total_states, 7);
assert_eq!(s.final_states, 1);
assert!(s.guarded_transitions > 0);
assert!(s.deadline_transitions > 0);
assert_eq!(chart.datamodel.items.len(), 3);
let committee = &chart.states[4]; assert_eq!(committee.transitions[0].quorum, Some(3));
}
#[test]
fn example_document_lifecycle() {
let xml = load_example("document_lifecycle.scxml");
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let s = stats(&chart);
assert_eq!(s.total_states, 5);
assert_eq!(s.final_states, 1);
}
#[test]
fn example_settlement() {
let xml = load_example("settlement.scxml");
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let s = stats(&chart);
assert_eq!(s.total_states, 6);
assert_eq!(s.deadline_transitions, 1);
let (states, _transitions) = flatten::flatten(&chart);
assert_eq!(states.len(), 6);
}
#[test]
fn example_parallel_checks() {
let xml = load_example("parallel_checks.scxml");
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let s = stats(&chart);
assert_eq!(s.parallel_states, 1);
assert_eq!(s.compound_states, 2); assert!(s.max_depth > 0);
}
#[test]
fn example_onboarding_approval() {
let xml = load_example("onboarding_approval.scxml");
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let s = stats(&chart);
assert_eq!(s.parallel_states, 1);
assert!(s.compound_states >= 3); assert!(s.final_states >= 2); assert!(s.guarded_transitions > 0);
assert!(s.deadline_transitions > 0); assert_eq!(chart.datamodel.items.len(), 2);
let parallel = chart.find_state("parallel_checks").unwrap();
assert_eq!(parallel.kind, scxml::model::StateKind::Parallel);
assert_eq!(parallel.children.len(), 3); assert!(
parallel.transitions.len() >= 2,
"parallel state should have exit transitions (checks_complete + checks_failed)"
);
let committee = chart.find_state("committee_review").unwrap();
assert_eq!(committee.transitions[0].quorum, Some(2));
assert_eq!(
committee.transitions[0].guard.as_deref(),
Some("approval.committee")
);
let (flat_states, flat_transitions) = flatten::flatten(&chart);
assert!(flat_states.len() >= 16, "should flatten all nested states");
assert!(
flat_transitions.len() >= 10,
"should flatten all transitions"
);
let dot = scxml::export::dot::to_dot(&chart);
assert!(dot.contains("cluster_parallel_checks"));
assert!(dot.contains("cluster_kyc"));
assert!(dot.contains("cluster_credit"));
assert!(dot.contains("cluster_aml"));
let mermaid = scxml::export::mermaid::to_mermaid(&chart);
assert!(mermaid.contains("state parallel_checks"));
assert!(mermaid.contains("--"));
let xml_out = scxml::export::xml::to_xml(&chart);
let chart2 = parse_xml(&xml_out).unwrap();
validate(&chart2).unwrap();
assert_eq!(
chart.iter_all_states().count(),
chart2.iter_all_states().count()
);
}
#[test]
fn all_examples_produce_valid_dot() {
for name in [
"new_product_approval.scxml",
"document_lifecycle.scxml",
"settlement.scxml",
"parallel_checks.scxml",
"onboarding_approval.scxml",
] {
let xml = load_example(name);
let chart = parse_xml(&xml).unwrap();
let dot = scxml::export::dot::to_dot(&chart);
assert!(
dot.contains("digraph statechart"),
"DOT missing header for {name}"
);
assert!(dot.contains("__start"), "DOT missing start node for {name}");
}
}
#[cfg(feature = "xstate")]
#[test]
fn example_document_lifecycle_xstate() {
let json = load_example("document_lifecycle.xstate.json");
let from_xstate = parse_xstate(&json).unwrap();
validate(&from_xstate).unwrap();
let xml = load_example("document_lifecycle.scxml");
let from_scxml = parse_xml(&xml).unwrap();
assert_eq!(from_xstate.states.len(), from_scxml.states.len());
assert_eq!(from_xstate.initial, from_scxml.initial);
let s = stats(&from_xstate);
assert_eq!(s.total_states, 5);
assert_eq!(s.final_states, 1);
}
#[cfg(feature = "xstate")]
#[test]
fn example_onboarding_approval_xstate() {
let json = load_example("onboarding_approval.xstate.json");
let from_xstate = parse_xstate(&json).unwrap();
validate(&from_xstate).unwrap();
let s = stats(&from_xstate);
assert_eq!(s.parallel_states, 1);
assert!(s.compound_states >= 3);
assert!(s.final_states >= 2);
let parallel = from_xstate.find_state("parallel_checks").unwrap();
assert_eq!(parallel.kind, scxml::model::StateKind::Parallel);
assert_eq!(parallel.children.len(), 3);
assert!(parallel.transitions.len() >= 2);
}
#[test]
fn all_examples_resolve_with_valid_invariants() {
use std::collections::HashSet;
for name in [
"new_product_approval.scxml",
"document_lifecycle.scxml",
"settlement.scxml",
"parallel_checks.scxml",
"onboarding_approval.scxml",
] {
let xml = load_example(name);
let chart = parse_xml(&xml).unwrap();
validate(&chart).unwrap();
let resolved = scxml::resolve(&chart);
let source_state_count = chart.iter_all_states().count();
assert_eq!(
resolved.states.len(),
source_state_count,
"resolved chart drops or invents states for {name}"
);
let state_ids: HashSet<&str> = resolved.states.iter().map(|s| s.id.as_str()).collect();
let event_set: HashSet<&str> = resolved.events.iter().map(|e| e.as_str()).collect();
let mut sorted = resolved.events.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
resolved.events, sorted,
"events catalog not sorted+deduped for {name}"
);
for state in &resolved.states {
let children_set: HashSet<&str> = state.children.iter().map(|c| c.as_str()).collect();
if let Some(ref ic) = state.initial_child {
assert!(
children_set.contains(ic.as_str()),
"{name}: state {} initial_child {ic} not in children",
state.id
);
}
if state.depth == 0 {
assert!(
state.parent.is_none(),
"{name}: depth-0 state {} has parent",
state.id
);
} else {
assert!(
state.parent.is_some(),
"{name}: nested state {} missing parent",
state.id
);
}
for t in &state.transitions {
assert!(
state_ids.contains(t.defined_in.as_str()),
"{name}: transition in state {} has defined_in {} not in chart",
state.id,
t.defined_in
);
if let Some(ref ev) = t.event {
assert!(
event_set.contains(ev.as_str()),
"{name}: event {ev} missing from catalog",
);
}
}
}
}
}
#[test]
fn onboarding_descendants_inherit_parallel_exit_transitions() {
let xml = load_example("onboarding_approval.scxml");
let chart = parse_xml(&xml).unwrap();
let resolved = scxml::resolve(&chart);
let descendant_ids: Vec<&str> = resolved
.states
.iter()
.filter(|s| {
let mut cursor = s.parent.as_deref();
while let Some(p) = cursor {
if p == "parallel_checks" {
return true;
}
cursor = resolved
.states
.iter()
.find(|x| x.id == p)
.and_then(|x| x.parent.as_deref());
}
false
})
.map(|s| s.id.as_str())
.collect();
assert!(
!descendant_ids.is_empty(),
"no descendants found under parallel_checks"
);
for id in &descendant_ids {
let state = resolved.states.iter().find(|s| s.id == *id).unwrap();
let has_complete = state.transitions.iter().any(|t| {
t.event.as_deref() == Some("checks_complete") && t.defined_in == "parallel_checks"
});
let has_failed = state.transitions.iter().any(|t| {
t.event.as_deref() == Some("checks_failed") && t.defined_in == "parallel_checks"
});
assert!(
has_complete,
"descendant {id} missing inherited checks_complete from parallel_checks"
);
assert!(
has_failed,
"descendant {id} missing inherited checks_failed from parallel_checks"
);
}
}
#[test]
fn all_examples_roundtrip_xml() {
for name in [
"new_product_approval.scxml",
"document_lifecycle.scxml",
"settlement.scxml",
"parallel_checks.scxml",
"onboarding_approval.scxml",
] {
let xml = load_example(name);
let chart = parse_xml(&xml).unwrap();
let exported = scxml::export::xml::to_xml(&chart);
let chart2 = parse_xml(&exported).unwrap();
validate(&chart2).unwrap();
assert_eq!(
chart.states.len(),
chart2.states.len(),
"state count mismatch after roundtrip for {name}"
);
}
}