623 lines
22 KiB
Rust
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()
|
|
);
|
|
}
|