Add persisted state-machine diagram contexts and shared geometry
This commit is contained in:
@@ -0,0 +1,744 @@
|
||||
use oa_core::{projection::*, routing::*, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn point(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
fn add(p: &mut Project, kind: Kind, name: &str, parent: Option<Id>) -> Id {
|
||||
p.add_element(kind, Layer::Oa, name, parent).unwrap()
|
||||
}
|
||||
fn view(p: &mut Project, machine: Id, alias: &str) -> Id {
|
||||
let id = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: alias.into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::StateMachine { machine },
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
fn node(p: &Project, d: Id, e: Id) -> Id {
|
||||
p.diagrams[&d]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == e)
|
||||
.unwrap()
|
||||
.id
|
||||
}
|
||||
fn place(p: &mut Project, d: Id, e: Id, x: f32, y: f32, w: f32, h: f32) -> Id {
|
||||
let id = p.place(d, e, point(x, y));
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
id
|
||||
}
|
||||
fn transition(p: &mut Project, a: Id, b: Id, kind: TransitionKind) -> Id {
|
||||
let id = p.relate(RelationKind::StateTransition, "Transition", a, b);
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind, ..
|
||||
} = &mut p.relations.get_mut(&id).unwrap().body
|
||||
{
|
||||
*transition_kind = kind;
|
||||
}
|
||||
id
|
||||
}
|
||||
fn code(p: &Project, expected: &str) {
|
||||
assert!(
|
||||
validation::validate(p).iter().any(|i| i.code == expected),
|
||||
"{:?}",
|
||||
validation::validate(p)
|
||||
);
|
||||
}
|
||||
fn orthogonal(points: &[Position]) {
|
||||
assert!(
|
||||
points
|
||||
.windows(2)
|
||||
.all(|p| p[0].x == p[1].x || p[0].y == p[1].y),
|
||||
"{points:?}"
|
||||
);
|
||||
}
|
||||
struct Fixture {
|
||||
p: Project,
|
||||
machine: Id,
|
||||
region: Id,
|
||||
a: Id,
|
||||
b: Id,
|
||||
inner: Id,
|
||||
leaf: Id,
|
||||
entry: Id,
|
||||
exit: Id,
|
||||
d: Id,
|
||||
external: Id,
|
||||
self_edge: Id,
|
||||
internal: Id,
|
||||
local: Id,
|
||||
}
|
||||
fn fixture() -> Fixture {
|
||||
let mut p = Project::empty();
|
||||
let performer = add(&mut p, Kind::Entity, "Crew", None);
|
||||
let machine = add(
|
||||
&mut p,
|
||||
Kind::StateMachine,
|
||||
"Operating modes",
|
||||
Some(performer),
|
||||
);
|
||||
let region = add(&mut p, Kind::Region, "Main region", Some(machine));
|
||||
let initial = add(&mut p, Kind::Initial, "Start", Some(region));
|
||||
let a = add(&mut p, Kind::State, "Operating", Some(region));
|
||||
let b = add(&mut p, Kind::State, "Standby", Some(region));
|
||||
let inner = add(&mut p, Kind::Region, "Nested region", Some(a));
|
||||
let leaf = add(&mut p, Kind::State, "Working", Some(inner));
|
||||
let entry = add(&mut p, Kind::EntryPoint, "Enter", Some(inner));
|
||||
let exit = add(&mut p, Kind::ExitPoint, "Exit", Some(inner));
|
||||
transition(&mut p, initial, a, TransitionKind::External);
|
||||
let external = transition(&mut p, a, b, TransitionKind::External);
|
||||
if let RelationBody::StateTransition { guard, .. } =
|
||||
&mut p.relations.get_mut(&external).unwrap().body
|
||||
{
|
||||
*guard = "ready".into();
|
||||
}
|
||||
let self_edge = transition(&mut p, b, b, TransitionKind::External);
|
||||
let internal = transition(&mut p, a, a, TransitionKind::Internal);
|
||||
let local = transition(&mut p, entry, leaf, TransitionKind::Local);
|
||||
transition(&mut p, leaf, exit, TransitionKind::External);
|
||||
let d = view(&mut p, machine, "operating_view");
|
||||
for (e, x, y, w, h) in [
|
||||
(machine, 0., 0., 1100., 900.),
|
||||
(region, 24., 64., 1000., 800.),
|
||||
(initial, 60., 160., 224., 94.),
|
||||
(a, 180., 180., 440., 500.),
|
||||
(b, 760., 180., 224., 94.),
|
||||
(inner, 204., 320., 380., 300.),
|
||||
(leaf, 310., 420., 224., 94.),
|
||||
(entry, -200., 390., 224., 94.),
|
||||
(exit, 2000., 550., 224., 94.),
|
||||
] {
|
||||
place(&mut p, d, e, x, y, w, h);
|
||||
}
|
||||
validate_candidate(&p).unwrap();
|
||||
Fixture {
|
||||
p,
|
||||
machine,
|
||||
region,
|
||||
a,
|
||||
b,
|
||||
inner,
|
||||
leaf,
|
||||
entry,
|
||||
exit,
|
||||
d,
|
||||
external,
|
||||
self_edge,
|
||||
internal,
|
||||
local,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_context_wire_source_and_admission_contract() {
|
||||
let f = fixture();
|
||||
let json = serde_json::json!({"StateMachine":{"machine":f.machine}});
|
||||
assert_eq!(
|
||||
serde_json::to_value(f.p.diagrams[&f.d].projection).unwrap(),
|
||||
json
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ProjectionKind>(serde_json::json!("Overview")).unwrap(),
|
||||
ProjectionKind::Overview
|
||||
);
|
||||
for invalid in [
|
||||
serde_json::json!("StateMachine"),
|
||||
serde_json::json!({"StateMachine":{}}),
|
||||
serde_json::json!({"StateMachine":{"machine":null}}),
|
||||
serde_json::json!({"StateMachine":{"machine":f.machine,"extra":1}}),
|
||||
] {
|
||||
assert!(serde_json::from_value::<ProjectionKind>(invalid).is_err());
|
||||
}
|
||||
let source = language::format(&f.p);
|
||||
assert!(
|
||||
source.contains(
|
||||
"projection {\n StateMachine {\n machine @operating_modes;"
|
||||
),
|
||||
"{source}"
|
||||
);
|
||||
assert_eq!(language::parse(&source).unwrap(), f.p);
|
||||
assert!(
|
||||
language::parse(&source.replace(
|
||||
"machine @operating_modes;",
|
||||
"machine @operating_modes; extra 1;"
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
let mut limits = language::ParseLimits::UNLIMITED;
|
||||
limits.max_records = f.p.identities().len() - 1;
|
||||
assert!(language::parse_limited(&source, limits).is_err());
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Project>(&serde_json::to_vec(&f.p).unwrap()).unwrap(),
|
||||
f.p
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contexts_restrict_membership_without_changing_legacy_hidden_placements() {
|
||||
let mut f = fixture();
|
||||
let other = add(&mut f.p, Kind::StateMachine, "Other machine", None);
|
||||
let foreign_region = add(&mut f.p, Kind::Region, "Other region", Some(other));
|
||||
let foreign = add(&mut f.p, Kind::State, "Other state", Some(foreign_region));
|
||||
assert!(state_machine_contains(&f.p, f.machine, f.leaf));
|
||||
assert!(!state_machine_contains(&f.p, f.machine, foreign));
|
||||
let foreign_node = f.p.place(f.d, foreign, point(0., 0.));
|
||||
code(&f.p, "state_diagram_member");
|
||||
assert!(
|
||||
!project_view(&f.p, f.d)
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|n| n.id == foreign_node)
|
||||
);
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().projection = ProjectionKind::FunctionalDataflow;
|
||||
validate_candidate(&f.p).unwrap();
|
||||
assert!(project_view(&f.p, f.d).nodes.is_empty());
|
||||
for machine in [Id::nil(), f.region] {
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().projection = ProjectionKind::StateMachine { machine };
|
||||
code(&f.p, "state_diagram_context");
|
||||
}
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().projection =
|
||||
ProjectionKind::StateMachine { machine: f.machine };
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&foreign_node);
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().layer = Layer::Sa;
|
||||
code(&f.p, "state_diagram_context");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_partial_views_are_read_only_and_omit_unrelated_edges() {
|
||||
let mut f = fixture();
|
||||
let empty = view(&mut f.p, f.machine, "empty_state_view");
|
||||
validate_candidate(&f.p).unwrap();
|
||||
let before = f.p.clone();
|
||||
assert!(project_view(&f.p, empty).nodes.is_empty());
|
||||
assert_eq!(before, f.p);
|
||||
let unrelated =
|
||||
f.p.relate(RelationKind::Availability, "Availability", f.a, f.b);
|
||||
let partial = f.p.diagrams.get_mut(&f.d).unwrap();
|
||||
partial.nodes.retain(|_, n| [f.a, f.b].contains(&n.element));
|
||||
let before = f.p.clone();
|
||||
let projected = project_view(&f.p, f.d);
|
||||
let _ = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||
assert_eq!(before, f.p);
|
||||
assert!(
|
||||
!projected
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == unrelated || e.id == f.internal)
|
||||
);
|
||||
assert!(projected.edges.iter().any(|e| e.id == f.self_edge));
|
||||
assert!(projected.nodes.iter().all(|n| n.visual_parent.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_state_symbols_boundaries_internal_rows_and_self_loops_share_geometry() {
|
||||
let f = fixture();
|
||||
let projected = project_view(&f.p, f.d);
|
||||
let bounds = |id| projected.nodes.iter().find(|n| n.element == id).unwrap();
|
||||
let a = bounds(f.a);
|
||||
let entry = bounds(f.entry);
|
||||
let exit = bounds(f.exit);
|
||||
assert_eq!(entry.size, state_symbol_size(Kind::EntryPoint).unwrap());
|
||||
assert_eq!(entry.position.x + entry.size.width / 2., a.position.x);
|
||||
assert_eq!(
|
||||
exit.position.x + exit.size.width / 2.,
|
||||
a.position.x + a.size.width
|
||||
);
|
||||
assert!(entry.position.y >= a.position.y + state_header_height(&f.p, f.a));
|
||||
assert_eq!(entry.visual_parent, Some(node(&f.p, f.d, f.inner)));
|
||||
assert_eq!(state_header_height(&f.p, f.a), 120.);
|
||||
assert!(!projected.edges.iter().any(|e| e.id == f.internal));
|
||||
assert!(projected.edges.iter().any(|e| e.id == f.local));
|
||||
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||
let path = &routed.edges[&f.self_edge];
|
||||
orthogonal(&path.points);
|
||||
assert_ne!(
|
||||
path.points.first(),
|
||||
path.points.last(),
|
||||
"Self-transition returns at a different boundary anchor"
|
||||
);
|
||||
assert!(path.points.len() >= 5, "{:?}", path.points);
|
||||
assert!(path.warnings.is_empty(), "{:?}", path.warnings);
|
||||
let b = bounds(f.b);
|
||||
assert!(path.points.iter().any(|p| p.x < b.position.x
|
||||
|| p.x > b.position.x + b.size.width
|
||||
|| p.y < b.position.y
|
||||
|| p.y > b.position.y + b.size.height));
|
||||
assert!(routed.edges[&f.external].label.text.contains("[ready]"));
|
||||
for edge in routed.edges.values() {
|
||||
orthogonal(&edge.points);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folding_retains_authored_routes_without_false_self_transitions() {
|
||||
let mut f = fixture();
|
||||
let hidden_self = transition(&mut f.p, f.leaf, f.leaf, TransitionKind::External);
|
||||
let out = transition(&mut f.p, f.leaf, f.b, TransitionKind::External);
|
||||
let pins = vec![point(690., 480.), point(690., 230.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: out,
|
||||
waypoints: pins.clone(),
|
||||
label: None,
|
||||
});
|
||||
let a = node(&f.p, f.d, f.a);
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&a)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let projected = project_view(&f.p, f.d);
|
||||
assert!(
|
||||
!projected
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == hidden_self || e.id == f.local)
|
||||
);
|
||||
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||
assert!(routed.edges[&out].custom_suspended);
|
||||
assert!(routed.edges[&out].pins.is_empty());
|
||||
assert_eq!(f.p.diagrams[&f.d].routes[0].waypoints, pins);
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&a)
|
||||
.unwrap()
|
||||
.collapsed = false;
|
||||
let restored = route_diagram(&f.p, &f.p.diagrams[&f.d], &project_view(&f.p, f.d));
|
||||
assert_eq!(restored.edges[&out].pins, pins);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_moves_translate_hidden_descendants_and_pins_only_in_one_view() {
|
||||
let mut f = fixture();
|
||||
let copied = view(&mut f.p, f.machine, "second_view");
|
||||
for n in f.p.diagrams[&f.d]
|
||||
.nodes
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
place(
|
||||
&mut f.p,
|
||||
copied,
|
||||
n.element,
|
||||
n.position.x,
|
||||
n.position.y,
|
||||
n.size.width,
|
||||
n.size.height,
|
||||
);
|
||||
}
|
||||
let moved = node(&f.p, f.d, f.a);
|
||||
let label = EdgeLabel {
|
||||
fraction: 0.4,
|
||||
offset: point(3., -9.),
|
||||
};
|
||||
let pins = vec![point(570., 460.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.local,
|
||||
waypoints: pins.clone(),
|
||||
label: Some(label.clone()),
|
||||
});
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.external,
|
||||
waypoints: vec![point(690., 230.)],
|
||||
label: None,
|
||||
});
|
||||
f.p.diagrams
|
||||
.get_mut(&f.d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&moved)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let before = f.p.clone();
|
||||
let mut session = Session::try_new(f.p).unwrap();
|
||||
session
|
||||
.execute(Command::MoveNode {
|
||||
diagram: f.d,
|
||||
node: moved,
|
||||
position: point(220., 240.),
|
||||
with_descendants: true,
|
||||
})
|
||||
.unwrap();
|
||||
let after = session.project();
|
||||
assert_eq!(after.diagrams[&copied], before.diagrams[&copied]);
|
||||
for e in [f.a, f.inner, f.leaf, f.entry, f.exit] {
|
||||
let n = node(&before, f.d, e);
|
||||
let old = before.diagrams[&f.d].nodes[&n].position;
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].nodes[&n].position,
|
||||
point(old.x + 40., old.y + 60.)
|
||||
);
|
||||
assert_eq!(after.elements[&e], before.elements[&e]);
|
||||
}
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].routes[0].waypoints,
|
||||
vec![point(610., 520.)]
|
||||
);
|
||||
assert_eq!(after.diagrams[&f.d].routes[0].label, Some(label));
|
||||
assert_eq!(
|
||||
after.diagrams[&f.d].routes[1],
|
||||
before.diagrams[&f.d].routes[1]
|
||||
);
|
||||
let after = after.clone();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &before);
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_machine_reparent_requires_atomic_placement_and_route_repair() {
|
||||
let mut f = fixture();
|
||||
let orphan = add(&mut f.p, Kind::State, "Independent", Some(f.region));
|
||||
let edge = transition(&mut f.p, orphan, orphan, TransitionKind::External);
|
||||
let placement = f.p.place(f.d, orphan, point(30., 750.));
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
});
|
||||
let machine = add(&mut f.p, Kind::StateMachine, "Other machine", None);
|
||||
let region = add(&mut f.p, Kind::Region, "Other region", Some(machine));
|
||||
let other = view(&mut f.p, machine, "other_view");
|
||||
let before = f.p.clone();
|
||||
assert!(
|
||||
prepare_command(
|
||||
&f.p,
|
||||
&Command::Reparent {
|
||||
element: orphan,
|
||||
parent: Some(region)
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(f.p, before);
|
||||
let repaired = prepare_command(
|
||||
&f.p,
|
||||
&Command::Batch(vec![
|
||||
Command::Reparent {
|
||||
element: orphan,
|
||||
parent: Some(region),
|
||||
},
|
||||
Command::RelocateNode {
|
||||
node: placement,
|
||||
diagram: other,
|
||||
},
|
||||
Command::RemoveEdgeRoute { diagram: f.d, edge },
|
||||
Command::SetEdgeRoute {
|
||||
diagram: other,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
assert_eq!(repaired.diagrams[&other].nodes[&placement].element, orphan);
|
||||
assert!(!repaired.diagrams[&f.d].nodes.contains_key(&placement));
|
||||
assert!(repaired.diagrams[&f.d].routes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remapping_and_machine_deletion_include_dedicated_views_and_dependents() {
|
||||
let mut f = fixture();
|
||||
let sa_owner =
|
||||
f.p.add_element(Kind::System, Layer::Sa, "SA system", None)
|
||||
.unwrap();
|
||||
let counterpart =
|
||||
f.p.add_element(Kind::StateMachine, Layer::Sa, "SA modes", Some(sa_owner))
|
||||
.unwrap();
|
||||
let trace = Id::new_v4();
|
||||
f.p.traces.insert(
|
||||
trace,
|
||||
Trace {
|
||||
id: trace,
|
||||
alias: "machine_trace".into(),
|
||||
name: "Machine trace".into(),
|
||||
description: String::new(),
|
||||
oa: f.machine,
|
||||
sa: counterpart,
|
||||
kind: TraceKind::State,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
let artifact = Id::new_v4();
|
||||
f.p.artifact_links.insert(
|
||||
artifact,
|
||||
ArtifactLink {
|
||||
id: artifact,
|
||||
alias: "view_review".into(),
|
||||
name: "View review".into(),
|
||||
description: String::new(),
|
||||
owner: f.d,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "file:///review.txt".into(),
|
||||
revision: None,
|
||||
fragment: None,
|
||||
},
|
||||
);
|
||||
validate_candidate(&f.p).unwrap();
|
||||
let renamed = Id::new_v4();
|
||||
let remapped = prepare_command(&f.p, &Command::Reidentify(f.machine, renamed))
|
||||
.unwrap()
|
||||
.project;
|
||||
assert_eq!(
|
||||
remapped.diagrams[&f.d].projection.state_machine(),
|
||||
Some(renamed)
|
||||
);
|
||||
assert_eq!(remapped.elements[&f.region].parent, Some(renamed));
|
||||
let impact = references::deletion_impact(&remapped, renamed);
|
||||
for id in [f.d, artifact, trace, node(&remapped, f.d, f.a)] {
|
||||
assert!(impact.contains(&id));
|
||||
}
|
||||
assert!(!impact.contains(&counterpart));
|
||||
let deleted = prepare_command(&remapped, &Command::Delete(renamed))
|
||||
.unwrap()
|
||||
.project;
|
||||
assert!(!deleted.diagrams.contains_key(&f.d));
|
||||
assert!(deleted.elements.contains_key(&counterpart));
|
||||
let hidden = prepare_command(&f.p, &Command::Delete(node(&f.p, f.d, f.machine)))
|
||||
.unwrap()
|
||||
.project;
|
||||
assert!(hidden.diagrams.contains_key(&f.d));
|
||||
assert!(hidden.elements.contains_key(&f.machine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_updates_preserve_context_identity_and_unrelated_authored_bytes() {
|
||||
let f = fixture();
|
||||
let mut bundle = format_bundle(&f.p);
|
||||
bundle
|
||||
.files
|
||||
.get_mut(&bundle.entry.clone())
|
||||
.unwrap()
|
||||
.insert_str(0, "// state source rationale\n");
|
||||
let original = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
let fresh = Id::new_v4();
|
||||
let prepared = prepare_command(
|
||||
&f.p,
|
||||
&Command::Batch(vec![
|
||||
Command::Reidentify(f.machine, fresh),
|
||||
Command::SetAlias(fresh, "reviewed_modes".into()),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
let updated = update_bundle_with_identity_map(
|
||||
&original,
|
||||
&prepared.project,
|
||||
&prepared.identity_map,
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated.decoded.project, prepared.project);
|
||||
assert!(
|
||||
updated
|
||||
.bundle
|
||||
.files
|
||||
.values()
|
||||
.any(|s| s.contains("machine @reviewed_modes;"))
|
||||
);
|
||||
assert!(updated.bundle.files[&bundle.entry].starts_with("// state source rationale\n"));
|
||||
assert_eq!(
|
||||
compile_bundle(&updated.bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
updated.bundle
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_reserves_internal_rows_and_state_routes_retain_hard_pins() {
|
||||
let mut f = fixture();
|
||||
let pins = vec![point(730., 130.), point(1060., 130.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.self_edge,
|
||||
waypoints: pins.clone(),
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.25,
|
||||
offset: point(11., 7.),
|
||||
}),
|
||||
});
|
||||
let before = f.p.clone();
|
||||
let routed = route_diagram(&f.p, &f.p.diagrams[&f.d], &project_view(&f.p, f.d));
|
||||
assert_eq!(routed.edges[&f.self_edge].pins, pins);
|
||||
let mut previous = 0;
|
||||
for pin in &pins {
|
||||
let i = routed.edges[&f.self_edge]
|
||||
.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(previous)
|
||||
.find(|(_, p)| *p == pin)
|
||||
.unwrap()
|
||||
.0;
|
||||
previous = i + 1;
|
||||
}
|
||||
assert_eq!(f.p, before);
|
||||
let laid = prepare_command(&f.p, &Command::Layout(f.d))
|
||||
.unwrap()
|
||||
.project;
|
||||
let nodes = &laid.diagrams[&f.d].nodes;
|
||||
let a = &nodes[&node(&laid, f.d, f.a)];
|
||||
let inner = &nodes[&node(&laid, f.d, f.inner)];
|
||||
assert!(inner.position.y >= a.position.y + state_header_height(&laid, f.a));
|
||||
let first = language::format(&laid);
|
||||
assert_eq!(language::parse(&first).unwrap(), laid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_keeps_identity_and_validates_replacement_without_double_counting() {
|
||||
let f = fixture();
|
||||
let initial =
|
||||
f.p.elements
|
||||
.values()
|
||||
.find(|e| e.kind() == Kind::Initial)
|
||||
.unwrap()
|
||||
.id;
|
||||
let edge =
|
||||
f.p.relations
|
||||
.values()
|
||||
.find(|r| r.source() == initial)
|
||||
.unwrap();
|
||||
let mut replacement = edge.clone();
|
||||
if let RelationBody::StateTransition { target, .. } = &mut replacement.body {
|
||||
*target = f.b;
|
||||
}
|
||||
let after = prepare_command(&f.p, &Command::EditRelation(edge.id, replacement.clone()))
|
||||
.unwrap()
|
||||
.project;
|
||||
assert_eq!(after.relations[&edge.id], replacement);
|
||||
let mut invalid = replacement.clone();
|
||||
if let RelationBody::StateTransition { target, .. } = &mut invalid.body {
|
||||
*target = initial;
|
||||
}
|
||||
assert!(prepare_command(&after, &Command::EditRelation(edge.id, invalid)).is_err());
|
||||
assert_eq!(after.relations[&edge.id], replacement);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_a_region_does_not_translate_pins_attached_to_a_stationary_state_boundary() {
|
||||
let mut f = fixture();
|
||||
let pins = vec![point(280., 390.), point(280., 470.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.local,
|
||||
waypoints: pins.clone(),
|
||||
label: None,
|
||||
});
|
||||
let before = project_view(&f.p, f.d);
|
||||
let entry_before = before
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == f.entry)
|
||||
.unwrap()
|
||||
.position;
|
||||
let region = node(&f.p, f.d, f.inner);
|
||||
let old = f.p.diagrams[&f.d].nodes[®ion].position;
|
||||
let after = prepare_command(
|
||||
&f.p,
|
||||
&Command::MoveNode {
|
||||
diagram: f.d,
|
||||
node: region,
|
||||
position: point(old.x + 10., old.y + 10.),
|
||||
with_descendants: true,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
let projected = project_view(&after, f.d);
|
||||
let entry_after = projected
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == f.entry)
|
||||
.unwrap()
|
||||
.position;
|
||||
assert_eq!(
|
||||
entry_after.x, entry_before.x,
|
||||
"Entry stays on the unmoved composite boundary"
|
||||
);
|
||||
assert_eq!(after.diagrams[&f.d].routes[0].waypoints, pins);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_transition_attaches_to_the_compact_symbol_and_preview_keeps_settled_geometry() {
|
||||
let mut f = fixture();
|
||||
let initial =
|
||||
f.p.elements
|
||||
.values()
|
||||
.find(|e| e.kind() == Kind::Initial)
|
||||
.unwrap()
|
||||
.id;
|
||||
let edge =
|
||||
f.p.relations
|
||||
.values()
|
||||
.find(|r| r.source() == initial)
|
||||
.unwrap()
|
||||
.id;
|
||||
let projected = project_view(&f.p, f.d);
|
||||
let n = projected
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == initial)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
n.size,
|
||||
Size {
|
||||
width: 16.,
|
||||
height: 16.
|
||||
}
|
||||
);
|
||||
let settled = route_diagram(&f.p, &f.p.diagrams[&f.d], &projected);
|
||||
let first = settled.edges[&edge].points[0];
|
||||
assert!(
|
||||
[
|
||||
point(n.position.x, n.position.y + 8.),
|
||||
point(n.position.x + 16., n.position.y + 8.),
|
||||
point(n.position.x + 8., n.position.y),
|
||||
point(n.position.x + 8., n.position.y + 16.)
|
||||
]
|
||||
.contains(&first)
|
||||
);
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.self_edge,
|
||||
waypoints: vec![],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.3,
|
||||
offset: point(11., 9.),
|
||||
}),
|
||||
});
|
||||
let preview = route_edge_preview(&f.p, &f.p.diagrams[&f.d], &projected, &settled, f.self_edge);
|
||||
assert_eq!(
|
||||
preview.edges[&f.self_edge].points,
|
||||
settled.edges[&f.self_edge].points
|
||||
);
|
||||
assert_eq!(preview.edges[&edge], settled.edges[&edge]);
|
||||
}
|
||||
Reference in New Issue
Block a user