Files
ARCADIA-Studio/crates/oa-desktop/src/semantic_review/ordered_tests.rs
T

623 lines
22 KiB
Rust

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<Id>) {
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::<Vec<_>>()
.join("; ")
}
fn oracle(f: &Fixture, s: &State, expected: Option<Element>) -> 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()
);
}