diff options
Diffstat (limited to 'third_party/rust/dogear/src/tests.rs')
-rw-r--r-- | third_party/rust/dogear/src/tests.rs | 2929 |
1 files changed, 2929 insertions, 0 deletions
diff --git a/third_party/rust/dogear/src/tests.rs b/third_party/rust/dogear/src/tests.rs new file mode 100644 index 0000000000..33645c35a2 --- /dev/null +++ b/third_party/rust/dogear/src/tests.rs @@ -0,0 +1,2929 @@ +// Copyright 2018-2019 Mozilla + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + cell::Cell, + convert::{TryFrom, TryInto}, + sync::Once, +}; + +use env_logger; + +use crate::driver::{DefaultAbortSignal, Driver}; +use crate::error::{Error, ErrorKind, Result}; +use crate::guid::{Guid, ROOT_GUID, UNFILED_GUID}; +use crate::merge::{to_strings, Merger, StructureCounts}; +use crate::tree::{ + self, Builder, Content, DivergedParent, DivergedParentGuid, Item, Kind, MergeState, Problem, + ProblemCounts, Problems, Tree, Validity, +}; + +#[derive(Debug)] +struct Node { + item: Item, + children: Vec<Node>, +} + +impl Node { + fn new(item: Item) -> Node { + Node { + item, + children: Vec::new(), + } + } + /// For convenience. + fn into_tree(self) -> Result<Tree> { + self.try_into() + } +} + +impl TryFrom<Node> for Builder { + type Error = Error; + + fn try_from(node: Node) -> Result<Builder> { + fn inflate(b: &mut Builder, parent_guid: &Guid, node: Node) -> Result<()> { + let guid = node.item.guid.clone(); + if let Err(err) = b.item(node.item) { + match err.kind() { + ErrorKind::DuplicateItem(_) => {} + _ => return Err(err), + } + } + b.parent_for(&guid).by_structure(&parent_guid)?; + for child in node.children { + inflate(b, &guid, child)?; + } + Ok(()) + } + + let guid = node.item.guid.clone(); + let mut builder = Tree::with_root(node.item); + builder.reparent_orphans_to(&UNFILED_GUID); + for child in node.children { + inflate(&mut builder, &guid, child)?; + } + Ok(builder) + } +} + +impl TryFrom<Node> for Tree { + type Error = Error; + fn try_from(node: Node) -> Result<Tree> { + Builder::try_from(node)?.try_into() + } +} + +macro_rules! nodes { + ($children:tt) => { nodes!(ROOT_GUID, Folder[needs_merge = true], $children) }; + ($guid:expr, $kind:ident) => { nodes!(Guid::from($guid), $kind[]) }; + ($guid:expr, $kind:ident [ $( $name:ident = $value:expr ),* ]) => {{ + #[allow(unused_mut)] + let mut item = Item::new(Guid::from($guid), Kind::$kind); + $({ item.$name = $value; })* + Node::new(item) + }}; + ($guid:expr, $kind:ident, $children:tt) => { nodes!($guid, $kind[], $children) }; + ($guid:expr, $kind:ident [ $( $name:ident = $value:expr ),* ], { $(( $($children:tt)+ )),* }) => {{ + #[allow(unused_mut)] + let mut node = nodes!($guid, $kind [ $( $name = $value ),* ]); + $({ + let child = nodes!($($children)*); + node.children.push(child.into()); + })* + node + }}; +} + +/// The name of a merge state. These match `tree::MergeState`, but without the +/// associated nodes to simplify comparisons. We also don't distinguish between +/// `{Local, Remote}Only` and `{Local, Remote}`, since that doesn't matter for +/// tests. +#[derive(Debug)] +enum MergeStateName { + Local, + LocalWithNewLocalStructure, + Remote, + RemoteWithNewRemoteStructure, + Unchanged, + UnchangedWithNewLocalStructure, +} + +/// A merged node produced by the `merged_nodes!` macro. Can be compared to +/// a `tree::MergedNode` using `assert_eq!`. +#[derive(Debug)] +struct MergedNode { + guid: Guid, + merge_state_name: MergeStateName, + children: Vec<MergedNode>, +} + +impl MergedNode { + fn new(guid: Guid, merge_state_name: MergeStateName) -> MergedNode { + MergedNode { + guid, + merge_state_name, + children: Vec::new(), + } + } +} + +impl<'t> PartialEq<tree::MergedNode<'t>> for MergedNode { + fn eq(&self, other: &tree::MergedNode<'t>) -> bool { + if self.guid != other.guid { + return false; + } + let merge_state_matches = match (&self.merge_state_name, other.merge_state) { + (MergeStateName::Local, MergeState::LocalOnly(_)) => true, + ( + MergeStateName::LocalWithNewLocalStructure, + MergeState::LocalOnlyWithNewLocalStructure(_), + ) => true, + (MergeStateName::Remote, MergeState::RemoteOnly(_)) => true, + ( + MergeStateName::RemoteWithNewRemoteStructure, + MergeState::RemoteOnlyWithNewRemoteStructure(_), + ) => true, + (MergeStateName::Local, MergeState::Local { .. }) => true, + ( + MergeStateName::LocalWithNewLocalStructure, + MergeState::LocalWithNewLocalStructure { .. }, + ) => true, + (MergeStateName::Remote, MergeState::Remote { .. }) => true, + ( + MergeStateName::RemoteWithNewRemoteStructure, + MergeState::RemoteWithNewRemoteStructure { .. }, + ) => true, + (MergeStateName::Unchanged, MergeState::Unchanged { .. }) => true, + ( + MergeStateName::UnchangedWithNewLocalStructure, + MergeState::UnchangedWithNewLocalStructure { .. }, + ) => true, + _ => false, + }; + if !merge_state_matches { + return false; + } + self.children == other.merged_children + } +} + +macro_rules! merged_nodes { + ($children:tt) => { merged_nodes!(ROOT_GUID, Local, $children) }; + ($guid:expr, $state:ident) => { + MergedNode::new(Guid::from($guid), MergeStateName::$state) + }; + ($guid:expr, $state:ident, { $(( $($children:tt)+ )),* }) => {{ + #[allow(unused_mut)] + let mut node = merged_nodes!($guid, $state); + $({ + let child = merged_nodes!($($children)*); + node.children.push(child); + })* + node + }}; +} + +fn before_each() { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + env_logger::init(); + }); +} + +#[test] +fn reparent_and_reposition() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]), + ("folderBBBBBB", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark[needs_merge = true]), + ("bookmarkDDDD", Bookmark[needs_merge = true]) + }), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }), + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("unfiled_____", Folder[needs_merge = true], { + ("folderBBBBBB", Folder[needs_merge = true], { + ("bookmarkDDDD", Bookmark[needs_merge = true]), + ("bookmarkAAAA", Bookmark[needs_merge = true]), + ("bookmarkCCCC", Bookmark[needs_merge = true]) + }) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("folderAAAAAA", Folder, { + ("bookmarkFFFF", Bookmark[needs_merge = true]), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkFFFF", RemoteWithNewRemoteStructure) + }), + ("unfiled_____", Remote, { + ("folderBBBBBB", Remote, { + ("bookmarkDDDD", Remote), + ("bookmarkAAAA", Remote), + ("bookmarkCCCC", Remote) + }) + }), + ("toolbar_____", Remote, { + ("folderAAAAAA", LocalWithNewLocalStructure, { + ("bookmarkEEEE", Remote) + }) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 10, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +// This test moves a bookmark that exists locally into a new folder that only +// exists remotely, and is a later sibling of the local parent. +#[test] +fn move_into_parent_sibling() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true]), + ("folderCCCCCC", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("folderAAAAAA", Remote), + ("folderCCCCCC", Remote, { + ("bookmarkBBBB", Remote) + }) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 4, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn reorder_and_insert() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark), + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }), + ("toolbar_____", Folder, { + ("bookmarkDDDD", Bookmark), + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark) + }) + }) + .into_tree() + .unwrap(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark), + ("bookmarkAAAA", Bookmark), + ("bookmarkBBBB", Bookmark) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("bookmarkDDDD", Bookmark), + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark), + ("bookmarkGGGG", Bookmark[needs_merge = true]), + ("bookmarkHHHH", Bookmark[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + ("bookmarkAAAA", Bookmark[age = 5]), + ("bookmarkBBBB", Bookmark[age = 5]), + ("bookmarkCCCC", Bookmark[age = 5]), + ("bookmarkIIII", Bookmark[needs_merge = true]), + ("bookmarkJJJJ", Bookmark[needs_merge = true]) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("bookmarkFFFF", Bookmark), + ("bookmarkDDDD", Bookmark), + ("bookmarkEEEE", Bookmark) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + // The server has an older menu, so we should use the local order (C A B) + // as the base, then append (I J). + ("bookmarkCCCC", Unchanged), + ("bookmarkAAAA", Unchanged), + ("bookmarkBBBB", Unchanged), + ("bookmarkIIII", Remote), + ("bookmarkJJJJ", Remote) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + // The server has a newer toolbar, so we should use the remote order (F D E) + // as the base, then append (G H). However, we always prefer the local state + // for roots, to avoid clobbering titles, so this is + // `LocalWithNewLocalStructure` instead of `RemoteWithNewRemoteStructure`. + ("bookmarkFFFF", Unchanged), + ("bookmarkDDDD", Unchanged), + ("bookmarkEEEE", Unchanged), + ("bookmarkGGGG", Local), + ("bookmarkHHHH", Local) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 12, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn unchanged_newer_changed_older() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder[age = 5], { + ("folderAAAAAA", Folder[age = 5]), + ("bookmarkBBBB", Bookmark[age = 5]) + }), + ("toolbar_____", Folder[age = 5], { + ("folderCCCCCC", Folder[age = 5]), + ("bookmarkDDDD", Bookmark[age = 5]) + }) + }) + .into_tree() + .unwrap(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + // Even though the local menu is newer (local = 5s, remote = 9s; + // adding E updated the modified times of A and the menu), it's + // not *changed* locally, so we should merge remote children first. + ("menu________", Folder, { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }), + ("bookmarkBBBB", Bookmark[age = 5]) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("bookmarkDDDD", Bookmark[age = 5]) + }) + })) + .unwrap(); + local_tree_builder.deletion("folderCCCCCC".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + ("bookmarkBBBB", Bookmark[age = 5]) + }), + // Even though the remote toolbar is newer (local = 15s, remote = 10s), it's + // not changed remotely, so we should merge local children first. + ("toolbar_____", Folder[age = 5], { + ("folderCCCCCC", Folder[needs_merge = true], { + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }), + ("bookmarkDDDD", Bookmark[age = 5]) + }) + })) + .unwrap(); + remote_tree_builder.deletion("folderAAAAAA".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkBBBB", Unchanged), + ("bookmarkEEEE", LocalWithNewLocalStructure) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + ("bookmarkDDDD", Unchanged), + ("bookmarkFFFF", RemoteWithNewRemoteStructure) + }) + }); + let expected_deletions = &["folderAAAAAA", "folderCCCCCC"]; + let expected_telem = StructureCounts { + local_deletes: 1, + remote_deletes: 1, + merged_nodes: 6, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn newer_local_moves() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder[age = 10], { + ("bookmarkAAAA", Bookmark[age = 10]), + ("folderBBBBBB", Folder[age = 10], { + ("bookmarkCCCC", Bookmark[age = 10]) + }), + ("folderDDDDDD", Folder[age = 10]) + }), + ("toolbar_____", Folder[age = 10], { + ("bookmarkEEEE", Bookmark[age = 10]), + ("folderFFFFFF", Folder[age = 10], { + ("bookmarkGGGG", Bookmark[age = 10]) + }), + ("folderHHHHHH", Folder[age = 10]) + }) + }) + .into_tree() + .unwrap(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderDDDDDD", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark[needs_merge = true]) + }) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("folderHHHHHH", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }), + ("folderFFFFFF", Folder[needs_merge = true]), + ("bookmarkEEEE", Bookmark[age = 10]) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]) + }), + ("mobile______", Folder[needs_merge = true], { + ("folderBBBBBB", Folder[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("mobile______", Folder[needs_merge = true, age = 5], { + ("bookmarkAAAA", Bookmark[needs_merge = true, age = 5]) + }), + ("unfiled_____", Folder[needs_merge = true, age = 5], { + ("folderBBBBBB", Folder[needs_merge = true, age = 5]) + }), + ("menu________", Folder[needs_merge = true, age = 5], { + ("folderDDDDDD", Folder[needs_merge = true, age = 5], { + ("bookmarkGGGG", Bookmark[needs_merge = true, age = 5]) + }) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("folderFFFFFF", Folder[needs_merge = true, age = 5]), + ("bookmarkEEEE", Bookmark[age = 10]), + ("folderHHHHHH", Folder[needs_merge = true, age = 5], { + ("bookmarkCCCC", Bookmark[needs_merge = true, age = 5]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", Local, { + ("folderDDDDDD", Local, { + ("bookmarkCCCC", Local) + }) + }), + ("toolbar_____", Local, { + ("folderHHHHHH", Local, { + ("bookmarkGGGG", Local) + }), + ("folderFFFFFF", Local), + ("bookmarkEEEE", Unchanged) + }), + ("unfiled_____", Local, { + ("bookmarkAAAA", Local) + }), + ("mobile______", Local, { + ("folderBBBBBB", Local) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 12, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn newer_remote_moves() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder[age = 10], { + ("bookmarkAAAA", Bookmark[age = 10]), + ("folderBBBBBB", Folder[age = 10], { + ("bookmarkCCCC", Bookmark[age = 10]) + }), + ("folderDDDDDD", Folder[age = 10]) + }), + ("toolbar_____", Folder[age = 10], { + ("bookmarkEEEE", Bookmark[age = 10]), + ("folderFFFFFF", Folder[age = 10], { + ("bookmarkGGGG", Bookmark[age = 10]) + }), + ("folderHHHHHH", Folder[age = 10]) + }) + }) + .into_tree() + .unwrap(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + ("folderDDDDDD", Folder[needs_merge = true, age = 5], { + ("bookmarkCCCC", Bookmark[needs_merge = true, age = 5]) + }) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("folderHHHHHH", Folder[needs_merge = true, age = 5], { + ("bookmarkGGGG", Bookmark[needs_merge = true, age = 5]) + }), + ("folderFFFFFF", Folder[needs_merge = true, age = 5]), + ("bookmarkEEEE", Bookmark[age = 10]) + }), + ("unfiled_____", Folder[needs_merge = true, age = 5], { + ("bookmarkAAAA", Bookmark[needs_merge = true, age = 5]) + }), + ("mobile______", Folder[needs_merge = true, age = 5], { + ("folderBBBBBB", Folder[needs_merge = true, age = 5]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("folderBBBBBB", Folder[needs_merge = true]) + }), + ("menu________", Folder[needs_merge = true], { + ("folderDDDDDD", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("folderFFFFFF", Folder[needs_merge = true]), + ("bookmarkEEEE", Bookmark[age = 10]), + ("folderHHHHHH", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", Local, { + ("folderDDDDDD", Remote, { + ("bookmarkGGGG", Remote) + }) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + ("folderFFFFFF", Remote), + ("bookmarkEEEE", Unchanged), + ("folderHHHHHH", Remote, { + ("bookmarkCCCC", Remote) + }) + }), + ("unfiled_____", LocalWithNewLocalStructure, { + ("folderBBBBBB", Remote) + }), + ("mobile______", LocalWithNewLocalStructure, { + ("bookmarkAAAA", Remote) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 12, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn value_structure_conflict() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }), + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark) + }) + }) + }) + .into_tree() + .unwrap(); + + let local_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder[needs_merge = true, age = 10], { + ("bookmarkCCCC", Bookmark) + }), + ("folderDDDDDD", Folder[needs_merge = true, age = 10], { + ("bookmarkBBBB", Bookmark), + ("bookmarkEEEE", Bookmark[age = 10]) + }) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }), + ("folderDDDDDD", Folder[needs_merge = true, age = 5], { + ("bookmarkEEEE", Bookmark[needs_merge = true, age = 5]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", Unchanged, { + ("folderAAAAAA", Local, { + ("bookmarkCCCC", Unchanged) + }), + ("folderDDDDDD", RemoteWithNewRemoteStructure, { + ("bookmarkEEEE", Remote), + ("bookmarkBBBB", Local) + }) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 6, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn complex_move_with_additions() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }) + }) + }) + .into_tree() + .unwrap(); + + let local_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark), + ("bookmarkDDDD", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark[needs_merge = true]) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("menu________", UnchangedWithNewLocalStructure, { + ("bookmarkCCCC", Remote) + }), + ("toolbar_____", Remote, { + ("folderAAAAAA", RemoteWithNewRemoteStructure, { + // We can guarantee child order (B E D), since we always walk remote + // children first, and the remote folder A record is newer than the + // local folder. If the local folder were newer, the order would be + // (D B E). + ("bookmarkBBBB", Unchanged), + ("bookmarkEEEE", Remote), + ("bookmarkDDDD", Local) + }) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 7, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn complex_orphaning() { + before_each(); + + let _shared_tree = nodes!({ + ("toolbar_____", Folder, { + ("folderAAAAAA", Folder, { + ("folderBBBBBB", Folder) + }) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder, { + ("folderEEEEEE", Folder) + }) + }) + }) + }) + .into_tree() + .unwrap(); + + // Locally: delete E, add B > F. + let mut local_tree_builder = Builder::try_from(nodes!({ + ("toolbar_____", Folder[needs_merge = false], { + ("folderAAAAAA", Folder, { + ("folderBBBBBB", Folder[needs_merge = true], { + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }) + }) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder[needs_merge = true]) + }) + }) + })) + .unwrap(); + local_tree_builder.deletion("folderEEEEEE".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + // Remotely: delete B, add E > G. + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("toolbar_____", Folder, { + ("folderAAAAAA", Folder[needs_merge = true]) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder, { + ("folderEEEEEE", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }) + }) + }) + }) + })) + .unwrap(); + remote_tree_builder.deletion("folderBBBBBB".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("toolbar_____", Unchanged, { + ("folderAAAAAA", RemoteWithNewRemoteStructure, { + // B was deleted remotely, so F moved to A, the closest + // surviving parent. + ("bookmarkFFFF", LocalWithNewLocalStructure) + }) + }), + ("menu________", Unchanged, { + ("folderCCCCCC", Unchanged, { + ("folderDDDDDD", LocalWithNewLocalStructure, { + // E was deleted locally, so G moved to D. + ("bookmarkGGGG", RemoteWithNewRemoteStructure) + }) + }) + }) + }); + let expected_deletions = &["folderBBBBBB", "folderEEEEEE"]; + let expected_telem = StructureCounts { + local_deletes: 1, + remote_deletes: 1, + merged_nodes: 7, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn locally_modified_remotely_deleted() { + before_each(); + + let _shared_tree = nodes!({ + ("toolbar_____", Folder, { + ("folderAAAAAA", Folder, { + ("folderBBBBBB", Folder) + }) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder, { + ("folderEEEEEE", Folder) + }) + }) + }) + }) + .into_tree() + .unwrap(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("toolbar_____", Folder, { + ("folderAAAAAA", Folder, { + ("folderBBBBBB", Folder[needs_merge = true], { + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }) + }) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder[needs_merge = true]) + }) + }) + })) + .unwrap(); + local_tree_builder.deletion("folderEEEEEE".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("toolbar_____", Folder, { + ("folderAAAAAA", Folder[needs_merge = true]) + }), + ("menu________", Folder, { + ("folderCCCCCC", Folder, { + ("folderDDDDDD", Folder, { + ("folderEEEEEE", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }) + }) + }) + }) + })) + .unwrap(); + remote_tree_builder.deletion("folderBBBBBB".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("toolbar_____", Unchanged, { + ("folderAAAAAA", RemoteWithNewRemoteStructure, { + ("bookmarkFFFF", LocalWithNewLocalStructure) + }) + }), + ("menu________", Unchanged, { + ("folderCCCCCC", Unchanged, { + ("folderDDDDDD", LocalWithNewLocalStructure, { + ("bookmarkGGGG", RemoteWithNewRemoteStructure) + }) + }) + }) + }); + let expected_deletions = &["folderBBBBBB", "folderEEEEEE"]; + let expected_telem = StructureCounts { + local_deletes: 1, + remote_deletes: 1, + merged_nodes: 7, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn locally_deleted_remotely_modified() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark), + ("folderBBBBBB", Folder, { + ("bookmarkCCCC", Bookmark), + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark) + }) + }) + }) + }) + .into_tree() + .unwrap(); + + let mut local_tree_builder = + Builder::try_from(nodes!({ ("menu________", Folder[needs_merge = true]) })).unwrap(); + local_tree_builder + .deletion("bookmarkAAAA".into()) + .deletion("folderBBBBBB".into()) + .deletion("bookmarkCCCC".into()) + .deletion("folderDDDDDD".into()) + .deletion("bookmarkEEEE".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark[needs_merge = true]), + ("folderBBBBBB", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark), + ("folderDDDDDD", Folder[needs_merge = true], { + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }), + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkAAAA", Remote), + ("bookmarkFFFF", RemoteWithNewRemoteStructure), + ("bookmarkGGGG", RemoteWithNewRemoteStructure) + }) + }); + let expected_deletions = &[ + "bookmarkCCCC", + "bookmarkEEEE", + "folderBBBBBB", + "folderDDDDDD", + ]; + let expected_telem = StructureCounts { + remote_revives: 1, + local_deletes: 2, + merged_nodes: 4, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn nonexistent_on_one_side() { + before_each(); + + // A doesn't exist remotely. + let mut local_tree_builder = Tree::with_root(Item::new(ROOT_GUID, Kind::Folder)); + local_tree_builder.deletion("bookmarkAAAA".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + // B doesn't exist locally. + let mut remote_tree_builder = Tree::with_root(Item::new(ROOT_GUID, Kind::Folder)); + remote_tree_builder.deletion("bookmarkBBBB".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let mut expected_root = Item::new(ROOT_GUID, Kind::Folder); + expected_root.needs_merge = true; + let expected_tree = merged_nodes!(ROOT_GUID, Unchanged, {}); + let expected_deletions = &["bookmarkAAAA", "bookmarkBBBB"]; + let expected_telem = StructureCounts { + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + let ops = merged_root.completion_ops(); + assert_eq!( + ops.summarize(), + &[ + "Flag remote bookmarkBBBB as merged", + "Delete local tombstone bookmarkAAAA", + ] + ); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn clear_folder_then_delete() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }), + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark) + }) + }) + }) + .into_tree() + .unwrap(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark) + }), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }), + ("mobile______", Folder[needs_merge = true], { + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder.deletion("folderDDDDDD".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]), + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark) + }) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkCCCC", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + remote_tree_builder.deletion("folderAAAAAA".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkBBBB", Remote), + ("bookmarkEEEE", Local) + }), + ("mobile______", Local, { + ("bookmarkFFFF", Local) + }), + ("unfiled_____", Remote, { + ("bookmarkCCCC", Remote) + }) + }); + let expected_deletions = &["folderAAAAAA", "folderDDDDDD"]; + let expected_telem = StructureCounts { + merged_nodes: 7, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn newer_move_to_deleted() { + before_each(); + + let _shared_tree = nodes!({ + ("menu________", Folder, { + ("folderAAAAAA", Folder, { + ("bookmarkBBBB", Bookmark) + }), + ("folderCCCCCC", Folder, { + ("bookmarkDDDD", Bookmark) + }) + }) + }) + .into_tree() + .unwrap(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + // A is younger locally. However, we should *not* revert + // remotely moving B to the toolbar. (Locally, B exists in A, + // but we deleted the now-empty A remotely). + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[age = 5]), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("bookmarkDDDD", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder.deletion("folderCCCCCC".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + // C is younger remotely. However, we should *not* revert + // locally moving D to the toolbar. (Locally, D exists in C, + // but we deleted the now-empty C locally). + ("folderCCCCCC", Folder[needs_merge = true], { + ("bookmarkDDDD", Bookmark[age = 5]), + ("bookmarkFFFF", Bookmark[needs_merge = true]) + }) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("bookmarkBBBB", Bookmark[needs_merge = true, age = 5]) + }) + })) + .unwrap(); + remote_tree_builder.deletion("folderAAAAAA".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkEEEE", LocalWithNewLocalStructure), + ("bookmarkFFFF", RemoteWithNewRemoteStructure) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + ("bookmarkDDDD", Local), + ("bookmarkBBBB", Remote) + }) + }); + let expected_deletions = &["folderAAAAAA", "folderCCCCCC"]; + let expected_telem = StructureCounts { + local_deletes: 1, + remote_deletes: 1, + merged_nodes: 6, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn deduping_multiple_candidates() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + ("folderAAAAA1", Folder[needs_merge = true, age = 5]), + ("folderAAAAA2", Folder[needs_merge = true, age = 5]) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("folderBBBBB1", Folder[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"folderAAAAA1".into()) + .content(Content::Folder { title: "A".into() }); + local_tree_builder + .mutate(&"folderAAAAA2".into()) + .content(Content::Folder { title: "A".into() }); + local_tree_builder + .mutate(&"folderBBBBB1".into()) + .content(Content::Folder { title: "B".into() }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAA1", Folder[needs_merge = true]) + }), + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("folderBBBBB1", Folder[needs_merge = true, age = 5]), + ("folderBBBBB2", Folder[needs_merge = true, age = 5]) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"folderAAAAA1".into()) + .content(Content::Folder { title: "A".into() }); + remote_tree_builder + .mutate(&"folderBBBBB1".into()) + .content(Content::Folder { title: "B".into() }); + remote_tree_builder + .mutate(&"folderBBBBB2".into()) + .content(Content::Folder { title: "B".into() }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("folderAAAAA1", Remote), + ("folderAAAAA2", Local) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + ("folderBBBBB1", Local), + ("folderBBBBB2", Remote) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 6, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn deduping_local_newer() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkAAA1", Bookmark[needs_merge = true]), + ("bookmarkAAA2", Bookmark[needs_merge = true]), + ("bookmarkAAA3", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"bookmarkAAA1".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + local_tree_builder + .mutate(&"bookmarkAAA2".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + local_tree_builder + .mutate(&"bookmarkAAA3".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + ("bookmarkAAAA", Bookmark[needs_merge = true, age = 5]), + ("bookmarkAAA4", Bookmark[needs_merge = true, age = 5]), + ("bookmarkAAA5", Bookmark) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"bookmarkAAAA".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + remote_tree_builder + .mutate(&"bookmarkAAA4".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkAAAA", LocalWithNewLocalStructure), + ("bookmarkAAA4", LocalWithNewLocalStructure), + ("bookmarkAAA3", Local), + ("bookmarkAAA5", Remote) + }) + }); + let expected_telem = StructureCounts { + dupes: 2, + merged_nodes: 5, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn deduping_remote_newer() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true, age = 5], { + // Shouldn't dedupe to `folderAAAAA1` because it's not in + // `new_local_contents`. + ("folderAAAAAA", Folder[needs_merge = true, age = 5], { + // Shouldn't dedupe to `bookmarkBBB1`. (bookmarkG111) + ("bookmarkBBBB", Bookmark[age = 10]), + // Not a candidate for `bookmarkCCC1` because the URLs are + // different. (bookmarkH111) + ("bookmarkCCCC", Bookmark[needs_merge = true, age = 5]) + }), + // Should dedupe to `folderDDDDD1`. (folderB11111) + ("folderDDDDDD", Folder[needs_merge = true, age = 5], { + // Should dedupe to `bookmarkEEE1`. (bookmarkC222) + ("bookmarkEEEE", Bookmark[needs_merge = true, age = 5]), + // Should dedupe to `separatorFF1` because the positions are + // the same. (separatorF11) + ("separatorFFF", Separator[needs_merge = true, age = 5]) + }), + // Shouldn't dedupe to `separatorGG1`, because the positions are + // different. (separatorE11) + ("separatorGGG", Separator[needs_merge = true, age = 5]), + // Shouldn't dedupe to `bookmarkHHH1` because the parents are + // different. (bookmarkC222) + ("bookmarkHHHH", Bookmark[needs_merge = true, age = 5]), + // Should dedupe to `queryIIIIII1`. + ("queryIIIIIII", Query[needs_merge = true, age = 5]) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"bookmarkCCCC".into()) + .content(Content::Bookmark { + title: "C".into(), + url_href: "http://example.com/c".into(), + }); + local_tree_builder + .mutate(&"folderDDDDDD".into()) + .content(Content::Folder { title: "D".into() }); + local_tree_builder + .mutate(&"bookmarkEEEE".into()) + .content(Content::Bookmark { + title: "E".into(), + url_href: "http://example.com/e".into(), + }); + local_tree_builder + .mutate(&"separatorFFF".into()) + .content(Content::Separator); + local_tree_builder + .mutate(&"separatorGGG".into()) + .content(Content::Separator); + local_tree_builder + .mutate(&"bookmarkHHHH".into()) + .content(Content::Bookmark { + title: "H".into(), + url_href: "http://example.com/h".into(), + }); + local_tree_builder + .mutate(&"queryIIIIIII".into()) + .content(Content::Bookmark { + title: "I".into(), + url_href: "place:maxResults=10&sort=8".into(), + }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[age = 10]), + ("bookmarkCCC1", Bookmark[needs_merge = true]) + }), + ("folderDDDDD1", Folder[needs_merge = true], { + ("bookmarkEEE1", Bookmark[needs_merge = true]), + ("separatorFF1", Separator[needs_merge = true]) + }), + ("separatorGG1", Separator[needs_merge = true]), + ("bookmarkHHH1", Bookmark[needs_merge = true]), + ("queryIIIIII1", Query[needs_merge = true]) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"bookmarkCCC1".into()) + .content(Content::Bookmark { + title: "C".into(), + url_href: "http://example.com/c1".into(), + }); + remote_tree_builder + .mutate(&"folderDDDDD1".into()) + .content(Content::Folder { title: "D".into() }); + remote_tree_builder + .mutate(&"bookmarkEEE1".into()) + .content(Content::Bookmark { + title: "E".into(), + url_href: "http://example.com/e".into(), + }); + remote_tree_builder + .mutate(&"separatorFF1".into()) + .content(Content::Separator); + remote_tree_builder + .mutate(&"separatorGG1".into()) + .content(Content::Separator); + remote_tree_builder + .mutate(&"bookmarkHHH1".into()) + .content(Content::Bookmark { + title: "H".into(), + url_href: "http://example.com/h".into(), + }); + remote_tree_builder + .mutate(&"queryIIIIII1".into()) + .content(Content::Bookmark { + title: "I".into(), + url_href: "place:maxResults=10&sort=8".into(), + }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("folderAAAAAA", RemoteWithNewRemoteStructure, { + ("bookmarkBBBB", Unchanged), + ("bookmarkCCC1", Remote), + ("bookmarkCCCC", Local) + }), + ("folderDDDDD1", Remote, { + ("bookmarkEEE1", Remote), + ("separatorFF1", Remote) + }), + ("separatorGG1", Remote), + ("bookmarkHHH1", Remote), + ("queryIIIIII1", Remote) + }) + }); + let expected_telem = StructureCounts { + dupes: 6, + merged_nodes: 11, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn complex_deduping() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAAA", Folder[needs_merge = true, age = 10], { + ("bookmarkBBBB", Bookmark[needs_merge = true, age = 10]), + ("bookmarkCCCC", Bookmark[needs_merge = true, age = 10]) + }), + ("folderDDDDDD", Folder[needs_merge = true], { + ("bookmarkEEEE", Bookmark[needs_merge = true, age = 5]) + }), + ("folderFFFFFF", Folder[needs_merge = true, age = 5], { + ("bookmarkGGGG", Bookmark[needs_merge = true, age = 5]) + }) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"folderAAAAAA".into()) + .content(Content::Folder { title: "A".into() }); + local_tree_builder + .mutate(&"bookmarkBBBB".into()) + .content(Content::Bookmark { + title: "B".into(), + url_href: "http://example.com/b".into(), + }); + local_tree_builder + .mutate(&"bookmarkCCCC".into()) + .content(Content::Bookmark { + title: "C".into(), + url_href: "http://example.com/c".into(), + }); + local_tree_builder + .mutate(&"folderDDDDDD".into()) + .content(Content::Folder { title: "D".into() }); + local_tree_builder + .mutate(&"bookmarkEEEE".into()) + .content(Content::Bookmark { + title: "E".into(), + url_href: "http://example.com/e".into(), + }); + local_tree_builder + .mutate(&"folderFFFFFF".into()) + .content(Content::Folder { title: "F".into() }); + local_tree_builder + .mutate(&"bookmarkGGGG".into()) + .content(Content::Bookmark { + title: "G".into(), + url_href: "http://example.com/g".into(), + }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("folderAAAAA1", Folder[needs_merge = true], { + ("bookmarkBBB1", Bookmark[needs_merge = true]) + }), + ("folderDDDDD1", Folder[needs_merge = true, age = 5], { + ("bookmarkEEE1", Bookmark[needs_merge = true]) + }), + ("folderFFFFF1", Folder[needs_merge = true], { + ("bookmarkGGG1", Bookmark[needs_merge = true, age = 5]), + ("bookmarkHHH1", Bookmark[needs_merge = true]) + }) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"folderAAAAA1".into()) + .content(Content::Folder { title: "A".into() }); + remote_tree_builder + .mutate(&"bookmarkBBB1".into()) + .content(Content::Bookmark { + title: "B".into(), + url_href: "http://example.com/b".into(), + }); + remote_tree_builder + .mutate(&"folderDDDDD1".into()) + .content(Content::Folder { title: "D".into() }); + remote_tree_builder + .mutate(&"bookmarkEEE1".into()) + .content(Content::Bookmark { + title: "E".into(), + url_href: "http://example.com/e".into(), + }); + remote_tree_builder + .mutate(&"folderFFFFF1".into()) + .content(Content::Folder { title: "F".into() }); + remote_tree_builder + .mutate(&"bookmarkGGG1".into()) + .content(Content::Bookmark { + title: "G".into(), + url_href: "http://example.com/g".into(), + }); + remote_tree_builder + .mutate(&"bookmarkHHH1".into()) + .content(Content::Bookmark { + title: "H".into(), + url_href: "http://example.com/h".into(), + }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", LocalWithNewLocalStructure, { + ("folderAAAAA1", RemoteWithNewRemoteStructure, { + ("bookmarkBBB1", Remote), + ("bookmarkCCCC", Local) + }), + ("folderDDDDD1", LocalWithNewLocalStructure, { + ("bookmarkEEE1", Remote) + }), + ("folderFFFFF1", Remote, { + ("bookmarkGGG1", Remote), + ("bookmarkHHH1", Remote) + }) + }) + }); + let expected_telem = StructureCounts { + dupes: 6, + merged_nodes: 9, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn left_pane_root() { + before_each(); + + let local_tree = Tree::with_root(Item::new(ROOT_GUID, Kind::Folder)) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("folderLEFTPR", Folder[needs_merge = true], { + ("folderLEFTPQ", Query[needs_merge = true]), + ("folderLEFTPF", Folder[needs_merge = true], { + ("folderLEFTPC", Query[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, Local); + let expected_deletions = &[ + "folderLEFTPC", + "folderLEFTPF", + "folderLEFTPQ", + "folderLEFTPR", + ]; + let expected_telem = StructureCounts { + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn livemarks() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder, { + ("livemarkAAAA", Livemark), + ("livemarkBBBB", Folder), + ("livemarkCCCC", Livemark) + }), + ("toolbar_____", Folder, { + ("livemarkDDDD", Livemark) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder, { + ("livemarkAAAA", Livemark), + ("livemarkBBBB", Livemark), + ("livemarkCCCC", Folder) + }), + ("unfiled_____", Folder, { + ("livemarkEEEE", Livemark) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("menu________", LocalWithNewLocalStructure), + ("toolbar_____", LocalWithNewLocalStructure), + ("unfiled_____", RemoteWithNewRemoteStructure) + }); + let expected_deletions = &[ + "livemarkAAAA", + "livemarkBBBB", + "livemarkCCCC", + "livemarkDDDD", + "livemarkEEEE", + ]; + let expected_telem = StructureCounts { + merged_nodes: 3, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn non_syncable_items() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + // A is non-syncable remotely, but B doesn't exist remotely, so + // we'll remove A from the merged structure, and move B to the + // menu. + ("folderAAAAAA", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]) + }) + }), + ("unfiled_____", Folder, { + // Orphaned left pane queries. + ("folderLEFTPQ", Query), + ("folderLEFTPC", Query) + }), + ("rootCCCCCCCC", Folder, { + // Non-syncable local root and children. + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark) + }), + ("bookmarkFFFF", Bookmark) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("unfiled_____", Folder[needs_merge = true], { + // D, J, and G are syncable remotely, but D is non-syncable + // locally. Since J and G don't exist locally, and are syncable + // remotely, we'll remove D, and move J and G to unfiled. + ("folderDDDDDD", Folder[needs_merge = true], { + ("bookmarkJJJJ", Bookmark[needs_merge = true]) + }), + ("bookmarkGGGG", Bookmark) + }), + ("rootHHHHHHHH", Folder[needs_merge = true], { + // H is a non-syncable root that only exists remotely. A is + // non-syncable remotely, and syncable locally. We should + // remove A and its descendants locally, since its parent + // H is known to be non-syncable remotely. + ("folderAAAAAA", Folder, { + // F exists in two different non-syncable folders: C + // locally, and A remotely. + ("bookmarkFFFF", Bookmark), + ("bookmarkIIII", Bookmark) + }) + }), + ("folderLEFTPR", Folder[needs_merge = true], { + // The complete left pane root. We should remove all left pane + // queries locally, even though they're syncable, since the left + // pane root is known to be non-syncable. + ("folderLEFTPQ", Query[needs_merge = true]), + ("folderLEFTPF", Folder[needs_merge = true], { + ("folderLEFTPC", Query[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkBBBB", LocalWithNewLocalStructure) + }), + ("unfiled_____", LocalWithNewLocalStructure, { + ("bookmarkJJJJ", RemoteWithNewRemoteStructure), + ("bookmarkGGGG", Remote) + }) + }); + let expected_deletions = &[ + "bookmarkEEEE", // Non-syncable locally. + "bookmarkFFFF", // Non-syncable locally. + "bookmarkIIII", // Non-syncable remotely. + "folderAAAAAA", // Non-syncable remotely. + "folderDDDDDD", // Non-syncable locally. + "folderLEFTPC", // Non-syncable remotely. + "folderLEFTPF", // Non-syncable remotely. + "folderLEFTPQ", // Non-syncable remotely. + "folderLEFTPR", // Non-syncable remotely. + "rootCCCCCCCC", // Non-syncable locally. + "rootHHHHHHHH", // Non-syncable remotely. + ]; + let expected_telem = StructureCounts { + merged_nodes: 5, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn applying_two_empty_folders_doesnt_smush() { + before_each(); + + let local_tree = Tree::with_root(Item::new(ROOT_GUID, Kind::Folder)) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("emptyempty01", Folder[needs_merge = true]), + ("emptyempty02", Folder[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, UnchangedWithNewLocalStructure, { + ("mobile______", Remote, { + ("emptyempty01", Remote), + ("emptyempty02", Remote) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 3, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn applying_two_empty_folders_matches_only_one() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("emptyempty02", Folder[needs_merge = true]), + ("emptyemptyL0", Folder[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"emptyempty02".into()) + .content(Content::Folder { + title: "Empty".into(), + }); + local_tree_builder + .mutate(&"emptyemptyL0".into()) + .content(Content::Folder { + title: "Empty".into(), + }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("emptyempty01", Folder[needs_merge = true]), + ("emptyempty02", Folder[needs_merge = true]), + ("emptyempty03", Folder[needs_merge = true]) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"emptyempty01".into()) + .content(Content::Folder { + title: "Empty".into(), + }); + remote_tree_builder + .mutate(&"emptyempty02".into()) + .content(Content::Folder { + title: "Empty".into(), + }); + remote_tree_builder + .mutate(&"emptyempty03".into()) + .content(Content::Folder { + title: "Empty".into(), + }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("mobile______", LocalWithNewLocalStructure, { + ("emptyempty01", Remote), + ("emptyempty02", Remote), + ("emptyempty03", Remote) + }) + }); + let expected_telem = StructureCounts { + dupes: 1, + merged_nodes: 4, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +// Bug 747699: we should follow the hierarchy when merging, instead of +// deduping by parent title. +#[test] +fn deduping_ignores_parent_title() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("bookmarkAAA1", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder + .mutate(&"mobile______".into()) + .content(Content::Folder { + title: "Favoritos do celular".into(), + }); + local_tree_builder + .mutate(&"bookmarkAAA1".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + remote_tree_builder + .mutate(&"mobile______".into()) + .content(Content::Folder { + title: "Mobile Bookmarks".into(), + }); + remote_tree_builder + .mutate(&"bookmarkAAAA".into()) + .content(Content::Bookmark { + title: "A".into(), + url_href: "http://example.com/a".into(), + }); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("mobile______", LocalWithNewLocalStructure, { + ("bookmarkAAAA", Remote) + }) + }); + let expected_telem = StructureCounts { + dupes: 1, + merged_nodes: 2, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn mismatched_compatible_bookmark_kinds() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("queryAAAAAAA", Query[needs_merge = true]), + ("bookmarkBBBB", Bookmark[needs_merge = true, age = 5]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("queryAAAAAAA", Bookmark[needs_merge = true, age = 5]), + ("bookmarkBBBB", Query[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", Local, { + ("queryAAAAAAA", Local), + ("bookmarkBBBB", Remote) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 3, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn mismatched_incompatible_bookmark_kinds() { + before_each(); + + let local_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkAAAA", Folder[needs_merge = true, age = 5]) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + match merger.merge() { + Ok(_) => panic!("Should not merge trees with mismatched kinds"), + Err(err) => { + match err.kind() { + ErrorKind::MismatchedItemKind { .. } => {} + kind => panic!("Got {:?} merging trees with mismatched kinds", kind), + }; + } + }; +} + +#[test] +fn invalid_guids() { + before_each(); + + #[derive(Default)] + struct GenerateNewGuid(Cell<usize>); + + impl Driver for GenerateNewGuid { + fn generate_new_guid(&self, old_guid: &Guid) -> Result<Guid> { + let count = self.0.get(); + self.0.set(count + 1); + assert!( + &[")(*&", "shortGUID", "loooooongGUID", "!@#$%^", ""].contains(&old_guid.as_str()), + "Didn't expect to generate new GUID for {}", + old_guid + ); + Ok(format!("item{:0>8}", count).into()) + } + } + + let local_tree = nodes!({ + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("bookmarkAAAA", Bookmark[needs_merge = true, age = 5]), + ("bookmarkBBBB", Bookmark[needs_merge = true, age = 5]), + (")(*&", Bookmark[needs_merge = true, age = 5]) + }), + ("menu________", Folder[needs_merge = true], { + ("shortGUID", Bookmark[needs_merge = true]), + ("loooooongGUID", Bookmark[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("toolbar_____", Folder[needs_merge = true, age = 5], { + ("!@#$%^", Bookmark[needs_merge = true, age = 5]), + ("shortGUID", Bookmark[needs_merge = true, age = 5]), + ("", Bookmark[needs_merge = true, age = 5]), + ("loooooongGUID", Bookmark[needs_merge = true, age = 5]) + }), + ("menu________", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]), + ("bookmarkBBBB", Bookmark[needs_merge = true]) + }) + }) + .into_tree() + .unwrap(); + + let driver = GenerateNewGuid::default(); + let merger = Merger::with_driver(&driver, &DefaultAbortSignal, &local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("toolbar_____", LocalWithNewLocalStructure, { + ("item00000000", RemoteWithNewRemoteStructure), + ("item00000001", RemoteWithNewRemoteStructure), + ("item00000002", LocalWithNewLocalStructure) + }), + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkAAAA", Remote), + ("bookmarkBBBB", Remote), + ("item00000003", LocalWithNewLocalStructure), + ("item00000004", LocalWithNewLocalStructure) + }) + }); + let expected_deletions = &["", "!@#$%^", "loooooongGUID", "shortGUID"]; + let expected_telem = StructureCounts { + merged_nodes: 9, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn multiple_parents() { + before_each(); + + let local_tree = Tree::with_root(Item::new(ROOT_GUID, Kind::Folder)) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("toolbar_____", Folder[age = 5], { + ("bookmarkAAAA", Bookmark), + ("bookmarkBBBB", Bookmark), + ("folderCCCCCC", Folder, { + ("bookmarkDDDD", Bookmark), + ("bookmarkEEEE", Bookmark), + ("bookmarkFFFF", Bookmark) + }) + }), + ("menu________", Folder, { + ("bookmarkGGGG", Bookmark), + ("bookmarkAAAA", Bookmark), + ("folderCCCCCC", Folder, { + ("bookmarkHHHH", Bookmark), + ("bookmarkDDDD", Bookmark) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, UnchangedWithNewLocalStructure, { + ("toolbar_____", RemoteWithNewRemoteStructure, { + ("bookmarkBBBB", Remote) + }), + ("menu________", RemoteWithNewRemoteStructure, { + ("bookmarkGGGG", Remote), + ("bookmarkAAAA", RemoteWithNewRemoteStructure), + ("folderCCCCCC", RemoteWithNewRemoteStructure, { + ("bookmarkDDDD", RemoteWithNewRemoteStructure), + ("bookmarkEEEE", Remote), + ("bookmarkFFFF", Remote), + ("bookmarkHHHH", Remote) + }) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 10, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn reparent_orphans() { + before_each(); + + let local_tree = nodes!({ + ("toolbar_____", Folder, { + ("bookmarkAAAA", Bookmark), + ("bookmarkBBBB", Bookmark) + }), + ("unfiled_____", Folder, { + ("bookmarkCCCC", Bookmark) + }) + }) + .into_tree() + .unwrap(); + + let mut remote_tree_builder: Builder = nodes!({ + ("toolbar_____", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark), + ("bookmarkAAAA", Bookmark) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkDDDD", Bookmark[needs_merge = true]), + ("bookmarkCCCC", Bookmark) + }) + }) + .try_into() + .unwrap(); + remote_tree_builder + .item(Item { + guid: "bookmarkEEEE".into(), + kind: Kind::Bookmark, + age: 0, + needs_merge: true, + validity: Validity::Valid, + }) + .and_then(|p| p.by_parent_guid("toolbar_____".into())) + .expect("Should insert orphan E"); + remote_tree_builder + .item(Item { + guid: "bookmarkFFFF".into(), + kind: Kind::Bookmark, + age: 0, + needs_merge: true, + validity: Validity::Valid, + }) + .and_then(|p| p.by_parent_guid("nonexistent".into())) + .expect("Should insert orphan F"); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("toolbar_____", LocalWithNewLocalStructure, { + ("bookmarkBBBB", Unchanged), + ("bookmarkAAAA", Unchanged), + ("bookmarkEEEE", RemoteWithNewRemoteStructure) + }), + ("unfiled_____", LocalWithNewLocalStructure, { + ("bookmarkDDDD", Remote), + ("bookmarkCCCC", Unchanged), + ("bookmarkFFFF", RemoteWithNewRemoteStructure) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 8, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn deleted_user_content_roots() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + local_tree_builder + .deletion("mobile______".into()) + .deletion("toolbar_____".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]) + }) + })) + .unwrap(); + remote_tree_builder + .deletion("unfiled_____".into()) + .deletion("toolbar_____".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("unfiled_____", Local, { + ("bookmarkAAAA", Local) + }), + ("mobile______", Remote, { + ("bookmarkBBBB", Remote) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 4, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 1); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn moved_user_content_roots() { + before_each(); + + let local_tree = nodes!({ + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark[needs_merge = true]), + ("menu________", Folder[needs_merge = true], { + ("bookmarkBBBB", Bookmark[needs_merge = true]), + ("folderCCCCCC", Folder, { + ("bookmarkDDDD", Bookmark), + ("toolbar_____", Folder, { + ("bookmarkEEEE", Bookmark) + }) + }) + }) + }), + ("mobile______", Folder, { + ("bookmarkFFFF", Bookmark) + }) + }) + .into_tree() + .unwrap(); + + let remote_tree = nodes!({ + ("mobile______", Folder[needs_merge = true], { + ("toolbar_____", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]), + ("bookmarkEEEE", Bookmark) + }) + }), + ("menu________", Folder[needs_merge = true], { + ("bookmarkHHHH", Bookmark[needs_merge = true]), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkIIII", Bookmark[needs_merge = true]) + }) + }) + }) + .into_tree() + .unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!(ROOT_GUID, LocalWithNewLocalStructure, { + ("unfiled_____", LocalWithNewLocalStructure, { + ("bookmarkIIII", Remote), + ("bookmarkAAAA", Local) + }), + ("mobile______", Local, { + ("bookmarkFFFF", Local) + }), + ("menu________", LocalWithNewLocalStructure, { + ("bookmarkHHHH", Remote), + ("bookmarkBBBB", Local), + ("folderCCCCCC", LocalWithNewLocalStructure, { + ("bookmarkDDDD", Local) + }) + }), + ("toolbar_____", LocalWithNewLocalStructure, { + ("bookmarkGGGG", Remote), + ("bookmarkEEEE", Unchanged) + }) + }); + let expected_telem = StructureCounts { + merged_nodes: 13, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + assert_eq!(merged_root.deletions().count(), 0); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn cycle() { + before_each(); + + // Try to create a cycle: move A into B, and B into the menu, but keep + // B's parent by children as A. + let mut b: Builder = nodes!({ ("menu________", Folder) }).try_into().unwrap(); + + b.item(Item::new("folderAAAAAA".into(), Kind::Folder)) + .and_then(|p| p.by_parent_guid("folderBBBBBB".into())) + .expect("Should insert A"); + + b.item(Item::new("folderBBBBBB".into(), Kind::Folder)) + .and_then(|p| p.by_parent_guid("menu________".into())) + .and_then(|b| { + b.parent_for(&"folderBBBBBB".into()) + .by_children(&"folderAAAAAA".into()) + }) + .expect("Should insert B"); + + match b + .into_tree() + .expect_err("Should not build tree with cycles") + .kind() + { + ErrorKind::Cycle(guid) => assert_eq!(guid, &Guid::from("folderAAAAAA")), + err => panic!("Wrong error kind for cycle: {:?}", err), + } +} + +#[test] +fn reupload_replace() { + before_each(); + + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark) + }), + ("toolbar_____", Folder, { + ("folderBBBBBB", Folder, { + ("bookmarkCCCC", Bookmark[validity = Validity::Replace]) + }), + ("folderDDDDDD", Folder, { + ("bookmarkEEEE", Bookmark[validity = Validity::Replace]) + }) + }), + ("unfiled_____", Folder), + ("mobile______", Folder, { + ("bookmarkFFFF", Bookmark[validity = Validity::Replace]), + ("folderGGGGGG", Folder), + ("bookmarkHHHH", Bookmark[validity = Validity::Replace]) + }) + })) + .unwrap(); + local_tree_builder.deletion("bookmarkIIII".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark[validity = Validity::Replace]) + }), + ("toolbar_____", Folder, { + ("bookmarkJJJJ", Bookmark[validity = Validity::Replace]), + ("folderBBBBBB", Folder, { + ("bookmarkCCCC", Bookmark[validity = Validity::Replace]) + }), + ("folderDDDDDD", Folder) + }), + ("unfiled_____", Folder, { + ("bookmarkKKKK", Bookmark[validity = Validity::Reupload]) + }), + ("mobile______", Folder, { + ("bookmarkFFFF", Bookmark), + ("folderGGGGGG", Folder, { + ("bookmarkIIII", Bookmark[validity = Validity::Replace]) + }) + }) + })) + .unwrap(); + remote_tree_builder.deletion("bookmarkEEEE".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", Unchanged, { + // A is invalid remotely and valid locally, so replace. + ("bookmarkAAAA", Local) + }), + // Toolbar has new children. + ("toolbar_____", LocalWithNewLocalStructure, { + // B has new children. + ("folderBBBBBB", LocalWithNewLocalStructure), + ("folderDDDDDD", UnchangedWithNewLocalStructure) + }), + ("unfiled_____", UnchangedWithNewLocalStructure, { + // K was flagged for reupload. + ("bookmarkKKKK", RemoteWithNewRemoteStructure) + }), + ("mobile______", UnchangedWithNewLocalStructure, { + // F is invalid locally, so replace with remote. This isn't + // possible in Firefox Desktop or Rust Places, where the local + // tree is always valid, but we handle it for symmetry. + ("bookmarkFFFF", Remote), + ("folderGGGGGG", Local) + }) + }); + let expected_deletions = &[ + // C is invalid on both sides, so we need to upload a tombstone. + "bookmarkCCCC", + // E is invalid locally and deleted remotely, so doesn't need a + // tombstone. + "bookmarkEEEE", + // H is invalid locally and doesn't exist remotely, so doesn't need a + // tombstone. + "bookmarkHHHH", + // I is deleted locally and invalid remotely, so needs a tombstone. + "bookmarkIIII", + // J doesn't exist locally and invalid remotely, so needs a tombstone. + "bookmarkJJJJ", + ]; + let expected_telem = StructureCounts { + merged_nodes: 10, + ..StructureCounts::default() + }; + + assert_eq!(&expected_tree, merged_root.node()); + + let mut deletions = merged_root.deletions().collect::<Vec<_>>(); + deletions.sort(); + assert_eq!(deletions, expected_deletions); + + let ops = merged_root.completion_ops(); + let mut summary = ops.summarize(); + summary.sort(); + assert_eq!( + summary, + &[ + "Apply remote bookmarkFFFF", + "Apply remote bookmarkKKKK", + "Delete local item bookmarkCCCC", + "Delete local item bookmarkEEEE", + "Delete local item bookmarkHHHH", + "Flag local bookmarkAAAA as unmerged", + "Flag local bookmarkKKKK as unmerged", + "Flag local folderBBBBBB as unmerged", + "Flag local folderGGGGGG as unmerged", + "Flag local toolbar_____ as unmerged", + "Flag remote bookmarkEEEE as merged", + "Insert local tombstone bookmarkCCCC", + "Insert local tombstone bookmarkJJJJ", + "Move bookmarkKKKK into unfiled_____ at 0", + "Upload item bookmarkAAAA", + "Upload item bookmarkKKKK", + "Upload item folderBBBBBB", + "Upload item folderGGGGGG", + "Upload item toolbar_____", + "Upload tombstone bookmarkCCCC", + "Upload tombstone bookmarkIIII", + "Upload tombstone bookmarkJJJJ", + ] + ); + + assert_eq!(merged_root.counts(), &expected_telem); +} + +#[test] +fn completion_ops() { + let mut local_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder, { + ("bookmarkAAAA", Bookmark), + ("bookmarkBBBB", Bookmark), + ("bookmarkCCCC", Bookmark), + ("bookmarkDDDD", Bookmark) + }), + ("toolbar_____", Folder, { + ("bookmarkEEEE", Bookmark) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkIIII", Bookmark) + }), + ("mobile______", Folder, { + ("bookmarkFFFF", Bookmark[needs_merge = true, age = 10]) + }) + })) + .unwrap(); + local_tree_builder.deletion("bookmarkJJJJ".into()); + let local_tree = local_tree_builder.into_tree().unwrap(); + + let mut remote_tree_builder = Builder::try_from(nodes!({ + ("menu________", Folder[needs_merge = true], { + ("bookmarkAAAA", Bookmark), + ("bookmarkDDDD", Bookmark), + ("bookmarkCCCC", Bookmark), + ("bookmarkBBBB", Bookmark), + ("bookmarkEEEE", Bookmark[needs_merge = true]) + }), + ("toolbar_____", Folder[needs_merge = true], { + ("bookmarkGGGG", Bookmark[needs_merge = true]) + }), + ("unfiled_____", Folder[needs_merge = true], { + ("bookmarkHHHH", Bookmark[needs_merge = true]), + ("bookmarkJJJJ", Bookmark) + }), + ("mobile______", Folder, { + ("bookmarkFFFF", Bookmark[needs_merge = true, age = 5]) + }) + })) + .unwrap(); + remote_tree_builder.deletion("bookmarkIIII".into()); + let remote_tree = remote_tree_builder.into_tree().unwrap(); + + let merger = Merger::new(&local_tree, &remote_tree); + let merged_root = merger.merge().unwrap(); + + let expected_tree = merged_nodes!({ + ("menu________", UnchangedWithNewLocalStructure, { + ("bookmarkAAAA", Unchanged), + ("bookmarkDDDD", Unchanged), + ("bookmarkCCCC", Unchanged), + ("bookmarkBBBB", Unchanged), + ("bookmarkEEEE", Remote) + }), + ("toolbar_____", UnchangedWithNewLocalStructure, { + ("bookmarkGGGG", Remote) + }), + ("unfiled_____", LocalWithNewLocalStructure, { + ("bookmarkHHHH", Remote) + }), + ("mobile______", Unchanged, { + ("bookmarkFFFF", Remote) + }) + }); + + assert_eq!(&expected_tree, merged_root.node()); + + let ops = merged_root.completion_ops(); + assert!(ops.change_guids.is_empty()); + assert_eq!( + to_strings(&ops.apply_remote_items).collect::<Vec<_>>(), + &[ + "Apply remote bookmarkEEEE", + "Apply remote bookmarkGGGG", + "Apply remote bookmarkHHHH", + "Apply remote bookmarkFFFF", + ] + ); + assert_eq!( + to_strings(&ops.apply_new_local_structure).collect::<Vec<_>>(), + &[ + "Move bookmarkDDDD into menu________ at 1", + "Move bookmarkBBBB into menu________ at 3", + "Move bookmarkEEEE into menu________ at 4", + "Move bookmarkGGGG into toolbar_____ at 0", + "Move bookmarkHHHH into unfiled_____ at 0", + ] + ); + assert!(ops.set_local_unmerged.is_empty()); + assert_eq!( + to_strings(&ops.set_local_merged).collect::<Vec<_>>(), + &["Flag local bookmarkFFFF as merged"] + ); + assert_eq!( + to_strings(&ops.set_remote_merged).collect::<Vec<_>>(), + &[ + "Flag remote menu________ as merged", + "Flag remote bookmarkEEEE as merged", + "Flag remote toolbar_____ as merged", + "Flag remote bookmarkGGGG as merged", + "Flag remote bookmarkHHHH as merged", + "Flag remote bookmarkFFFF as merged", + "Flag remote bookmarkIIII as merged", + ] + ); + let mut delete_local_items = to_strings(&ops.delete_local_items).collect::<Vec<_>>(); + delete_local_items.sort(); + assert_eq!(delete_local_items, &["Delete local item bookmarkIIII"]); + assert!(ops.insert_local_tombstones.is_empty()); + assert_eq!( + to_strings(&ops.upload_items).collect::<Vec<_>>(), + &["Upload item unfiled_____"] + ); + let mut upload_tombstones = to_strings(&ops.upload_tombstones).collect::<Vec<_>>(); + upload_tombstones.sort(); + assert_eq!(upload_tombstones, &["Upload tombstone bookmarkJJJJ"]); +} + +#[test] +fn problems() { + let mut problems = Problems::default(); + + problems + .note(&"bookmarkAAAA".into(), Problem::Orphan) + .note( + &"menu________".into(), + Problem::MisparentedRoot(vec![DivergedParent::ByChildren("unfiled_____".into())]), + ) + .note(&"toolbar_____".into(), Problem::MisparentedRoot(Vec::new())) + .note( + &"bookmarkBBBB".into(), + Problem::DivergedParents(vec![ + DivergedParent::ByChildren("folderCCCCCC".into()), + DivergedParentGuid::Folder("folderDDDDDD".into()).into(), + ]), + ) + .note( + &"bookmarkEEEE".into(), + Problem::DivergedParents(vec![ + DivergedParent::ByChildren("folderFFFFFF".into()), + DivergedParentGuid::NonFolder("bookmarkGGGG".into()).into(), + ]), + ) + .note(&"bookmarkRRRR".into(), Problem::InvalidItem) + .note( + &"bookmarkHHHH".into(), + Problem::DivergedParents(vec![ + DivergedParent::ByChildren("folderIIIIII".into()), + DivergedParent::ByChildren("folderJJJJJJ".into()), + DivergedParentGuid::Missing("folderKKKKKK".into()).into(), + ]), + ) + .note(&"bookmarkLLLL".into(), Problem::DivergedParents(Vec::new())) + .note( + &"folderMMMMMM".into(), + Problem::MissingChild { + child_guid: "bookmarkNNNN".into(), + }, + ) + .note( + &"folderMMMMMM".into(), + Problem::MissingChild { + child_guid: "bookmarkOOOO".into(), + }, + ) + .note( + &"bookmarkPPPP".into(), + Problem::DivergedParents(vec![ + DivergedParentGuid::Deleted("folderQQQQQQ".into()).into() + ]), + ) + .note(&"bookmarkQQQQ".into(), Problem::InvalidItem); + + let mut summary = problems.summarize().collect::<Vec<_>>(); + summary.sort_by(|a, b| a.guid().cmp(b.guid())); + assert_eq!( + to_strings(&summary).collect::<Vec<_>>(), + &[ + "bookmarkAAAA is an orphan", + "bookmarkBBBB is in children of folderCCCCCC and has parent folderDDDDDD", + "bookmarkEEEE is in children of folderFFFFFF and has non-folder parent bookmarkGGGG", + "bookmarkHHHH is in children of folderIIIIII, is in children of folderJJJJJJ, and has \ + nonexistent parent folderKKKKKK", + "bookmarkLLLL has diverged parents", + "bookmarkPPPP has deleted parent folderQQQQQQ", + "bookmarkQQQQ is invalid", + "bookmarkRRRR is invalid", + "folderMMMMMM has nonexistent child bookmarkNNNN", + "folderMMMMMM has nonexistent child bookmarkOOOO", + "menu________ is a user content root, but is in children of unfiled_____", + "toolbar_____ is a user content root", + ] + ); + + assert_eq!( + problems.counts(), + ProblemCounts { + orphans: 1, + misparented_roots: 2, + multiple_parents_by_children: 3, + deleted_parent_guids: 1, + missing_parent_guids: 1, + non_folder_parent_guids: 1, + parent_child_disagreements: 7, + deleted_children: 0, + missing_children: 2, + invalid_items: 2, + } + ); +} |