Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
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, Event::Select(facet.clone()));
|
||||
state.update(p, Event::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,
|
||||
Event::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, Event::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,
|
||||
Event::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,
|
||||
Event::MemberOa(Some(view::choice(&f.p, f.rows[0][2]))),
|
||||
);
|
||||
s.update(
|
||||
&f.p,
|
||||
Event::MemberSa(Some(view::choice(&f.p, f.rows[1][2]))),
|
||||
);
|
||||
s.update(&f.p, Event::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, Event::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, Event::Presence(member(&f, 2), true));
|
||||
assert!(!s.can_apply());
|
||||
assert_eq!(s.order_drafts[&f.which], authored);
|
||||
s.update(&f.p, Event::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, Event::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, Event::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,
|
||||
Event::ReferenceOa(view::choice(&f.p, f.rows[0][i])),
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::ReferenceSa(view::choice(&f.p, f.rows[1][i])),
|
||||
);
|
||||
send(&mut app, Event::AddReference);
|
||||
}
|
||||
send(&mut app, Event::Select(member(&f, 2)));
|
||||
send(
|
||||
&mut app,
|
||||
Event::Decide(member(&f, 2), Some(SemanticDecision::TakeSource)),
|
||||
);
|
||||
send(&mut app, Event::Select(FacetSelector::Order(f.which)));
|
||||
send(
|
||||
&mut app,
|
||||
Event::Decide(
|
||||
FacetSelector::Order(f.which),
|
||||
Some(SemanticDecision::Candidate),
|
||||
),
|
||||
);
|
||||
send(&mut app, Event::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,
|
||||
Event::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, Event::Presence(member(&f, 0), false));
|
||||
}
|
||||
decide(
|
||||
&mut s,
|
||||
&f.p,
|
||||
FacetSelector::Header(HeaderFacet::Description),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
s.update(
|
||||
&f.p,
|
||||
Event::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]]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user