use super::{tests::*, *}; use crate::{app::App, document::Document}; struct Fixture { p: Project, owners: [Id; 2], rows: [[Id; 4]; 2], trace: Id, which: OrderedCollection, } fn fixture(shape: u8, reverse: bool, local: bool) -> Fixture { let mut p = Project::empty(); let mut owners = vec![]; let mut rows = vec![]; let which = if shape == 3 { OrderedCollection::FragmentBranches } else { OrderedCollection::Steps }; for layer in [Layer::Oa, Layer::Sa] { let owner = if shape == 0 { p.add_element( if layer == Layer::Oa { Kind::Process } else { Kind::FunctionalChain }, layer, "Startup sequence", None, ) .unwrap() } else { let scenario = p .add_element(Kind::Scenario, layer, "Startup scenario", None) .unwrap(); if shape == 1 { scenario } else { let fragment = p .add_element(Kind::Occurrence, layer, "Alternatives", Some(scenario)) .unwrap(); *p.elements.get_mut(&fragment).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![], }, precondition: String::new(), postcondition: String::new(), }; p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(fragment); if shape == 3 { fragment } else { let branch = p .add_element(Kind::Occurrence, layer, "Start permitted", Some(fragment)) .unwrap(); *p.elements.get_mut(&branch).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Branch { guard: "permitted".into(), occurrences: vec![], }, precondition: String::new(), postcondition: String::new(), }; if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = p.elements.get_mut(&fragment).unwrap().shared_mut() { branches.push(branch); } branch } } }; let ids = [ "Request start", "Energize", "Verify interlock", "Local self-test", ] .map(|name| { let id = p .add_element(Kind::Occurrence, layer, name, Some(owner)) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: if shape == 3 { OccurrenceBody::Branch { guard: name.into(), occurrences: vec![], } } else if shape == 0 { OccurrenceBody::Control { kind: ControlKind::Choice, } } else { OccurrenceBody::Action { action: None, lifeline: None, } }, precondition: String::new(), postcondition: String::new(), }; id }); owners.push(owner); rows.push(ids); } let source = usize::from(reverse); let target = 1 - source; set_order( &mut p, owners[source], which, vec![rows[source][0], rows[source][2], rows[source][1]], ); set_order( &mut p, owners[target], which, if local { vec![rows[target][0], rows[target][3], rows[target][1]] } else { vec![rows[target][0], rows[target][1]] }, ); let trace = add_trace(&mut p, owners[0], owners[1], "Startup ordered review"); validate_candidate(&p).unwrap(); Fixture { p, owners: [owners[0], owners[1]], rows: [rows[0], rows[1]], trace, which, } } fn set_order(p: &mut Project, id: Id, which: OrderedCollection, rows: Vec) { if which == OrderedCollection::Steps { *p.elements.get_mut(&id).unwrap().steps_mut().unwrap() = rows; } else if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() { *branches = rows; } } fn member(f: &Fixture, index: usize) -> FacetSelector { FacetSelector::Member { collection: ordered::collection(f.which), keys: KeyPair { oa: Some(f.rows[0][index]), sa: Some(f.rows[1][index]), }, } } fn ready(f: &Fixture, reverse: bool) -> State { let mut state = State::new(&f.p, f.owners[usize::from(reverse)]).unwrap(); for i in 0..3 { state.references.insert(f.rows[0][i], f.rows[1][i]); } state.refresh(&f.p); state } fn decide(state: &mut State, p: &Project, facet: FacetSelector, decision: SemanticDecision) { state.update(p, RowEvent::Select(facet.clone())); state.update(p, RowEvent::Decide(facet, Some(decision))); } fn errors(s: &State) -> String { s.error .iter() .chain(s.plan.iter().flat_map(|p| &p.conflicts)) .cloned() .collect::>() .join("; ") } fn oracle(f: &Fixture, s: &State, expected: Option) -> Project { let reverse = s.direction == TransitionDirection::SaToOa; let source = f.owners[usize::from(reverse)]; let target = f.owners[1 - usize::from(reverse)]; let mut transition = TransitionRequest::new(s.direction, vec![source]); transition.mappings.push(TransitionMapping { source, target: Some(target), target_kind: None, }); let mut request = SemanticTransitionRequest::new(transition); request.traces.insert(source, f.trace); request.references = s.references.clone(); request.reviews.push(SemanticReviewRequest { source, resolutions: s .decisions .iter() .map(|(facet, decision)| FacetResolution { facet: facet.clone(), decision: *decision, }) .collect(), reset: s.resets.iter().cloned().collect(), }); if let Some(expected) = expected { request.candidates.push(SemanticRecord::Element(expected)); } let plan = plan_semantic_transition(&f.p, request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let result = apply_semantic_transition(&f.p, &plan).unwrap(); assert!(s.can_apply(), "{}", errors(s)); assert_eq!(s.proposed.as_ref().unwrap(), &result); assert_eq!(s.plan.as_ref().unwrap().reviews, plan.reviews); result } #[test] fn ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_both_directions() { for shape in 0..4 { for reverse in [false, true] { let f = fixture(shape, reverse, false); let mut s = ready(&f, reverse); decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource); let header = FacetSelector::Header(HeaderFacet::Description); decide(&mut s, &f.p, header, SemanticDecision::Candidate); s.update( &f.p, RowEvent::Input("/description".into(), "Reviewed startup insertion".into()), ); let ti = 1 - usize::from(reverse); let mut expected = f.p.clone(); set_order( &mut expected, f.owners[ti], f.which, vec![f.rows[ti][0], f.rows[ti][2], f.rows[ti][1]], ); expected .elements .get_mut(&f.owners[ti]) .unwrap() .description = "Reviewed startup insertion".into(); oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone())); } } } #[test] fn ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positions() { for shape in 0..4 { for reverse in [false, true] { let f = fixture(shape, reverse, true); let mut s = ready(&f, reverse); decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource); assert!(!s.can_apply()); assert!(errors(&s).contains("anchor"), "{}", errors(&s)); let order = FacetSelector::Order(f.which); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); let ti = 1 - usize::from(reverse); assert_eq!( s.order_drafts[&f.which], vec![f.rows[ti][0], f.rows[ti][3], f.rows[ti][1], f.rows[ti][2]] ); s.update(&f.p, RowEvent::OrderMove(f.which, 3, -1)); let mut expected = f.p.clone(); set_order( &mut expected, f.owners[ti], f.which, vec![f.rows[ti][0], f.rows[ti][3], f.rows[ti][2], f.rows[ti][1]], ); let result = oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone())); let baseline = result.traces[&f.trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert_eq!(baseline.facets.len(), 2); assert!(baseline.facets.iter().any(|v| v.selector() == order)); } } } #[test] fn ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes() { for shape in 0..4 { let f = fixture(shape, false, true); let mut s = ready(&f, false); s.update( &f.p, RowEvent::MemberFamily(keyed::Family::Member(ordered::collection(f.which))), ); for (i, layer) in [(0, Layer::Oa), (1, Layer::Sa)] { let choices = s.member_choices(&f.p, layer); for id in f.rows[i] { assert!( choices.iter().any(|c| c.id == id), "shape {shape}: {choices:?}" ); } assert!(choices.iter().all(|c| !f.rows[1 - i].contains(&c.id))); } s.update( &f.p, RowEvent::MemberOa(Some(view::choice(&f.p, f.rows[0][2]))), ); s.update( &f.p, RowEvent::MemberSa(Some(view::choice(&f.p, f.rows[1][2]))), ); s.update(&f.p, RowEvent::StageMember); assert!(s.staged.contains(&member(&f, 2))); assert_eq!(s.references[&f.rows[0][2]], f.rows[1][2]); } } #[test] fn raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_reconcile() { for reverse in [false, true] { let f = fixture(0, reverse, true); let mut s = ready(&f, reverse); let order = FacetSelector::Order(f.which); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); s.update(&f.p, RowEvent::OrderMove(f.which, 1, -1)); let authored = s.order_drafts[&f.which].clone(); decide(&mut s, &f.p, member(&f, 2), SemanticDecision::Candidate); s.update(&f.p, RowEvent::Presence(member(&f, 2), true)); assert!(!s.can_apply()); assert_eq!(s.order_drafts[&f.which], authored); s.update(&f.p, RowEvent::Decide(order.clone(), None)); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); let saved = s.recovery_snapshot().unwrap(); let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap(); assert_eq!(restored.order_drafts[&f.which], authored); assert!(!restored.can_apply()); assert_eq!(restored.recovery_snapshot().unwrap(), saved); restored.update(&f.p, RowEvent::OrderReconcile(f.which)); let ti = 1 - usize::from(reverse); let mut expected = f.p.clone(); let mut final_order = authored; final_order.push(f.rows[ti][2]); set_order(&mut expected, f.owners[ti], f.which, final_order); oracle( &f, &restored, Some(expected.elements[&f.owners[ti]].clone()), ); let mut stale = f.p.clone(); stale.name.push_str(" changed"); assert!(State::from_recovery(&stale, saved).is_err()); } } #[test] fn order_take_preserves_destination_only_positions_and_order_alone_cannot_add_members() { for reverse in [false, true] { let mut f = fixture(0, reverse, true); let si = usize::from(reverse); let ti = 1 - si; set_order( &mut f.p, f.owners[si], f.which, vec![f.rows[si][0], f.rows[si][1]], ); let mut s = ready(&f, reverse); let order = FacetSelector::Order(f.which); decide(&mut s, &f.p, order.clone(), SemanticDecision::TakeSource); let result = oracle(&f, &s, None); assert_eq!(result.elements[&f.owners[ti]], f.p.elements[&f.owners[ti]]); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); s.order_drafts .get_mut(&f.which) .unwrap() .push(f.rows[ti][2]); s.refresh(&f.p); assert!(!s.can_apply()); assert!(errors(&s).contains("surviving"), "{}", errors(&s)); s.update(&f.p, RowEvent::OrderReconcile(f.which)); assert!(s.can_apply(), "{}", errors(&s)); } } #[test] fn ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo_redo() { let f = fixture(0, false, true); let mut app = App::with_project(f.p.clone()); let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry.clone()) .unwrap() .insert_str(0, "// Keep startup explanation verbatim\r\n"); app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); start(&mut app, f.owners[0]); for i in 0..3 { send( &mut app, RowEvent::ReferenceOa(view::choice(&f.p, f.rows[0][i])), ); send( &mut app, RowEvent::ReferenceSa(view::choice(&f.p, f.rows[1][i])), ); send(&mut app, RowEvent::AddReference); } send(&mut app, RowEvent::Select(member(&f, 2))); send( &mut app, RowEvent::Decide(member(&f, 2), Some(SemanticDecision::TakeSource)), ); send(&mut app, RowEvent::Select(FacetSelector::Order(f.which))); send( &mut app, RowEvent::Decide( FacetSelector::Order(f.which), Some(SemanticDecision::Candidate), ), ); send(&mut app, RowEvent::OrderMove(f.which, 3, -1)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let expected = state(&app).proposed.clone().unwrap(); apply(&mut app); assert_eq!(app.project(), &expected); let after = app.document.snapshot(); assert!( app.document.bundle().files[&app.document.bundle().entry] .starts_with("// Keep startup explanation verbatim\r\n") ); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.project(), &before.decoded.project); let _ = app.update(Message::Redo); assert_eq!(app.document.bundle(), &after.bundle); assert_eq!(app.project(), &expected); assert_eq!( Document::from_bundle( after.bundle.clone(), source_project::ProjectLimits::default() ) .unwrap() .project(), &expected ); } #[test] fn several_incoming_members_follow_source_order_despite_independent_identity_sorting() { for shape in 0..4 { for reverse in [false, true] { let mut f = fixture(shape, reverse, false); let si = usize::from(reverse); let ti = 1 - si; set_order( &mut f.p, f.owners[si], f.which, vec![f.rows[si][0], f.rows[si][2], f.rows[si][3], f.rows[si][1]], ); let mut s = ready(&f, reverse); s.references.insert(f.rows[0][3], f.rows[1][3]); decide(&mut s, &f.p, member(&f, 3), SemanticDecision::TakeSource); decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource); decide( &mut s, &f.p, FacetSelector::Header(HeaderFacet::Description), SemanticDecision::Candidate, ); s.update( &f.p, RowEvent::Input("/description".into(), "Multiple incoming steps".into()), ); let mut expected = f.p.clone(); set_order( &mut expected, f.owners[ti], f.which, vec![f.rows[ti][0], f.rows[ti][2], f.rows[ti][3], f.rows[ti][1]], ); expected .elements .get_mut(&f.owners[ti]) .unwrap() .description = "Multiple incoming steps".into(); oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone())); } } } #[test] fn ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_take_reorders_only() { for reverse in [false, true] { for decision in [ SemanticDecision::KeepTarget, SemanticDecision::TakeSource, SemanticDecision::Candidate, ] { let mut f = fixture(0, reverse, true); let si = usize::from(reverse); let ti = 1 - si; set_order(&mut f.p, f.owners[si], f.which, vec![f.rows[si][1]]); let mut s = ready(&f, reverse); decide(&mut s, &f.p, member(&f, 0), decision); if decision == SemanticDecision::Candidate { s.update(&f.p, RowEvent::Presence(member(&f, 0), false)); } decide( &mut s, &f.p, FacetSelector::Header(HeaderFacet::Description), SemanticDecision::Candidate, ); s.update( &f.p, RowEvent::Input("/description".into(), "Only selected membership".into()), ); let mut expected = f.p.clone(); if decision != SemanticDecision::KeepTarget { set_order( &mut expected, f.owners[ti], f.which, vec![f.rows[ti][3], f.rows[ti][1]], ); } expected .elements .get_mut(&f.owners[ti]) .unwrap() .description = "Only selected membership".into(); oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone())); } let mut f = fixture(0, reverse, false); let si = usize::from(reverse); let ti = 1 - si; set_order( &mut f.p, f.owners[si], f.which, vec![f.rows[si][1], f.rows[si][0]], ); for decision in [SemanticDecision::KeepTarget, SemanticDecision::TakeSource] { let mut s = ready(&f, reverse); decide(&mut s, &f.p, FacetSelector::Order(f.which), decision); let result = oracle(&f, &s, None); assert_eq!( result.elements[&f.owners[ti]].steps().unwrap(), &if decision == SemanticDecision::KeepTarget { vec![f.rows[ti][0], f.rows[ti][1]] } else { vec![f.rows[ti][1], f.rows[ti][0]] } ); } } } #[test] fn historical_pair_restore_uses_exact_member_keys_and_never_zips_order_observations() { let f = fixture(0, false, true); let mut state = ready(&f, false); decide( &mut state, &f.p, member(&f, 2), SemanticDecision::TakeSource, ); decide( &mut state, &f.p, FacetSelector::Order(f.which), SemanticDecision::Candidate, ); state.update(&f.p, RowEvent::OrderMove(f.which, 3, -1)); assert!(state.can_apply(), "{}", errors(&state)); let project = state.proposed.clone().unwrap(); let mut reopened = State::new(&project, f.trace).unwrap(); let exact = history::reviewed_pairs(&reopened, &project).unwrap(); assert_eq!(exact, BTreeMap::from([(f.rows[0][2], f.rows[1][2])])); assert_eq!( project.traces[&f.trace] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 2 ); reopened.update(&project, RowEvent::RestoreReviewedReferences); assert_eq!(reopened.references, exact); assert!(reopened.decisions.is_empty()); assert_eq!(reopened.proposed.as_ref(), Some(&project)); reopened.references.insert(f.rows[0][2], f.rows[1][0]); let prior = reopened.references.clone(); reopened.update(&project, RowEvent::RestoreReviewedReferences); assert!(reopened.error.as_ref().unwrap().contains("conflicts")); assert_eq!( reopened.references, prior, "Restore is atomic on disagreement" ); reopened.references.clear(); reopened.update(&project, RowEvent::Reset(member(&f, 2), true)); assert!( history::reviewed_pairs(&reopened, &project) .unwrap() .is_empty() ); }