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 { 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 }, view_options: Default::default(), 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::(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::(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::(&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::>() { 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]); }