use alloc::string::ToString;
use alloc::vec;
use super::builder::*;
use crate::compile::CompiledChart;
use crate::ir::*;
use crate::validate::{ViolationKind, validate};
fn data(id: &str) -> Data {
Data {
id: id.to_string(),
src: None,
expr: None,
content: None,
extensions: vec![],
span: Span::NONE,
}
}
fn conflicts(c: &Chart) -> usize {
match validate(c) {
Ok(()) => 0,
Err(v) => v
.iter()
.filter(|x| matches!(x.kind, ViolationKind::ConflictingAttributes { .. }))
.count(),
}
}
#[test]
fn resolved_data_src_is_well_formed() {
let mut c = chart(vec![atomic("s")]);
c.data.push(Data {
src: Some("file:test552.txt".into()),
content: Some("[1, 2]".into()),
..data("Var1")
});
assert_eq!(conflicts(&c), 0);
assert!(CompiledChart::new(c.clone()).is_ok());
c.data[0].expr = Some("1".into());
assert_eq!(conflicts(&c), 1, "src with expr still conflicts");
}
#[test]
fn resolved_invoke_src_is_well_formed() {
let body = |expr: Option<&str>| Content {
expr: expr.map(Into::into),
body: if expr.is_some() {
None
} else {
Some("<scxml/>".into())
},
span: Span::NONE,
};
let with = |inv: Invoke| chart(vec![atomic("s").invoke(inv)]);
let resolved = Invoke {
src: Some("file:sub.scxml".into()),
content: Some(body(None)),
..Invoke::default()
};
assert_eq!(conflicts(&with(resolved)), 0);
let srcexpr = Invoke {
srcexpr: Some("x".into()),
content: Some(body(None)),
..Invoke::default()
};
assert_eq!(conflicts(&with(srcexpr)), 1);
let src_and_expr = Invoke {
src: Some("file:sub.scxml".into()),
content: Some(body(Some("x"))),
..Invoke::default()
};
assert_eq!(conflicts(&with(src_and_expr)), 1);
}
#[test]
fn done_invoke_queued_before_cancel_is_processed() {
use crate::datamodel::{Environment, Event, EventType};
use crate::null::NullDatamodel;
use crate::session::{Effect, Input, step};
let cc = compile(chart(vec![
atomic("s").invoke(Invoke::default()).on(tr("child", "t")),
atomic("t").on(tr("done.invoke", "done")),
final_("done"),
]));
let o = run_null(&cc);
let Some(Effect::Invoke(req)) = o.effects.last() else {
panic!("no invoke effect")
};
let id = req.invokeid.clone();
let from_child = |name: &str| Event {
invokeid: Some(id.clone()),
..Event::named(name, EventType::External)
};
let o = step(
&cc,
o.session,
Input::event(from_child("child"), Environment::default()),
&mut NullDatamodel,
)
.unwrap();
assert_eq!(
o.effects,
vec![Effect::CancelInvoke {
invokeid: id.clone()
}]
);
let o = step(
&cc,
o.session,
Input::event(
from_child(&alloc::format!("done.invoke.{id}")),
Environment::default(),
),
&mut NullDatamodel,
)
.unwrap();
assert!(o.trace.terminated, "done.invoke was processed");
}
fn history_without_default(id: &str) -> State {
State::new(id, StateElement::History(HistoryType::Shallow))
}
fn has_violation(c: &Chart, f: impl Fn(&ViolationKind) -> bool) -> bool {
validate(c)
.err()
.is_some_and(|v| v.iter().any(|x| f(&x.kind)))
}
#[test]
fn history_target_excludes_its_siblings() {
let a = compound(
"a",
vec![
atomic("a1"),
atomic("a2"),
history("h", HistoryType::Shallow, "a1"),
],
);
let illegal = |targets: &str| {
let c = chart(vec![atomic("s").on(tr("e", targets)), a.clone()]);
has_violation(&c, |k| matches!(k, ViolationKind::IllegalTargetSet { .. }))
};
assert!(illegal("h a2"), "history and its sibling");
assert!(illegal("a2 h"), "order does not matter");
assert!(!illegal("h"), "a history target alone is fine");
let p = parallel(
"p",
vec![
compound(
"r1",
vec![atomic("x"), history("hr", HistoryType::Shallow, "x")],
),
compound("r2", vec![atomic("y"), atomic("z")]),
],
);
let c = chart(vec![atomic("s").on(tr("e", "hr z")), p.clone()]);
assert!(validate(&c).is_ok(), "{:?}", validate(&c));
let c = chart(vec![atomic("s").on(tr("e", "hr x")), p]);
assert!(has_violation(&c, |k| matches!(
k,
ViolationKind::IllegalTargetSet { .. }
)));
}
#[test]
fn history_default_cycles_are_rejected() {
let mut a = compound("a", vec![atomic("a1"), history_without_default("h")]);
a.initial = vec!["h".to_string()];
let c = chart(vec![a]);
assert!(has_violation(&c, |k| matches!(
k,
ViolationKind::HistoryDefaultCycle { history } if history == "h"
)));
assert!(CompiledChart::new(c).is_err());
let mut b = compound(
"b",
vec![
atomic("b1"),
history_without_default("h1"),
history("h2", HistoryType::Shallow, "h1"),
],
);
b.initial = vec!["h2".to_string()];
let c = chart(vec![b]);
assert!(has_violation(&c, |k| matches!(
k,
ViolationKind::HistoryDefaultCycle { .. }
)));
let ok = chart(vec![
atomic("s").on(tr("", "h")),
compound("c", vec![atomic("c1"), history_without_default("h")]),
]);
let cc = compile(ok);
let o = run_null(&cc);
assert_eq!(active(&cc, &o), ids(&["c", "c1"]));
}
#[test]
fn unknown_scxml_session_is_error_communication() {
use crate::session::{Effect, StartInput, start};
let target = |id: &str| Send {
target: Some(alloc::format!("#_scxml_{id}")),
..send_event("ev")
};
let build = |id: &str| {
compile(chart(vec![
atomic("s0")
.entry(vec![send(target(id)), raise("foo")])
.on(tr("error.communication", "pass"))
.on(tr("*", "fail")),
final_("pass"),
final_("fail"),
]))
};
let listed = |cc: &CompiledChart| {
let input = StartInput {
sessions: Some(vec!["sid".to_string()]),
..start_input()
};
start(cc, input, &mut crate::null::NullDatamodel).unwrap()
};
let cc = build("foo");
let o = listed(&cc);
assert_eq!(events(&o), ids(&["error.communication"]));
assert!(o.trace.terminated);
assert!(!o.effects.iter().any(|e| matches!(e, Effect::Send(_))));
let cc = build("sid");
let o = listed(&cc);
assert!(o.effects.iter().any(|e| matches!(e, Effect::Send(_))));
let cc = build("foo");
let o = run_null(&cc);
assert!(o.effects.iter().any(|e| matches!(e, Effect::Send(_))));
assert_eq!(events(&o), ids(&["foo"]));
}
#[test]
fn execution_error_carries_the_datamodel_message() {
use crate::datamodel::{Event, Value};
use crate::null::NullDatamodel;
use crate::session::start;
use crate::tape::{Request, TapeDatamodel};
let mut s = atomic("s");
s.onentry.push(vec![log_expr("1 + 1")]);
s.transitions.push(tr("error.execution", "pass"));
let cc = compile(chart(vec![s, final_("pass")]));
let mut null = NullDatamodel;
let mut tape = TapeDatamodel::new(&mut null);
let o = start(&cc, start_input(), &mut tape).expect("start");
assert_eq!(events(&o), ids(&["error.execution"]));
let err: &Event = tape
.entries()
.iter()
.find_map(|e| match &e.request {
Request::SetEvent(ev) if ev.name == "error.execution" => Some(ev),
_ => None,
})
.expect("error.execution was delivered");
let Value::Object(fields) = &err.data else {
panic!("error data is not an object: {:?}", err.data)
};
assert_eq!(
fields[0],
("kind".into(), Value::String("execution".into()))
);
let Value::String(message) = &fields[1].1 else {
panic!("message is not a string")
};
assert_eq!(fields[1].0, "message");
assert!(!message.is_empty() && message != crate::session::EXECUTION_ERROR_MESSAGE);
}
#[test]
fn failing_eventless_guard_loop_hits_the_budget() {
use crate::session::{StartError, start};
let cc = compile(chart(vec![
atomic("s0").on(tr("", "s1").cond("not_an_in_predicate")),
atomic("s1"),
]));
let mut input = start_input();
input.options.microstep_budget = 50;
match start(&cc, input, &mut crate::null::NullDatamodel) {
Err(StartError::BudgetExceeded { budget, partial }) => {
assert_eq!(budget, 50);
assert_eq!(partial.trace.microsteps.len(), 50);
assert!(
partial
.trace
.microsteps
.iter()
.all(|m| m.event.as_deref() == Some("error.execution"))
);
}
other => panic!("expected the budget to run out, got {other:?}"),
}
}