use super::*;
fn file(hash: &str) -> Entry {
Entry {
kind: EntryKind::File,
hash: hash.to_string(),
size: hash.len() as u64,
executable: false,
}
}
fn set(entries: &[(&str, Entry)]) -> BTreeMap<String, Entry> {
entries
.iter()
.map(|(path, entry)| ((*path).to_string(), entry.clone()))
.collect()
}
fn action_for<'a>(convergence: &'a Convergence, path: &str) -> Option<&'a Action> {
convergence.actions.iter().find(|a| a.path() == path)
}
#[test]
fn test9931_a_one_sided_change_moves_without_asking() {
let base = set(&[("a.rs", file("base")), ("b.rs", file("same"))]);
let here = set(&[("a.rs", file("mine")), ("b.rs", file("same"))]);
let there = set(&[("a.rs", file("base")), ("b.rs", file("same"))]);
let plan = plan(&here, &there, &base);
assert_eq!(
action_for(&plan, "a.rs"),
Some(&Action::Send {
path: "a.rs".into(),
entry: file("mine")
})
);
assert!(action_for(&plan, "b.rs").is_none());
assert_eq!(plan.pending(), 1);
assert!(!plan.is_converged());
let mirrored = super::plan(&there, &here, &base);
assert_eq!(
action_for(&mirrored, "a.rs"),
Some(&Action::Receive {
path: "a.rs".into(),
entry: file("mine")
})
);
}
#[test]
fn test9932_independent_changes_conflict_rather_than_race() {
let base = set(&[("src/router.rs", file("base"))]);
let here = set(&[("src/router.rs", file("desktop"))]);
let there = set(&[("src/router.rs", file("laptop"))]);
let plan = plan(&here, &there, &base);
assert_eq!(
action_for(&plan, "src/router.rs"),
Some(&Action::Conflict {
path: "src/router.rs".into(),
here: Some(file("desktop")),
there: Some(file("laptop")),
base: Some(file("base")),
})
);
assert_eq!(plan.pending(), 0);
assert_eq!(plan.conflicts().count(), 1);
assert!(!plan.is_converged());
}
#[test]
fn test9933_identical_edits_on_both_sides_converge() {
let base = set(&[("x", file("old"))]);
let both = set(&[("x", file("new"))]);
let plan = plan(&both, &both, &base);
assert_eq!(
action_for(&plan, "x"),
Some(&Action::Agree {
path: "x".into(),
entry: Some(file("new"))
})
);
assert_eq!(plan.pending(), 0);
assert_eq!(plan.conflicts().count(), 0);
assert!(plan.is_converged());
let advanced = advance(&base, &plan, &["x".to_string()].into_iter().collect());
assert_eq!(advanced.get("x"), Some(&file("new")));
}
#[test]
fn test9934_a_deletion_never_destroys_an_independent_edit() {
let base = set(&[("gone.rs", file("base")), ("kept.rs", file("base"))]);
let here = set(&[("kept.rs", file("base"))]);
let there = set(&[("gone.rs", file("base")), ("kept.rs", file("base"))]);
let plan = plan(&here, &there, &base);
assert_eq!(
action_for(&plan, "gone.rs"),
Some(&Action::SendDeletion {
path: "gone.rs".into()
})
);
let there_modified = set(&[("gone.rs", file("their work")), ("kept.rs", file("base"))]);
let plan = super::plan(&here, &there_modified, &base);
assert_eq!(
action_for(&plan, "gone.rs"),
Some(&Action::Conflict {
path: "gone.rs".into(),
here: None,
there: Some(file("their work")),
base: Some(file("base")),
})
);
assert_eq!(plan.pending(), 0);
let plan = super::plan(&there_modified, &here, &base);
assert!(action_for(&plan, "gone.rs").expect("an action").is_conflict());
}
#[test]
fn test9935_creations_without_a_baseline_are_still_compared() {
let base = Baseline::new();
let plan = plan(&set(&[("new", file("mine"))]), &set(&[("new", file("theirs"))]), &base);
assert_eq!(
action_for(&plan, "new"),
Some(&Action::Conflict {
path: "new".into(),
here: Some(file("mine")),
there: Some(file("theirs")),
base: None,
})
);
let same = set(&[("new", file("identical"))]);
let plan = super::plan(&same, &same, &base);
assert_eq!(
action_for(&plan, "new"),
Some(&Action::Agree {
path: "new".into(),
entry: Some(file("identical"))
})
);
let plan = super::plan(&set(&[("new", file("mine"))]), &BTreeMap::new(), &base);
assert!(matches!(action_for(&plan, "new"), Some(Action::Send { .. })));
}
#[test]
fn test9936_kind_and_executable_bit_are_part_of_identity() {
let plain = file("same-bytes");
let executable = Entry {
executable: true,
..plain.clone()
};
let link = Entry {
kind: EntryKind::Symlink,
..plain.clone()
};
assert!(!plain.same_as(&executable));
assert!(!plain.same_as(&link));
let base = set(&[("script", plain.clone())]);
let here = set(&[("script", executable.clone())]);
let plan = plan(&here, &set(&[("script", plain)]), &base);
assert_eq!(
action_for(&plan, "script"),
Some(&Action::Send {
path: "script".into(),
entry: executable
})
);
}
#[test]
fn test9937_the_baseline_advances_only_over_what_landed() {
let base = set(&[
("landed", file("base")),
("failed", file("base")),
("fought", file("base")),
]);
let here = set(&[
("landed", file("mine")),
("failed", file("mine")),
("fought", file("mine")),
]);
let there = set(&[
("landed", file("base")),
("failed", file("base")),
("fought", file("theirs")),
]);
let plan = plan(&here, &there, &base);
let applied = ["landed".to_string()].into_iter().collect();
let next = advance(&base, &plan, &applied);
assert_eq!(next.get("landed"), Some(&file("mine")));
assert_eq!(
next.get("failed"),
Some(&file("base")),
"a transfer that did not land must leave the baseline where it was, so it is retried"
);
assert_eq!(
next.get("fought"),
Some(&file("base")),
"a conflict must leave the baseline where it was, so it survives a restart"
);
let deleted_here = set(&[("failed", file("base")), ("fought", file("mine"))]);
let plan = super::plan(&deleted_here, &there, &base);
let next = advance(&base, &plan, &["landed".to_string()].into_iter().collect());
assert!(!next.contains_key("landed"));
}
#[test]
fn test9995_agreement_advances_both_baselines_with_nothing_sent() {
let base = set(&[("kept", file("same")), ("phantom", file("gone"))]);
let here = set(&[("kept", file("same"))]);
let there = set(&[("kept", file("same"))]);
let plan = plan(&here, &there, &base);
assert!(plan.is_converged(), "nothing has to move: {plan:?}");
assert_eq!(plan.pending(), 0);
let next = advance(&base, &plan, &BTreeSet::new());
assert!(
!next.contains_key("phantom"),
"a path neither machine has must leave the baseline: {next:?}"
);
assert_eq!(next.get("kept"), Some(&file("same")));
let mirrored = advance(&base, &super::plan(&there, &here, &base), &BTreeSet::new());
assert_eq!(next, mirrored);
let base = set(&[("x", file("old"))]);
let both = set(&[("x", file("new"))]);
let plan = super::plan(&both, &both, &base);
let next = advance(&base, &plan, &BTreeSet::new());
assert_eq!(
next.get("x"),
Some(&file("new")),
"identical edits are agreed, and an agreement nobody records is decided again forever"
);
}
#[test]
fn test9938_every_decision_is_symmetric() {
let base = set(&[
("edited-here", file("base")),
("edited-there", file("base")),
("deleted-here", file("base")),
("fought", file("base")),
]);
let here = set(&[
("edited-here", file("mine")),
("edited-there", file("base")),
("fought", file("mine")),
("new-here", file("mine")),
]);
let there = set(&[
("edited-here", file("base")),
("edited-there", file("theirs")),
("deleted-here", file("base")),
("fought", file("theirs")),
]);
let mine = plan(&here, &there, &base);
let theirs = plan(&there, &here, &base);
for action in &mine.actions {
let counterpart = action_for(&theirs, action.path()).expect("both sides see every path");
let matched = match (action, counterpart) {
(Action::Send { .. }, Action::Receive { .. }) => true,
(Action::Receive { .. }, Action::Send { .. }) => true,
(Action::SendDeletion { .. }, Action::ReceiveDeletion { .. }) => true,
(Action::ReceiveDeletion { .. }, Action::SendDeletion { .. }) => true,
(Action::Conflict { .. }, Action::Conflict { .. }) => true,
(Action::Agree { .. }, Action::Agree { .. }) => true,
_ => false,
};
assert!(
matched,
"{} is {action:?} here and {counterpart:?} there — the two sides disagree about who is authoritative",
action.path()
);
}
assert_eq!(mine.actions.len(), theirs.actions.len());
}
#[test]
fn test11269_auto_takes_what_arrives_and_never_pushes_what_does_not() {
use crate::peer::model::Continuity as C;
let incoming = Action::Receive {
path: "a.txt".into(),
entry: file("theirs"),
};
let incoming_deletion = Action::ReceiveDeletion { path: "b.txt".into() };
let outgoing = Action::Send {
path: "c.txt".into(),
entry: file("mine"),
};
let outgoing_deletion = Action::SendDeletion { path: "d.txt".into() };
for action in [&incoming, &incoming_deletion] {
assert!(
crosses(action, C::Auto, false),
"auto must take {}: it is what auto means",
action.kind()
);
}
for action in [&outgoing, &outgoing_deletion] {
assert!(
!crosses(action, C::Auto, false),
"auto must not push {} — whether the peer takes it is the peer's decision",
action.kind()
);
}
for action in [&incoming, &incoming_deletion, &outgoing, &outgoing_deletion] {
assert!(
!crosses(action, C::Manual, false),
"manual must not move {} unasked",
action.kind()
);
}
for mode in [C::Manual, C::Auto] {
for action in [&incoming, &incoming_deletion, &outgoing, &outgoing_deletion] {
assert!(
crosses(action, mode, true),
"{} must carry a decided {}",
mode.as_str(),
action.kind()
);
}
}
for mode in [C::Off, C::Paused] {
for decided in [false, true] {
for action in [&incoming, &outgoing] {
assert!(
!crosses(action, mode, decided),
"{} must move nothing (decided={decided})",
mode.as_str()
);
}
}
}
let agreed = Action::Agree {
path: "e.txt".into(),
entry: Some(file("same")),
};
for mode in [C::Manual, C::Auto] {
assert!(crosses(&agreed, mode, false), "{} must record agreement", mode.as_str());
}
}