Files
ARCADIA-Studio/crates/oa-core/tests/release_states.rs
T

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[&region].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);
}
}
}