757 lines
26 KiB
Rust
757 lines
26 KiB
Rust
use oa_core::{
|
|
references,
|
|
transitions::*,
|
|
validation::{self, Severity},
|
|
*,
|
|
};
|
|
|
|
fn add(p: &mut Project, kind: Kind, layer: Layer, parent: Option<Id>) -> Id {
|
|
p.add_element(kind, layer, &kind.to_string(), parent)
|
|
.unwrap()
|
|
}
|
|
fn vertex(p: &mut Project, region: Id, kind: Kind) -> Id {
|
|
add(p, kind, p.elements[®ion].layer(), Some(region))
|
|
}
|
|
fn region(p: &mut Project, owner: Id) -> Id {
|
|
add(p, Kind::Region, p.elements[&owner].layer(), Some(owner))
|
|
}
|
|
fn edge(p: &mut Project, source: Id, target: Id) -> Id {
|
|
p.relate(RelationKind::StateTransition, "Transition", source, target)
|
|
}
|
|
fn set_kind(p: &mut Project, id: Id, value: TransitionKind) {
|
|
if let RelationBody::StateTransition {
|
|
transition_kind, ..
|
|
} = &mut p.relations.get_mut(&id).unwrap().body
|
|
{
|
|
*transition_kind = value;
|
|
}
|
|
}
|
|
fn set_guard(p: &mut Project, id: Id, value: &str) {
|
|
if let RelationBody::StateTransition { guard, .. } = &mut p.relations.get_mut(&id).unwrap().body
|
|
{
|
|
*guard = value.into();
|
|
}
|
|
}
|
|
fn event(p: &mut Project, layer: Layer, body: EventBody) -> Id {
|
|
let id = add(p, Kind::Event, layer, None);
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Event { body };
|
|
id
|
|
}
|
|
fn diagnostics(p: &Project, code: &str) -> Vec<validation::Issue> {
|
|
validation::validate(p)
|
|
.into_iter()
|
|
.filter(|i| i.code == code)
|
|
.collect()
|
|
}
|
|
fn valid(p: &Project) {
|
|
let errors: Vec<_> = validation::validate(p)
|
|
.into_iter()
|
|
.filter(|i| i.severity == Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
}
|
|
fn rejects(p: &Project, code: &str) {
|
|
assert!(
|
|
diagnostics(p, code)
|
|
.iter()
|
|
.any(|i| i.severity == Severity::Error),
|
|
"Expected {code}: {:?}",
|
|
validation::validate(p)
|
|
);
|
|
}
|
|
fn fixture(layer: Layer) -> (Project, Id, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let owner = add(
|
|
&mut p,
|
|
if layer == Layer::Oa {
|
|
Kind::Entity
|
|
} else {
|
|
Kind::System
|
|
},
|
|
layer,
|
|
None,
|
|
);
|
|
let machine = add(&mut p, Kind::StateMachine, layer, Some(owner));
|
|
let main = region(&mut p, machine);
|
|
let initial = vertex(&mut p, main, Kind::Initial);
|
|
let ready = vertex(&mut p, main, Kind::State);
|
|
edge(&mut p, initial, ready);
|
|
(p, owner, machine, main, ready)
|
|
}
|
|
|
|
#[test]
|
|
fn every_vertex_is_an_editable_draft_but_only_stable_states_own_behavior_or_regions() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for kind in Kind::ALL.into_iter().filter(|k| k.is_state()) {
|
|
let (mut p, _, _, main, _) = fixture(layer);
|
|
if kind == Kind::Initial {
|
|
continue;
|
|
}
|
|
let id = vertex(&mut p, main, kind);
|
|
valid(&p);
|
|
assert!(!validation::can_parent(&p, kind, layer, None));
|
|
assert!(validation::can_parent(&p, kind, layer, Some(main)));
|
|
assert_eq!(
|
|
validation::can_parent(&p, Kind::Region, layer, Some(id)),
|
|
kind.is_stable_state()
|
|
);
|
|
let work = add(
|
|
&mut p,
|
|
if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
},
|
|
layer,
|
|
None,
|
|
);
|
|
assert_eq!(
|
|
RelationKind::StateAction.permits(kind, p.elements[&work].kind()),
|
|
kind.is_stable_state()
|
|
);
|
|
assert_eq!(
|
|
RelationKind::Availability.permits(p.elements[&work].kind(), kind),
|
|
kind.is_stable_state()
|
|
);
|
|
if !kind.is_stable_state() {
|
|
if let Some(SharedBody::State { entry, .. }) =
|
|
p.elements.get_mut(&id).unwrap().shared_mut()
|
|
{
|
|
*entry = "Retained legacy annotation".into();
|
|
}
|
|
assert_eq!(diagnostics(&p, "pseudostate_behavior").len(), 1);
|
|
valid(&p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typed_events_trigger_alternatives_and_effects_validate_without_owner_restrictions() {
|
|
let (mut p, _, _, main, ready) = fixture(Layer::Sa);
|
|
let end = vertex(&mut p, main, Kind::Final);
|
|
let item = add(&mut p, Kind::ExchangeItem, Layer::Sa, None);
|
|
let signal = event(&mut p, Layer::Sa, EventBody::Signal { items: vec![item] });
|
|
let time = event(
|
|
&mut p,
|
|
Layer::Sa,
|
|
EventBody::Time {
|
|
kind: TimeEventKind::After,
|
|
expression: "30 s".into(),
|
|
},
|
|
);
|
|
let change = event(
|
|
&mut p,
|
|
Layer::Sa,
|
|
EventBody::Change {
|
|
expression: "pressure > limit".into(),
|
|
},
|
|
);
|
|
let work = add(&mut p, Kind::Function, Layer::Sa, None);
|
|
let external = add(&mut p, Kind::SystemActor, Layer::Sa, None);
|
|
p.relate(
|
|
RelationKind::Allocation,
|
|
"External function",
|
|
external,
|
|
work,
|
|
);
|
|
let transition = edge(&mut p, ready, end);
|
|
if let RelationBody::StateTransition {
|
|
triggers,
|
|
effects,
|
|
trigger,
|
|
effect,
|
|
..
|
|
} = &mut p.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*triggers = vec![signal, time, change, work];
|
|
*effects = vec![work];
|
|
*trigger = "Any of these events".into();
|
|
*effect = "Record the reason".into();
|
|
}
|
|
valid(&p);
|
|
assert!(diagnostics(&p, "unresolved_trigger").is_empty());
|
|
let baseline = p.clone();
|
|
for replacement in [vec![signal, signal], vec![item], vec![Id::new_v4()]] {
|
|
let mut invalid = baseline.clone();
|
|
if let RelationBody::StateTransition { triggers, .. } =
|
|
&mut invalid.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*triggers = replacement;
|
|
}
|
|
assert!(Session::try_new(invalid).is_err());
|
|
}
|
|
if let RelationBody::StateTransition { effects, .. } =
|
|
&mut p.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*effects = vec![signal];
|
|
}
|
|
rejects(&p, "invalid_reference_kind");
|
|
p = baseline.clone();
|
|
if let Some(SharedBody::Event { body }) = p.elements.get_mut(&signal).unwrap().shared_mut() {
|
|
*body = EventBody::Signal { items: vec![work] };
|
|
}
|
|
rejects(&p, "invalid_reference_kind");
|
|
let mut p = baseline;
|
|
let foreign = event(&mut p, Layer::Oa, EventBody::Signal { items: vec![] });
|
|
if let RelationBody::StateTransition { triggers, .. } =
|
|
&mut p.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*triggers = vec![foreign];
|
|
}
|
|
rejects(&p, "invalid_reference_kind");
|
|
for body in [
|
|
EventBody::Time {
|
|
kind: TimeEventKind::At,
|
|
expression: String::new(),
|
|
},
|
|
EventBody::Change {
|
|
expression: " ".into(),
|
|
},
|
|
] {
|
|
let mut p = Project::empty();
|
|
event(&mut p, Layer::Oa, body);
|
|
valid(&p);
|
|
assert_eq!(diagnostics(&p, "missing_event_expression").len(), 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn event_reference_identity_cleanup_and_undo_preserve_triggering_intent() {
|
|
let (mut p, _, _, main, ready) = fixture(Layer::Oa);
|
|
let end = vertex(&mut p, main, Kind::Final);
|
|
let item = add(&mut p, Kind::ExchangeItem, Layer::Oa, None);
|
|
let signal = event(&mut p, Layer::Oa, EventBody::Signal { items: vec![item] });
|
|
let work = add(&mut p, Kind::Activity, Layer::Oa, None);
|
|
let transition = edge(&mut p, ready, end);
|
|
if let RelationBody::StateTransition {
|
|
triggers,
|
|
effects,
|
|
guard,
|
|
effect,
|
|
..
|
|
} = &mut p.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*triggers = vec![signal];
|
|
*effects = vec![work];
|
|
*guard = "accepted".into();
|
|
*effect = "Effect annotation survives".into();
|
|
}
|
|
valid(&p);
|
|
for id in [item, signal, work, transition, ready, main] {
|
|
let mut session = Session::new(p.clone());
|
|
let replacement = Id::new_v4();
|
|
session
|
|
.execute(Command::Reidentify(id, replacement))
|
|
.unwrap();
|
|
assert!(
|
|
session
|
|
.project()
|
|
.elements
|
|
.values()
|
|
.all(|e| !references::element_references(e).contains(&id))
|
|
);
|
|
assert!(
|
|
session
|
|
.project()
|
|
.relations
|
|
.values()
|
|
.all(|r| !references::relation_references(r).contains(&id))
|
|
);
|
|
let renamed = session.project().clone();
|
|
session.execute(Command::Delete(replacement)).unwrap();
|
|
valid(session.project());
|
|
if id == signal {
|
|
let RelationBody::StateTransition {
|
|
trigger,
|
|
triggers,
|
|
effects,
|
|
guard,
|
|
effect,
|
|
..
|
|
} = &session.project().relations[&transition].body
|
|
else {
|
|
panic!()
|
|
};
|
|
assert!(triggers.is_empty());
|
|
assert!(trigger.contains(&p.elements[&signal].name));
|
|
assert!(trigger.contains("replacement trigger"));
|
|
assert_eq!(effects, &[work]);
|
|
assert_eq!(guard, "accepted");
|
|
assert_eq!(effect, "Effect annotation survives");
|
|
assert_eq!(
|
|
diagnostics(session.project(), "unresolved_trigger").len(),
|
|
1
|
|
);
|
|
}
|
|
if id == transition {
|
|
assert!(session.project().elements.contains_key(&signal));
|
|
}
|
|
session.undo();
|
|
assert_eq!(session.project(), &renamed);
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
session.redo();
|
|
assert_eq!(session.project(), &renamed);
|
|
}
|
|
let mut session = Session::new(p);
|
|
let mut changed = session.project().relations[&transition].clone();
|
|
if let RelationBody::StateTransition { trigger, .. } = &mut changed.body {
|
|
*trigger = "Exact authored legacy trigger".into();
|
|
}
|
|
session
|
|
.execute(Command::EditRelation(transition, changed))
|
|
.unwrap();
|
|
session.execute(Command::Delete(signal)).unwrap();
|
|
assert!(
|
|
matches!(&session.project().relations[&transition].body, RelationBody::StateTransition { trigger, .. } if trigger == "Exact authored legacy trigger")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn transition_kinds_distinguish_internal_external_self_and_local_composite_routing() {
|
|
let (mut p, _, _, main, composite) = fixture(Layer::Sa);
|
|
let nested = region(&mut p, composite);
|
|
let child = vertex(&mut p, nested, Kind::Mode);
|
|
let other = vertex(&mut p, main, Kind::State);
|
|
let local = edge(&mut p, composite, child);
|
|
set_kind(&mut p, local, TransitionKind::Local);
|
|
let internal = edge(&mut p, composite, composite);
|
|
set_kind(&mut p, internal, TransitionKind::Internal);
|
|
edge(&mut p, child, child);
|
|
edge(&mut p, child, other);
|
|
valid(&p);
|
|
for (source, target, kind) in [
|
|
(child, other, TransitionKind::Internal),
|
|
(composite, composite, TransitionKind::Local),
|
|
(composite, other, TransitionKind::Local),
|
|
(child, other, TransitionKind::Local),
|
|
] {
|
|
let mut invalid = p.clone();
|
|
let id = edge(&mut invalid, source, target);
|
|
set_kind(&mut invalid, id, kind);
|
|
rejects(&invalid, "transition_kind");
|
|
}
|
|
let machine = add(&mut p, Kind::StateMachine, Layer::Sa, None);
|
|
let foreign = region(&mut p, machine);
|
|
let foreign = vertex(&mut p, foreign, Kind::State);
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
child,
|
|
foreign
|
|
));
|
|
edge(&mut p, child, foreign);
|
|
rejects(&p, "transition_machine");
|
|
}
|
|
|
|
#[test]
|
|
fn initial_final_and_terminate_reject_forbidden_routes_and_cardinality_atomically() {
|
|
let (p, _, _, main, ready) = fixture(Layer::Oa);
|
|
let initial = p
|
|
.children(main)
|
|
.find(|e| e.kind() == Kind::Initial)
|
|
.unwrap()
|
|
.id;
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
initial,
|
|
ready
|
|
));
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, initial, ready);
|
|
rejects(&invalid, "initial_transition_count");
|
|
for kind in [Kind::Final, Kind::Terminate] {
|
|
let mut p = p.clone();
|
|
let terminal = vertex(&mut p, main, kind);
|
|
edge(&mut p, ready, terminal);
|
|
valid(&p);
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
terminal,
|
|
ready
|
|
));
|
|
edge(&mut p, terminal, ready);
|
|
rejects(&p, "pseudostate_direction");
|
|
}
|
|
for kind in Kind::ALL
|
|
.into_iter()
|
|
.filter(|k| k.is_state() && !k.is_stable_state())
|
|
{
|
|
let mut p = p.clone();
|
|
let v = if kind == Kind::Initial {
|
|
initial
|
|
} else {
|
|
vertex(&mut p, main, kind)
|
|
};
|
|
let transition = edge(&mut p, v, v);
|
|
rejects(&p, "pseudostate_self");
|
|
let mut before = p.clone();
|
|
before.relations.remove(&transition);
|
|
let mut session = Session::new(before.clone());
|
|
assert!(session.execute(Command::Replace(p)).is_err());
|
|
assert_eq!(session.project(), &before);
|
|
}
|
|
for (prose, guard) in [("timer", ""), ("", "condition")] {
|
|
let mut p = p.clone();
|
|
let id = p
|
|
.relations
|
|
.values()
|
|
.find(|r| r.source() == initial)
|
|
.unwrap()
|
|
.id;
|
|
if let RelationBody::StateTransition {
|
|
trigger,
|
|
guard: actual,
|
|
..
|
|
} = &mut p.relations.get_mut(&id).unwrap().body
|
|
{
|
|
*trigger = prose.into();
|
|
*actual = guard.into();
|
|
}
|
|
rejects(&p, "initial_transition");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn choices_and_junctions_report_unfinished_guards_but_reject_conflicting_defaults_or_triggers() {
|
|
let (mut p, _, _, main, ready) = fixture(Layer::Sa);
|
|
let choice = vertex(&mut p, main, Kind::Choice);
|
|
let junction = vertex(&mut p, main, Kind::Junction);
|
|
let fallback = vertex(&mut p, main, Kind::State);
|
|
edge(&mut p, ready, choice);
|
|
let yes = edge(&mut p, choice, junction);
|
|
set_guard(&mut p, yes, "pressure <= limit");
|
|
let no = edge(&mut p, choice, fallback);
|
|
set_guard(&mut p, no, "else");
|
|
edge(&mut p, fallback, junction);
|
|
edge(&mut p, junction, ready);
|
|
valid(&p);
|
|
assert!(diagnostics(&p, "missing_choice_guard").is_empty());
|
|
let mut invalid = p.clone();
|
|
set_guard(&mut invalid, yes, "else");
|
|
rejects(&invalid, "choice_default");
|
|
let mut draft = p.clone();
|
|
set_guard(&mut draft, yes, "");
|
|
set_guard(&mut draft, no, "unknown");
|
|
valid(&draft);
|
|
assert_eq!(diagnostics(&draft, "missing_choice_guard").len(), 1);
|
|
assert_eq!(diagnostics(&draft, "missing_choice_default").len(), 1);
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
fallback,
|
|
choice
|
|
));
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, fallback, choice);
|
|
rejects(&invalid, "pseudostate_cardinality");
|
|
for source in [choice, junction] {
|
|
let mut invalid = p.clone();
|
|
let id = invalid
|
|
.relations
|
|
.values()
|
|
.find(|r| r.source() == source)
|
|
.unwrap()
|
|
.id;
|
|
if let RelationBody::StateTransition { trigger, .. } =
|
|
&mut invalid.relations.get_mut(&id).unwrap().body
|
|
{
|
|
*trigger = "after 30s".into();
|
|
}
|
|
rejects(&invalid, "pseudostate_trigger");
|
|
}
|
|
edge(&mut p, junction, fallback);
|
|
valid(&p);
|
|
assert_eq!(diagnostics(&p, "junction_guard").len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn histories_remain_per_region_with_shallow_direct_and_deep_nested_fallbacks() {
|
|
let (mut p, _, _, main, ready) = fixture(Layer::Sa);
|
|
let nested = region(&mut p, ready);
|
|
let nested_state = vertex(&mut p, nested, Kind::State);
|
|
let shallow = vertex(&mut p, main, Kind::ShallowHistory);
|
|
let deep = vertex(&mut p, main, Kind::DeepHistory);
|
|
assert_eq!(diagnostics(&p, "incomplete_state_vertex").len(), 2);
|
|
let fallback = edge(&mut p, shallow, ready);
|
|
edge(&mut p, deep, nested_state);
|
|
valid(&p);
|
|
let mut invalid = p.clone();
|
|
invalid
|
|
.relations
|
|
.get_mut(&fallback)
|
|
.unwrap()
|
|
.body
|
|
.set_endpoints(shallow, nested_state);
|
|
rejects(&invalid, "history_scope");
|
|
let mut invalid = p.clone();
|
|
set_guard(&mut invalid, fallback, "resume");
|
|
rejects(&invalid, "pseudostate_segment");
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, deep, ready);
|
|
rejects(&invalid, "pseudostate_cardinality");
|
|
for (kind, code) in [
|
|
(Kind::ShallowHistory, "multiple_shallow_history"),
|
|
(Kind::DeepHistory, "multiple_deep_history"),
|
|
] {
|
|
let mut invalid = p.clone();
|
|
vertex(&mut invalid, main, kind);
|
|
rejects(&invalid, code);
|
|
let mut valid_other = p.clone();
|
|
vertex(&mut valid_other, nested, kind);
|
|
valid(&valid_other);
|
|
}
|
|
let outside = add(&mut p, Kind::StateMachine, Layer::Sa, None);
|
|
let outside_region = region(&mut p, outside);
|
|
let foreign = vertex(&mut p, outside_region, Kind::State);
|
|
edge(&mut p, deep, foreign);
|
|
rejects(&p, "history_scope");
|
|
}
|
|
|
|
#[test]
|
|
fn fork_join_detect_concurrency_at_the_common_composite_not_deep_immediate_regions() {
|
|
let (mut p, _, machine, main, ready) = fixture(Layer::Sa);
|
|
let fork = vertex(&mut p, main, Kind::Fork);
|
|
let join = vertex(&mut p, main, Kind::Join);
|
|
let left = vertex(&mut p, main, Kind::State);
|
|
let left_region = region(&mut p, left);
|
|
let left_leaf = vertex(&mut p, left_region, Kind::State);
|
|
let right_region = region(&mut p, machine);
|
|
let right = vertex(&mut p, right_region, Kind::State);
|
|
let right_region = region(&mut p, right);
|
|
let right_leaf = vertex(&mut p, right_region, Kind::State);
|
|
edge(&mut p, ready, fork);
|
|
let arm = edge(&mut p, fork, left_leaf);
|
|
edge(&mut p, fork, right_leaf);
|
|
edge(&mut p, left_leaf, join);
|
|
edge(&mut p, right_leaf, join);
|
|
edge(&mut p, join, ready);
|
|
valid(&p);
|
|
let sibling_region = region(&mut p, left);
|
|
let sibling_leaf = vertex(&mut p, sibling_region, Kind::State);
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
fork,
|
|
sibling_leaf
|
|
));
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, fork, sibling_leaf);
|
|
rejects(&invalid, "orthogonal_regions");
|
|
let mut invalid = p.clone();
|
|
set_guard(&mut invalid, arm, "ready");
|
|
rejects(&invalid, "pseudostate_segment");
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, fork, join);
|
|
rejects(&invalid, "orthogonal_endpoint");
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, ready, fork);
|
|
rejects(&invalid, "pseudostate_cardinality");
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, join, left);
|
|
rejects(&invalid, "pseudostate_cardinality");
|
|
let mut invalid = p.clone();
|
|
let incoming = invalid
|
|
.relations
|
|
.values()
|
|
.find(|r| r.target() == join)
|
|
.unwrap()
|
|
.id;
|
|
if let RelationBody::StateTransition { trigger, .. } =
|
|
&mut invalid.relations.get_mut(&incoming).unwrap().body
|
|
{
|
|
*trigger = "synchronize".into();
|
|
}
|
|
rejects(&invalid, "pseudostate_segment");
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_points_enforce_inside_outside_local_routing_and_one_route_per_region() {
|
|
let (mut p, _, machine, main, composite) = fixture(Layer::Sa);
|
|
let outside = vertex(&mut p, main, Kind::State);
|
|
let nested = region(&mut p, composite);
|
|
let child = vertex(&mut p, nested, Kind::State);
|
|
let entry = vertex(&mut p, nested, Kind::EntryPoint);
|
|
let exit = vertex(&mut p, nested, Kind::ExitPoint);
|
|
edge(&mut p, outside, entry);
|
|
assert!(validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
entry,
|
|
child
|
|
));
|
|
let route = edge(&mut p, entry, child);
|
|
assert!(matches!(
|
|
p.relations[&route].body,
|
|
RelationBody::StateTransition {
|
|
transition_kind: TransitionKind::Local,
|
|
..
|
|
}
|
|
));
|
|
edge(&mut p, child, exit);
|
|
edge(&mut p, exit, outside);
|
|
valid(&p);
|
|
for (source, target) in [
|
|
(child, entry),
|
|
(entry, outside),
|
|
(outside, exit),
|
|
(exit, child),
|
|
] {
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
source,
|
|
target
|
|
));
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, source, target);
|
|
rejects(&invalid, "boundary_transition");
|
|
}
|
|
let other = vertex(&mut p, nested, Kind::Mode);
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::StateTransition,
|
|
entry,
|
|
other
|
|
));
|
|
let mut invalid = p.clone();
|
|
edge(&mut invalid, entry, other);
|
|
rejects(&invalid, "entry_point_cardinality");
|
|
let mut invalid = p.clone();
|
|
set_kind(&mut invalid, route, TransitionKind::External);
|
|
rejects(&invalid, "transition_kind");
|
|
let concurrent = region(&mut p, composite);
|
|
let concurrent_state = vertex(&mut p, concurrent, Kind::State);
|
|
assert_eq!(diagnostics(&p, "entry_region_default").len(), 1);
|
|
edge(&mut p, entry, concurrent_state);
|
|
valid(&p);
|
|
assert!(diagnostics(&p, "entry_region_default").is_empty());
|
|
let top_region = region(&mut p, machine);
|
|
let top_exit = vertex(&mut p, top_region, Kind::ExitPoint);
|
|
edge(&mut p, outside, top_exit);
|
|
valid(&p);
|
|
assert!(
|
|
!diagnostics(&p, "incomplete_state_vertex")
|
|
.iter()
|
|
.any(|i| i.element == Some(top_exit))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reachability_follows_nested_default_entry_and_reports_disconnected_vertices() {
|
|
let (mut p, _, _, main, composite) = fixture(Layer::Oa);
|
|
let nested = region(&mut p, composite);
|
|
let initial = vertex(&mut p, nested, Kind::Initial);
|
|
let child = vertex(&mut p, nested, Kind::State);
|
|
edge(&mut p, initial, child);
|
|
let isolated = vertex(&mut p, main, Kind::Final);
|
|
let unreachable = diagnostics(&p, "unreachable_state");
|
|
assert_eq!(unreachable.len(), 1);
|
|
assert_eq!(unreachable[0].element, Some(isolated));
|
|
edge(&mut p, child, isolated);
|
|
valid(&p);
|
|
assert!(diagnostics(&p, "unreachable_state").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn machine_subtrees_and_selected_edges_transition_events_work_and_payloads_in_both_directions() {
|
|
for direction in [TransitionDirection::OaToSa, TransitionDirection::SaToOa] {
|
|
let layer = direction.source_layer();
|
|
let (mut p, owner, machine, main, ready) = fixture(layer);
|
|
let end = vertex(&mut p, main, Kind::Terminate);
|
|
let vertices: Vec<_> = Kind::ALL
|
|
.into_iter()
|
|
.filter(|kind| {
|
|
kind.is_state() && !matches!(kind, Kind::Initial | Kind::State | Kind::Terminate)
|
|
})
|
|
.map(|kind| vertex(&mut p, main, kind))
|
|
.collect();
|
|
let item = add(&mut p, Kind::ExchangeItem, layer, None);
|
|
let signal = event(&mut p, layer, EventBody::Signal { items: vec![item] });
|
|
let work = add(
|
|
&mut p,
|
|
if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
},
|
|
layer,
|
|
None,
|
|
);
|
|
p.relate(RelationKind::Allocation, "Performs", owner, work);
|
|
let transition = edge(&mut p, ready, end);
|
|
if let RelationBody::StateTransition {
|
|
triggers,
|
|
effects,
|
|
guard,
|
|
..
|
|
} = &mut p.relations.get_mut(&transition).unwrap().body
|
|
{
|
|
*triggers = vec![signal, work];
|
|
*effects = vec![work];
|
|
*guard = "accepted".into();
|
|
}
|
|
let action = p.relate(RelationKind::StateAction, "On entry", ready, work);
|
|
let availability = p.relate(RelationKind::Availability, "Available", work, ready);
|
|
valid(&p);
|
|
for (source, scope) in [
|
|
(machine, DependencyScope::Subtree),
|
|
(transition, DependencyScope::Required),
|
|
] {
|
|
let mut request = TransitionRequest::new(direction, vec![source]);
|
|
request.scope = scope;
|
|
request.mappings.push(TransitionMapping {
|
|
source: owner,
|
|
target: None,
|
|
target_kind: Some(if layer == Layer::Oa {
|
|
Kind::System
|
|
} else {
|
|
Kind::Entity
|
|
}),
|
|
});
|
|
let plan = plan_transition(&p, &request).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let candidate = execute_batch(&p, &plan.commands).unwrap();
|
|
valid(&candidate);
|
|
let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target;
|
|
assert!(
|
|
matches!(&candidate.relations[&mapped(transition)].body, RelationBody::StateTransition { triggers, effects, guard, .. } if *triggers == vec![mapped(signal), mapped(work)] && *effects == vec![mapped(work)] && guard == "accepted")
|
|
);
|
|
assert!(
|
|
matches!(&candidate.elements[&mapped(signal)].shared(), Some(SharedBody::Event { body: EventBody::Signal { items } }) if *items == vec![mapped(item)])
|
|
);
|
|
if scope == DependencyScope::Subtree {
|
|
assert!(candidate.relations.contains_key(&mapped(action)));
|
|
for id in &vertices {
|
|
assert_eq!(
|
|
candidate.elements[&mapped(*id)].kind(),
|
|
p.elements[id].kind()
|
|
);
|
|
assert!(
|
|
candidate
|
|
.traces
|
|
.values()
|
|
.any(|t| t.kind == TraceKind::State && (t.oa == *id || t.sa == *id))
|
|
);
|
|
}
|
|
}
|
|
assert!(!plan.changes.iter().any(|c| c.source == availability));
|
|
assert!(
|
|
candidate
|
|
.traces
|
|
.values()
|
|
.any(|t| t.kind == TraceKind::State && (t.oa == signal || t.sa == signal))
|
|
);
|
|
let mut session = Session::new(p.clone());
|
|
session.execute_many(&plan.commands).unwrap();
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
session.redo();
|
|
assert_eq!(session.project(), &candidate);
|
|
}
|
|
}
|
|
}
|