Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
firefox-merge-…
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Deferred
import Foundation
import Shared
@testable import Storage
@testable import Sync
import XCTest
extension Dictionary {
init<S: Sequence>(seq: S) where S.Iterator.Element == Element {
self.init()
for (k, v) in seq {
self[k] = v
}
}
}
class MockLocalItemSource: LocalItemSource {
var local: [GUID: BookmarkMirrorItem] = [:]
func getLocalItemWithGUID(_ guid: GUID) -> Deferred<Maybe<BookmarkMirrorItem>> {
guard let item = self.local[guid] else {
return deferMaybe(DatabaseError(description: "Couldn't find item \(guid)."))
}
return deferMaybe(item)
}
func getLocalItemsWithGUIDs<T: Collection>(_ guids: T) -> Deferred<Maybe<[GUID: BookmarkMirrorItem]>> where T.Iterator.Element == GUID {
var acc: [GUID: BookmarkMirrorItem] = [:]
guids.forEach { guid in
if let item = self.local[guid] {
acc[guid] = item
}
}
return deferMaybe(acc)
}
func prefetchLocalItemsWithGUIDs<T: Collection>(_ guids: T) -> Success where T.Iterator.Element == GUID {
return succeed()
}
}
class MockMirrorItemSource: MirrorItemSource {
var mirror: [GUID: BookmarkMirrorItem] = [:]
func getMirrorItemWithGUID(_ guid: GUID) -> Deferred<Maybe<BookmarkMirrorItem>> {
guard let item = self.mirror[guid] else {
return deferMaybe(DatabaseError(description: "Couldn't find item \(guid)."))
}
return deferMaybe(item)
}
func getMirrorItemsWithGUIDs<T: Collection>(_ guids: T) -> Deferred<Maybe<[GUID: BookmarkMirrorItem]>> where T.Iterator.Element == GUID {
var acc: [GUID: BookmarkMirrorItem] = [:]
guids.forEach { guid in
if let item = self.mirror[guid] {
acc[guid] = item
}
}
return deferMaybe(acc)
}
func prefetchMirrorItemsWithGUIDs<T: Collection>(_ guids: T) -> Success where T.Iterator.Element == GUID {
return succeed()
}
}
class MockBufferItemSource: BufferItemSource {
var buffer: [GUID: BookmarkMirrorItem] = [:]
func getBufferItemsWithGUIDs<T: Collection>(_ guids: T) -> Deferred<Maybe<[GUID: BookmarkMirrorItem]>> where T.Iterator.Element == GUID {
var acc: [GUID: BookmarkMirrorItem] = [:]
guids.forEach { guid in
if let item = self.buffer[guid] {
acc[guid] = item
}
}
return deferMaybe(acc)
}
func getBufferItemWithGUID(_ guid: GUID) -> Deferred<Maybe<BookmarkMirrorItem>> {
guard let item = self.buffer[guid] else {
return deferMaybe(DatabaseError(description: "Couldn't find item \(guid)."))
}
return deferMaybe(item)
}
func getBufferChildrenGUIDsForParent(_ guid: GUID) -> Deferred<Maybe<[GUID]>> {
return deferMaybe(DatabaseError(description: "Not implemented"))
}
func prefetchBufferItemsWithGUIDs<T: Collection>(_ guids: T) -> Success where T.Iterator.Element == GUID {
return succeed()
}
}
class MockUploader {
var deletions: Set<GUID> = Set<GUID>()
var added: Set<GUID> = Set<GUID>()
var records: [GUID: Record<BookmarkBasePayload>] = [:]
func getStorer() -> TrivialBookmarkStorer {
return TrivialBookmarkStorer(uploader: self.doUpload)
}
func doUpload(recs: [Record<BookmarkBasePayload>], lastTimestamp: Timestamp?, onUpload: (POSTResult, Timestamp) -> DeferredTimestamp) -> DeferredTimestamp {
var success: [GUID] = []
recs.forEach { rec in
success.append(rec.id)
self.records[rec.id] = rec
if rec.payload.deleted {
self.deletions.insert(rec.id)
} else {
self.added.insert(rec.id)
}
}
// Now pretend we did the upload.
return onUpload(POSTResult(success: success, failed: [:]), Date.now())
}
}
// Thieved mercilessly from TestSQLiteBookmarks.
private func getBrowserDBForFile(filename: String, files: FileAccessor) -> BrowserDB? {
let db = BrowserDB(filename: filename, schema: BrowserSchema(), files: files)
db.touch().succeeded() // Ensure the schema is created/updated in advance
return db
}
class FailFastTestCase: XCTestCase {
// This is how to make an assertion failure stop the current test function
// but continue with other test functions in the same test case.
// See http://stackoverflow.com/a/27016786/22003
override func invokeTest() {
self.continueAfterFailure = false
defer { self.continueAfterFailure = true }
super.invokeTest()
}
}
class TestBookmarkTreeMerging: FailFastTestCase {
let files = MockFiles()
override func tearDown() {
do {
try self.files.removeFilesInDirectory()
} catch {
}
super.tearDown()
}
private func getBrowserDB(name: String) -> BrowserDB? {
let file = "TBookmarkTreeMerging\(name).db"
return getBrowserDBForFile(filename: file, files: self.files)
}
private func mockStatsSessionForBookmarks() -> SyncEngineStatsSession {
let session = SyncEngineStatsSession(collection: "bookmarks")
session.start()
return session
}
func getSyncableBookmarks(name: String) -> MergedSQLiteBookmarks? {
guard let db = self.getBrowserDB(name: name) else {
XCTFail("Couldn't get prepared DB.")
return nil
}
return MergedSQLiteBookmarks(db: db)
}
func getSQLiteBookmarks(name: String) -> SQLiteBookmarks? {
guard let db = self.getBrowserDB(name: name) else {
XCTFail("Couldn't get prepared DB.")
return nil
}
return SQLiteBookmarks(db: db)
}
func dbLocalTree(name: String) -> BookmarkTree? {
guard let bookmarks = self.getSQLiteBookmarks(name: name) else {
XCTFail("Couldn't get bookmarks.")
return nil
}
return bookmarks.treeForLocal().value.successValue
}
func localTree() -> BookmarkTree {
let roots = BookmarkRoots.RootChildren.map { BookmarkTreeNode.folder(guid: $0, children: []) }
let places = BookmarkTreeNode.folder(guid: BookmarkRoots.RootGUID, children: roots)
var lookup: [GUID: BookmarkTreeNode] = [:]
var parents: [GUID: GUID] = [:]
for n in roots {
lookup[n.recordGUID] = n
parents[n.recordGUID] = BookmarkRoots.RootGUID
}
lookup[BookmarkRoots.RootGUID] = places
return BookmarkTree(subtrees: [places], lookup: lookup, parents: parents, orphans: Set(), deleted: Set(), modified: Set(lookup.keys), virtual: Set())
}
// Our synthesized tree is the same as the one we pull out of a brand new local DB.
func testLocalTreeAssumption() {
let constructed = self.localTree()
let fromDB = self.dbLocalTree(name: "A")
XCTAssertNotNil(fromDB)
XCTAssertTrue(fromDB!.isFullyRootedIn(constructed))
XCTAssertTrue(constructed.isFullyRootedIn(fromDB!))
XCTAssertTrue(fromDB!.virtual.isEmpty)
XCTAssertTrue(constructed.virtual.isEmpty)
}
// This should never occur in the wild: local will never be empty.
func testMergingEmpty() {
let r = BookmarkTree.emptyTree()
let m = BookmarkTree.emptyMirrorTree()
let l = BookmarkTree.emptyTree()
let s = ItemSources(local: MockLocalItemSource(), mirror: MockMirrorItemSource(), buffer: MockBufferItemSource())
XCTAssertEqual(m.virtual, BookmarkRoots.Real)
let merger = ThreeWayTreeMerger(local: l, mirror: m, remote: r, itemSources: s)
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't merge.")
return
}
mergedTree.dump()
XCTAssertEqual(mergedTree.allGUIDs, BookmarkRoots.Real)
guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else {
XCTFail("Couldn't produce result.")
return
}
// We don't test the local override completion, because of course we saw mirror items.
XCTAssertTrue(result.uploadCompletion.isNoOp)
XCTAssertTrue(result.bufferCompletion.isNoOp)
}
func getItemSourceIncludingEmptyRoots() -> ItemSources {
let local = MockLocalItemSource()
func makeRoot(guid: GUID, _ name: String) {
local.local[guid] = BookmarkMirrorItem.folder(guid, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: name, description: nil, children: [])
}
makeRoot(guid: BookmarkRoots.MenuFolderGUID, "Bookmarks Menu")
makeRoot(guid: BookmarkRoots.ToolbarFolderGUID, "Bookmarks Toolbar")
makeRoot(guid: BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks")
makeRoot(guid: BookmarkRoots.UnfiledFolderGUID, "Unsorted Bookmarks")
makeRoot(guid: BookmarkRoots.RootGUID, "")
return ItemSources(local: local, mirror: MockMirrorItemSource(), buffer: MockBufferItemSource())
}
func testMergingOnlyLocalRoots() {
let r = BookmarkTree.emptyTree()
let m = BookmarkTree.emptyMirrorTree()
let l = self.localTree()
let s = self.getItemSourceIncludingEmptyRoots()
let merger = ThreeWayTreeMerger(local: l, mirror: m, remote: r, itemSources: s)
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't merge.")
return
}
mergedTree.dump()
XCTAssertEqual(mergedTree.allGUIDs, BookmarkRoots.Real)
guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else {
XCTFail("Couldn't produce result.")
return
}
XCTAssertFalse(result.isNoOp)
XCTAssertTrue(result.overrideCompletion.processedLocalChanges.isSuperset(of: Set(BookmarkRoots.RootChildren)))
// We should be dropping the roots from local, and uploading roots to the server.
// The outgoing records should use Sync-style IDs.
// We never upload the places root.
let banned = Set<GUID>(["places", BookmarkRoots.RootGUID])
XCTAssertTrue(banned.isDisjoint(with: Set(result.uploadCompletion.records.map { $0.id })))
XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromBuffer.keys))
XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromMirror.keys))
XCTAssertTrue(banned.isDisjoint(with: result.uploadCompletion.amendChildrenFromLocal.keys))
XCTAssertEqual(Set(BookmarkRoots.RootChildren), Set(result.uploadCompletion.amendChildrenFromLocal.keys))
XCTAssertEqual(Set(BookmarkRoots.Real), result.overrideCompletion.processedLocalChanges)
}
func testMergingStorageLocalRootsEmptyServer() {
guard let bookmarks = self.getSyncableBookmarks(name: "B") else {
XCTFail("Couldn't get bookmarks.")
return
}
// The mirror is never actually empty.
let mirrorTree = bookmarks.treeForMirror().value.successValue!
XCTAssertFalse(mirrorTree.isEmpty)
XCTAssertEqual(mirrorTree.lookup.keys.count, 5) // Root and four children.
XCTAssertEqual(1, mirrorTree.subtrees.count)
let edgesBefore = bookmarks.treesForEdges().value.successValue!
XCTAssertFalse(edgesBefore.local.isEmpty)
XCTAssertTrue(edgesBefore.buffer.isEmpty)
let uploader = MockUploader()
let storer = uploader.getStorer()
let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
applier.go().succeeded()
// Now the local contents are replicated into the mirror, and both the buffer and local are empty.
guard let mirror = bookmarks.treeForMirror().value.successValue else {
XCTFail("Couldn't get mirror!")
return
}
XCTAssertFalse(mirror.isEmpty)
XCTAssertTrue(mirror.subtrees[0].recordGUID == BookmarkRoots.RootGUID)
let edgesAfter = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesAfter.local.isEmpty)
XCTAssertTrue(edgesAfter.buffer.isEmpty)
XCTAssertEqual(uploader.added, Set(BookmarkRoots.RootChildren.map(BookmarkRoots.translateOutgoingRootGUID)))
}
func testComplexOrphaning() {
// This test describes a scenario like this:
//
// [] [] []
// [M] [T] [M] [T] [M] [T]
// | | | | | |
// [C] [A] [C] [A] [C] [A]
// | | | |
// [D] [D] [B] [B]
// | |
// F E
//
// That is: we have locally added 'E' to folder B and deleted folder D,
// and remotely added 'F' to folder D and deleted folder B.
//
// This is a fundamental conflict that would ordinarily produce orphans.
// Our resolution for this is to put those orphans _somewhere_.
//
// That place is the lowest surviving parent: walk the tree until we find
// a folder that still exists, and put the orphans there. This is a little
// better than just dumping the records into Unsorted Bookmarks, and no
// more complex than dumping them into the closest root.
//
// We expect:
//
// []
// [M] [T]
// | |
// [C] [A]
// | |
// F E
//
guard let bookmarks = self.getSyncableBookmarks(name: "G") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Set up the mirror.
let dateAdded = Date.now() - 200000
let mirrorDate = Date.now() - 100000
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren),
BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["folderCCCCCC"]),
BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]),
BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: ["folderBBBBBB"]),
BookmarkMirrorItem.folder("folderBBBBBB", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "B", description: "", children: []),
BookmarkMirrorItem.folder("folderCCCCCC", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "C", description: "", children: ["folderDDDDDD"]),
BookmarkMirrorItem.folder("folderDDDDDD", dateAdded: dateAdded, modified: mirrorDate, hasDupe: false, parentID: "folderCCCCCC", parentName: "C", title: "D", description: "", children: []),
]
bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate)
bookmarks.wipeLocal()
// Now the buffer is empty, and the mirror tree is what we expect.
let mirrorTree = bookmarks.treeForMirror().value.successValue!
XCTAssertFalse(mirrorTree.isEmpty)
XCTAssertEqual(mirrorTree.lookup.keys.count, 9)
XCTAssertEqual(1, mirrorTree.subtrees.count)
XCTAssertEqual(mirrorTree.find("folderAAAAAA")!.children!.map { $0.recordGUID }, ["folderBBBBBB"])
XCTAssertEqual(mirrorTree.find("folderCCCCCC")!.children!.map { $0.recordGUID }, ["folderDDDDDD"])
let edgesBefore = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced.
XCTAssertTrue(edgesBefore.buffer.isEmpty)
// Set up the buffer.
let bufferDate = Date.now()
let changedBufferRecords = [
BookmarkMirrorItem.deleted(BookmarkNodeType.folder, guid: "folderBBBBBB", modified: bufferDate),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: []),
BookmarkMirrorItem.folder("folderDDDDDD", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: "folderCCCCCC", parentName: "C", title: "D", description: "", children: ["bookmarkFFFF"]),
BookmarkMirrorItem.bookmark("bookmarkFFFF", dateAdded: dateAdded, modified: bufferDate, hasDupe: false, parentID: "folderDDDDDD", parentName: "D", title: "F", description: nil, URI: "http://example.com/f", tags: "", keyword: nil),
]
bookmarks.applyRecords(changedBufferRecords).succeeded()
// Make local changes.
bookmarks.local.modelFactory.value.successValue!.removeByGUID("folderDDDDDD").succeeded()
bookmarks.local.insertBookmark("http://example.com/e".asURL!, title: "E", favicon: nil, intoFolder: "folderBBBBBB", withTitle: "B").succeeded()
let insertedGUID = bookmarks.local.db.getGUIDs("SELECT guid FROM bookmarksLocal WHERE title IS 'E'")[0]
let edges = bookmarks.treesForEdges().value.successValue!
XCTAssertFalse(edges.local.isEmpty)
XCTAssertFalse(edges.buffer.isEmpty)
XCTAssertTrue(edges.local.isFullyRootedIn(mirrorTree))
XCTAssertTrue(edges.buffer.isFullyRootedIn(mirrorTree))
XCTAssertTrue(edges.buffer.deleted.contains("folderBBBBBB"))
XCTAssertFalse(edges.buffer.deleted.contains("folderDDDDDD"))
XCTAssertFalse(edges.local.deleted.contains("folderBBBBBB"))
XCTAssertTrue(edges.local.deleted.contains("folderDDDDDD"))
// Now merge.
let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks)
let merger = storageMerger.getMerger().value.successValue!
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
// Dump it so we can see it.
mergedTree.dump()
XCTAssertTrue(mergedTree.deleteLocally.contains("folderBBBBBB"))
XCTAssertTrue(mergedTree.deleteFromMirror.contains("folderBBBBBB"))
XCTAssertTrue(mergedTree.deleteRemotely.contains("folderDDDDDD"))
XCTAssertTrue(mergedTree.deleteFromMirror.contains("folderDDDDDD"))
XCTAssertTrue(mergedTree.acceptLocalDeletion.contains("folderDDDDDD"))
XCTAssertTrue(mergedTree.acceptRemoteDeletion.contains("folderBBBBBB"))
// E and F still exist, in Menu and Toolbar respectively.
// Note that the merge itself includes asserts for this; we shouldn't even get here if
// this part will fail.
XCTAssertTrue(mergedTree.allGUIDs.contains(insertedGUID))
XCTAssertTrue(mergedTree.allGUIDs.contains("bookmarkFFFF"))
let menu = mergedTree.root.mergedChildren![0] // menu, toolbar, unfiled, mobile, so 0.
let toolbar = mergedTree.root.mergedChildren![1] // menu, toolbar, unfiled, mobile, so 1.
XCTAssertEqual(BookmarkRoots.MenuFolderGUID, menu.guid)
XCTAssertEqual(BookmarkRoots.ToolbarFolderGUID, toolbar.guid)
let folderC = menu.mergedChildren![0]
let folderA = toolbar.mergedChildren![0]
XCTAssertEqual("folderCCCCCC", folderC.guid)
XCTAssertEqual("folderAAAAAA", folderA.guid)
XCTAssertEqual(insertedGUID, folderA.mergedChildren![0].guid)
XCTAssertEqual("bookmarkFFFF", folderC.mergedChildren![0].guid)
guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
XCTAssertFalse(result.isNoOp)
// Everything in local is getting dropped.
let formerLocalIDs = Set(edges.local.lookup.keys)
XCTAssertTrue(result.overrideCompletion.processedLocalChanges.isSuperset(of: formerLocalIDs))
// Everything in the buffer is getting dropped.
let formerBufferIDs = Set(edges.buffer.lookup.keys)
XCTAssertTrue(result.bufferCompletion.processedBufferChanges.isSuperset(of: formerBufferIDs))
// The mirror will now contain everything added by each side, and not the deleted folders.
XCTAssertEqual(result.overrideCompletion.mirrorItemsToDelete, Set(["folderBBBBBB", "folderDDDDDD"]))
XCTAssertTrue(result.overrideCompletion.mirrorValuesToCopyFromLocal.isEmpty)
XCTAssertTrue(result.overrideCompletion.mirrorValuesToCopyFromBuffer.isEmpty)
XCTAssertTrue(result.overrideCompletion.mirrorItemsToInsert.keys.contains(insertedGUID)) // Because it was reparented.
XCTAssertTrue(result.overrideCompletion.mirrorItemsToInsert.keys.contains("bookmarkFFFF")) // Because it was reparented.
// The mirror now has the right structure.
XCTAssertEqual(result.overrideCompletion.mirrorStructures["folderCCCCCC"]!, ["bookmarkFFFF"])
XCTAssertEqual(result.overrideCompletion.mirrorStructures["folderAAAAAA"]!, [insertedGUID])
// We're going to upload new records for C (lost a child), F (changed parent),
// insertedGUID (new local record), A (new child), and D (deleted).
// Anything that was in the mirror and only changed structure:
let expected: [GUID: [GUID]] = ["folderCCCCCC": ["bookmarkFFFF"], "folderAAAAAA": [insertedGUID]]
let actual: [GUID: [GUID]] = result.uploadCompletion.amendChildrenFromMirror
XCTAssertEqual(actual as NSDictionary, expected as NSDictionary)
// Anything deleted:
XCTAssertTrue(result.uploadCompletion.records.contains(where: { (($0.id == "folderDDDDDD") && ($0.payload["deleted"].bool ?? false)) }))
// Inserted:
let uploadE = result.uploadCompletion.records.find { $0.id == insertedGUID }
XCTAssertNotNil(uploadE)
XCTAssertEqual(uploadE!.payload["title"].stringValue, "E")
// We fixed the parent before uploading.
XCTAssertEqual(uploadE!.payload["parentid"].stringValue, "folderAAAAAA")
XCTAssertEqual(uploadE!.payload["parentName"].string ?? "", "A")
// Reparented:
let uploadF = result.uploadCompletion.records.find { $0.id == "bookmarkFFFF" }
XCTAssertNotNil(uploadF)
XCTAssertEqual(uploadF!.payload["title"].stringValue, "F")
// We fixed the parent before uploading.
XCTAssertEqual(uploadF!.payload["parentid"].stringValue, "folderCCCCCC")
XCTAssertEqual(uploadF!.payload["parentName"].string ?? "", "C")
}
func testComplexMoveWithAdditions() {
// This test describes a scenario like this:
//
// [] [] []
// [X] [Y] [X] [Y] [X] [Y]
// | | | |
// [A] C [A] [A]
// / \ / \ / | \
// B E B C B D C
//
// That is: we have locally added 'D' to folder A, and remotely moved
// A to a different root, added 'E', and moved 'C' back to the old root.
//
// Our expected result is:
//
// []
// [X] [Y]
// | |
// [A] C
// / | \
// B D E
//
// … but we'll settle for any order of children for [A] that preserves B < E
// and B < D -- in other words, (B E D) and (B D E) are both acceptable.
//
guard let bookmarks = self.getSyncableBookmarks(name: "F") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Set up the mirror.
let mirrorDate = Date.now() - 100000
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren),
BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["folderAAAAAA"]),
BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: []),
BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "Bookmarks Menu", title: "A", description: "", children: ["bookmarkBBBB", "bookmarkCCCC"]),
BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "B", description: nil, URI: "http://example.com/b", tags: "", keyword: nil),
BookmarkMirrorItem.bookmark("bookmarkCCCC", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "C", description: nil, URI: "http://example.com/c", tags: "", keyword: nil),
]
bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate)
bookmarks.wipeLocal()
// Now the buffer is empty, and the mirror tree is what we expect.
let mirrorTree = bookmarks.treeForMirror().value.successValue!
XCTAssertFalse(mirrorTree.isEmpty)
XCTAssertEqual(mirrorTree.lookup.keys.count, 8)
XCTAssertEqual(1, mirrorTree.subtrees.count)
XCTAssertEqual(mirrorTree.find("folderAAAAAA")!.children!.map { $0.recordGUID }, ["bookmarkBBBB", "bookmarkCCCC"])
let edgesBefore = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced.
XCTAssertTrue(edgesBefore.buffer.isEmpty)
// Set up the buffer.
let bufferDate = Date.now()
let changedBufferRecords = [
BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: ["bookmarkCCCC"]),
BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: ["bookmarkBBBB", "bookmarkEEEE"]),
BookmarkMirrorItem.bookmark("bookmarkCCCC", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: BookmarkRoots.MenuFolderGUID, parentName: "A", title: "C", description: nil, URI: "http://example.com/c", tags: "", keyword: nil),
BookmarkMirrorItem.bookmark("bookmarkEEEE", dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "E", description: nil, URI: "http://example.com/e", tags: "", keyword: nil),
]
bookmarks.applyRecords(changedBufferRecords).succeeded()
let populatedBufferTree = bookmarks.treesForEdges().value.successValue!.buffer
XCTAssertFalse(populatedBufferTree.isEmpty)
XCTAssertTrue(populatedBufferTree.isFullyRootedIn(mirrorTree))
XCTAssertFalse(populatedBufferTree.find("bookmarkEEEE")!.isUnknown)
XCTAssertFalse(populatedBufferTree.find("bookmarkCCCC")!.isUnknown)
XCTAssertTrue(populatedBufferTree.find("bookmarkBBBB")!.isUnknown)
// Now let's make local changes with the API.
bookmarks.local.insertBookmark("http://example.com/d".asURL!, title: "D (local)", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded()
let populatedLocalTree = bookmarks.treesForEdges().value.successValue!.local
let newMirrorTree = bookmarks.treeForMirror().value.successValue!
XCTAssertEqual(1, newMirrorTree.subtrees.count)
XCTAssertFalse(populatedLocalTree.isEmpty)
XCTAssertTrue(populatedLocalTree.isFullyRootedIn(mirrorTree))
XCTAssertTrue(populatedLocalTree.isFullyRootedIn(newMirrorTree)) // It changed.
XCTAssertNil(populatedLocalTree.find("bookmarkEEEE"))
XCTAssertTrue(populatedLocalTree.find("bookmarkCCCC")!.isUnknown)
XCTAssertTrue(populatedLocalTree.find("bookmarkBBBB")!.isUnknown)
XCTAssertFalse(populatedLocalTree.find("folderAAAAAA")!.isUnknown)
// Now merge.
let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks)
let merger = storageMerger.getMerger().value.successValue!
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
// Dump it so we can see it.
mergedTree.dump()
guard let menu = mergedTree.root.mergedChildren?[0] else {
XCTFail("Expected a child of the root.")
return
}
XCTAssertEqual(menu.guid, BookmarkRoots.MenuFolderGUID)
XCTAssertEqual(menu.mergedChildren?[0].guid, "bookmarkCCCC")
guard let toolbar = mergedTree.root.mergedChildren?[1] else {
XCTFail("Expected a second child of the root.")
return
}
XCTAssertEqual(toolbar.guid, BookmarkRoots.ToolbarFolderGUID)
let toolbarChildren = toolbar.mergedChildren!
XCTAssertEqual(toolbarChildren.count, 1) // A.
let aaa = toolbarChildren[0]
XCTAssertEqual(aaa.guid, "folderAAAAAA")
let aaaChildren = aaa.mergedChildren!
XCTAssertEqual(aaaChildren.count, 3) // B, E, new local.
XCTAssertFalse(aaaChildren.contains { $0.guid == "bookmarkCCCC" })
}
func testApplyingTwoEmptyFoldersDoesntSmush() {
guard let bookmarks = self.getSyncableBookmarks(name: "C") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Insert two identical folders. We mark them with hasDupe because that's the Syncy
// thing to do.
let now = Date.now()
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: ["emptyempty01", "emptyempty02"]),
BookmarkMirrorItem.folder("emptyempty01", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []),
BookmarkMirrorItem.folder("emptyempty02", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []),
]
bookmarks.buffer.applyRecords(records).succeeded()
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 3)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 2)
let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks)
let merger = storageMerger.getMerger().value.successValue!
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
// Dump it so we can see it.
mergedTree.dump()
// Now let's look at the tree.
XCTAssertTrue(mergedTree.deleteFromMirror.isEmpty)
XCTAssertEqual(BookmarkRoots.RootGUID, mergedTree.root.guid)
XCTAssertEqual(BookmarkRoots.RootGUID, mergedTree.root.mirror?.recordGUID)
XCTAssertNil(mergedTree.root.remote)
XCTAssertTrue(MergeState<BookmarkMirrorItem>.unchanged == mergedTree.root.valueState)
XCTAssertTrue(MergeState<BookmarkTreeNode>.unchanged == mergedTree.root.structureState)
XCTAssertEqual(4, mergedTree.root.mergedChildren?.count)
guard let mergedMobile = mergedTree.root.mergedChildren?[BookmarkRoots.RootChildren.index(of: BookmarkRoots.MobileFolderGUID) ?? -1] else {
XCTFail("Didn't get a merged mobile folder.")
return
}
XCTAssertEqual(BookmarkRoots.MobileFolderGUID, mergedMobile.guid)
XCTAssertTrue(MergeState<BookmarkMirrorItem>.remote == mergedMobile.valueState)
// This ends up as Remote because we didn't change any of its structure
// when compared to the incoming record.
guard case MergeState<BookmarkTreeNode>.remote = mergedMobile.structureState else {
XCTFail("Didn't get expected Remote state.")
return
}
XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.remote!.children!.map { $0.recordGUID })
XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.asMergedTreeNode().children!.map { $0.recordGUID })
XCTAssertEqual(["emptyempty01", "emptyempty02"], mergedMobile.mergedChildren!.map { $0.guid })
let empty01 = mergedMobile.mergedChildren![0]
let empty02 = mergedMobile.mergedChildren![1]
XCTAssertNil(empty01.local)
XCTAssertNil(empty02.local)
XCTAssertNil(empty01.mirror)
XCTAssertNil(empty02.mirror)
XCTAssertNotNil(empty01.remote)
XCTAssertNotNil(empty02.remote)
XCTAssertEqual("emptyempty01", empty01.remote!.recordGUID)
XCTAssertEqual("emptyempty02", empty02.remote!.recordGUID)
XCTAssertTrue(MergeState<BookmarkTreeNode>.remote == empty01.structureState)
XCTAssertTrue(MergeState<BookmarkTreeNode>.remote == empty02.structureState)
XCTAssertTrue(MergeState<BookmarkMirrorItem>.remote == empty01.valueState)
XCTAssertTrue(MergeState<BookmarkMirrorItem>.remote == empty02.valueState)
XCTAssertTrue(empty01.mergedChildren?.isEmpty ?? false)
XCTAssertTrue(empty02.mergedChildren?.isEmpty ?? false)
guard let result = merger.produceMergeResultFromMergedTree(mergedTree).value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
let uploader = MockUploader()
let storer = uploader.getStorer()
let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
applier.applyResult(result).succeeded()
guard let mirror = bookmarks.treeForMirror().value.successValue else {
XCTFail("Couldn't get mirror!")
return
}
// After merge, the buffer and local are empty.
let edgesAfter = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesAfter.local.isEmpty)
XCTAssertTrue(edgesAfter.buffer.isEmpty)
// When merged in, we do not smush these two records together!
XCTAssertFalse(mirror.isEmpty)
XCTAssertTrue(mirror.subtrees[0].recordGUID == BookmarkRoots.RootGUID)
XCTAssertNotNil(mirror.find("emptyempty01"))
XCTAssertNotNil(mirror.find("emptyempty02"))
XCTAssertTrue(mirror.deleted.isEmpty)
guard let mobile = mirror.find(BookmarkRoots.MobileFolderGUID) else {
XCTFail("No mobile folder in mirror.")
return
}
if case let .folder(_, children) = mobile {
XCTAssertEqual(children.map { $0.recordGUID }, ["emptyempty01", "emptyempty02"])
} else {
XCTFail("Mobile isn't a folder.")
}
}
/**
* Input:
*
* [M] [] [M]
* _______|_______ _____|_____
* / | \ / \
* [e01] [e02] [e03] [e02] [eL0]
*
* Expected output:
*
* Take remote, delete emptyemptyL0, upload the roots that
* weren't remotely present.
*/
func testApplyingTwoEmptyFoldersMatchesOnlyOne() {
guard let bookmarks = self.getSyncableBookmarks(name: "D") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Insert three identical folders. We mark them with hasDupe because that's the Syncy
// thing to do.
let now = Date.now()
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: ["emptyempty01", "emptyempty02", "emptyempty03"]),
BookmarkMirrorItem.folder("emptyempty01", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []),
BookmarkMirrorItem.folder("emptyempty02", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []),
BookmarkMirrorItem.folder("emptyempty03", dateAdded: now, modified: now, hasDupe: true, parentID: BookmarkRoots.MobileFolderGUID, parentName: "Mobile Bookmarks", title: "Empty", description: "", children: []),
]
bookmarks.buffer.validate().succeeded() // It's valid! Empty.
bookmarks.buffer.applyRecords(records).succeeded()
bookmarks.buffer.validate().succeeded() // It's valid! Rooted in mobile_______.
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 4)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 3)
// Add one matching empty folder locally.
// Add one by GUID, too. This is the most complex possible case.
bookmarks.local.db.run("INSERT INTO bookmarksLocal (guid, type, date_added, title, parentid, parentName, sync_status, local_modified) VALUES ('emptyempty02', \(BookmarkNodeType.folder.rawValue), \(now), 'Empty', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.changed.rawValue), \(Date.now()))").succeeded()
bookmarks.local.db.run("INSERT INTO bookmarksLocal (guid, type, date_added, title, parentid, parentName, sync_status, local_modified) VALUES ('emptyemptyL0', \(BookmarkNodeType.folder.rawValue), \(now), 'Empty', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.new.rawValue), \(Date.now()))").succeeded()
bookmarks.local.db.run("INSERT INTO bookmarksLocalStructure (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'emptyempty02', 0)").succeeded()
bookmarks.local.db.run("INSERT INTO bookmarksLocalStructure (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'emptyemptyL0', 1)").succeeded()
let uploader = MockUploader()
let storer = uploader.getStorer()
let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
applier.go().succeeded()
guard let mirror = bookmarks.treeForMirror().value.successValue else {
XCTFail("Couldn't get mirror!")
return
}
// After merge, the buffer and local are empty.
let edgesAfter = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesAfter.local.isEmpty)
XCTAssertTrue(edgesAfter.buffer.isEmpty)
// All of the incoming records exist.
XCTAssertFalse(mirror.isEmpty)
XCTAssertEqual(mirror.subtrees[0].recordGUID, BookmarkRoots.RootGUID)
XCTAssertNotNil(mirror.find("emptyempty01"))
XCTAssertNotNil(mirror.find("emptyempty02"))
XCTAssertNotNil(mirror.find("emptyempty03"))
// The other roots are now in the mirror.
XCTAssertTrue(BookmarkRoots.Real.every({ mirror.find($0) != nil }))
// And are queued for upload.
XCTAssertTrue(uploader.added.contains("toolbar"))
XCTAssertTrue(uploader.added.contains("menu"))
XCTAssertTrue(uploader.added.contains("unfiled"))
XCTAssertFalse(uploader.added.contains("mobile")) // Structure and value didn't change.
// The local record that was smushed is not present…
XCTAssertNil(mirror.find("emptyemptyL0"))
// … and because it was marked New, we don't bother trying to delete it.
XCTAssertFalse(uploader.deletions.contains("emptyemptyL0"))
guard let mobile = mirror.find(BookmarkRoots.MobileFolderGUID) else {
XCTFail("No mobile folder in mirror.")
return
}
if case let .folder(_, children) = mobile {
// This order isn't strictly specified, but try to preserve the remote order if we can.
XCTAssertEqual(children.map { $0.recordGUID }, ["emptyempty01", "emptyempty02", "emptyempty03"])
} else {
XCTFail("Mobile isn't a folder.")
}
}
// TODO: this test should be extended to also exercise the case of a conflict.
func testLocalRecordsKeepTheirFavicon() {
guard let bookmarks = self.getSyncableBookmarks(name: "E") else {
XCTFail("Couldn't get bookmarks.")
return
}
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 0)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 0)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksLocal", int: 5)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksLocalStructure", int: 4)
bookmarks.local.db.run("INSERT INTO favicons (id, url, width, height, type, date) VALUES (11, 'http://example.org/favicon.ico', 16, 16, 0, \(Date.now()))").succeeded()
bookmarks.local.db.run("INSERT INTO bookmarksLocal (guid, type, date_added, title, parentid, parentName, sync_status, bmkUri, faviconID) VALUES ('somebookmark', \(BookmarkNodeType.bookmark.rawValue), \(Date.now()), 'Some Bookmark', '\(BookmarkRoots.MobileFolderGUID)', 'Mobile Bookmarks', \(SyncStatus.new.rawValue), 'http://example.org/', 11)").succeeded()
bookmarks.local.db.run("INSERT INTO bookmarksLocalStructure (parent, child, idx) VALUES ('\(BookmarkRoots.MobileFolderGUID)', 'somebookmark', 0)").succeeded()
let uploader = MockUploader()
let storer = uploader.getStorer()
let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
applier.go().succeeded()
// After merge, the buffer and local are empty.
let edgesAfter = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesAfter.local.isEmpty)
XCTAssertTrue(edgesAfter.buffer.isEmpty)
// New record was uploaded.
XCTAssertTrue(uploader.added.contains("somebookmark"))
// So were all the roots, with the Sync-native names.
XCTAssertTrue(uploader.added.contains("toolbar"))
XCTAssertTrue(uploader.added.contains("menu"))
XCTAssertTrue(uploader.added.contains("unfiled"))
XCTAssertTrue(uploader.added.contains("mobile"))
// … but not the Places root.
XCTAssertFalse(uploader.added.contains("places"))
XCTAssertFalse(uploader.added.contains(BookmarkRoots.RootGUID))
// Their parent IDs are translated.
XCTAssertEqual(uploader.records["mobile"]?.payload["parentid"].string, "places")
XCTAssertEqual(uploader.records["somebookmark"]?.payload["parentid"].string, "mobile")
XCTAssertTrue(uploader.deletions.isEmpty)
// The record looks sane.
let bm = uploader.records["somebookmark"]!
XCTAssertEqual("bookmark", bm.payload["type"].string)
XCTAssertEqual("Some Bookmark", bm.payload["title"].string)
// New record still has its icon ID in the local DB.
bookmarks.local.db.assertQueryReturns("SELECT faviconID FROM bookmarksMirror WHERE bmkUri = 'http://example.org/'", int: 11)
}
func testRemoteValueOnlyChange() {
guard let bookmarks = self.getSyncableBookmarks(name: "F") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Insert one folder and one child.
let now = Date.now()
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: Date.now(), modified: now, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: ["folderAAAAAA"]),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: now, modified: now, hasDupe: false, parentID: BookmarkRoots.UnfiledFolderGUID, parentName: "Unsorted Bookmarks", title: "Folder A", description: "", children: ["bookmarkBBBB"]),
BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: now, modified: now, hasDupe: false, parentID: "folderAAAAAA", parentName: "Folder A", title: "Initial Title", description: "No desc.", URI: "http://example.org/foo", tags: "", keyword: nil),
]
bookmarks.buffer.validate().succeeded() // It's valid! Empty.
bookmarks.buffer.applyRecords(records).succeeded()
bookmarks.buffer.validate().succeeded() // It's valid! Rooted in mobile_______.
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 3)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 2)
let uploader = MockUploader()
let storer = uploader.getStorer()
let applier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: storer, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
applier.go().succeeded()
guard let _ = bookmarks.treeForMirror().value.successValue else {
XCTFail("Couldn't get mirror!")
return
}
// After merge, the buffer and local are empty.
let edgesAfter = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesAfter.local.isEmpty)
XCTAssertTrue(edgesAfter.buffer.isEmpty)
// Check the title.
let folder = bookmarks.modelFactory.value.successValue!
.modelForFolder("folderAAAAAA").value.successValue!.current[0]!
XCTAssertEqual(folder.title, "Initial Title")
// Now process an incoming change.
let changed = [
BookmarkMirrorItem.bookmark("bookmarkBBBB", dateAdded: now, modified: now, hasDupe: false, parentID: "folderAAAAAA", parentName: "Folder A", title: "New Title", description: "No desc.", URI: "http://example.org/foo", tags: "", keyword: nil),
]
bookmarks.buffer.validate().succeeded() // It's valid! Empty.
bookmarks.buffer.applyRecords(changed).succeeded()
bookmarks.buffer.validate().succeeded() // It's valid! One record.
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 1)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 0)
let uu = MockUploader()
let ss = uu.getStorer()
let changeApplier = MergeApplier(buffer: bookmarks, storage: bookmarks, client: ss, statsSession: mockStatsSessionForBookmarks(), greenLight: { true })
changeApplier.go().succeeded()
// The title changed.
let updatedFolder = bookmarks.modelFactory.value.successValue!
.modelForFolder("folderAAAAAA").value.successValue!.current[0]!
XCTAssertEqual(updatedFolder.title, "New Title")
// The buffer is empty.
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBuffer", int: 0)
bookmarks.buffer.db.assertQueryReturns("SELECT count(*) FROM bookmarksBufferStructure", int: 0)
}
func testMergeDateAdded() {
guard let bookmarks = self.getSyncableBookmarks(name: "H") else {
XCTFail("Couldn't get bookmarks.")
return
}
// Set up the mirror.
let mirrorDate = Date.now() - 100000
let records = [
BookmarkMirrorItem.folder(BookmarkRoots.RootGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "", description: "", children: BookmarkRoots.RootChildren),
BookmarkMirrorItem.folder(BookmarkRoots.MenuFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Menu", description: "", children: []),
BookmarkMirrorItem.folder(BookmarkRoots.UnfiledFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Unsorted Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder(BookmarkRoots.ToolbarFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Bookmarks Toolbar", description: "", children: ["folderAAAAAA"]),
BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.RootGUID, parentName: "", title: "Mobile Bookmarks", description: "", children: []),
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: []),
]
bookmarks.populateMirrorViaBuffer(items: records, atDate: mirrorDate)
bookmarks.wipeLocal()
// Now the buffer is empty, and the mirror tree is what we expect.
let mirrorTree = bookmarks.treeForMirror().value.successValue!
XCTAssertFalse(mirrorTree.isEmpty)
XCTAssertEqual(mirrorTree.lookup.keys.count, 6)
XCTAssertEqual(1, mirrorTree.subtrees.count)
let edgesBefore = bookmarks.treesForEdges().value.successValue!
XCTAssertTrue(edgesBefore.local.isEmpty) // Because we're fully synced.
XCTAssertTrue(edgesBefore.buffer.isEmpty)
// Make local changes.
bookmarks.local.insertBookmark("http://example.com/f".asURL!, title: "F", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded()
let insertedGUID = bookmarks.local.db.getGUIDs("SELECT guid FROM bookmarksLocal WHERE title IS 'F'")[0]
bookmarks.local.insertBookmark("http://example.com/g".asURL!, title: "G", favicon: nil, intoFolder: "folderAAAAAA", withTitle: "A").succeeded()
let insertedGUID2 = bookmarks.local.db.getGUIDs("SELECT guid FROM bookmarksLocal WHERE title IS 'G'")[0]
// Set up the buffer.
let bufferDate = Date.now() - 100000
let dateAfter = Date.now()
let changedBufferRecords = [
BookmarkMirrorItem.folder("folderAAAAAA", dateAdded: mirrorDate, modified: mirrorDate, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "A", description: "", children: [insertedGUID, insertedGUID2]),
BookmarkMirrorItem.bookmark(insertedGUID, dateAdded: bufferDate, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "F", description: nil, URI: "http://example.com/f", tags: "", keyword: nil),
BookmarkMirrorItem.bookmark(insertedGUID2, dateAdded: dateAfter, modified: bufferDate, hasDupe: false, parentID: "folderAAAAAA", parentName: "A", title: "G", description: nil, URI: "http://example.com/g", tags: "", keyword: nil),
]
bookmarks.applyRecords(changedBufferRecords).succeeded()
// Now merge.
let storageMerger = ThreeWayBookmarksStorageMerger(buffer: bookmarks, storage: bookmarks)
let merger = storageMerger.getMerger().value.successValue!
guard let mergedTree = merger.produceMergedTree().value.successValue else {
XCTFail("Couldn't get merge result.")
return
}
mergedTree.dump()
let folderChildren = mergedTree.root.mergedChildren!.find{ $0.guid == BookmarkRoots.ToolbarFolderGUID }!.mergedChildren!.find { $0.guid == "folderAAAAAA" }!.mergedChildren!
if case .new(let value) = folderChildren[0].valueState {
XCTAssertEqual(value.dateAdded, bufferDate)
} else {
XCTFail()
}
guard case .local = folderChildren[1].valueState else {
XCTFail()
return
}
}
}
class TestMergedTree: FailFastTestCase {
func testInitialState() {
let children = BookmarkRoots.RootChildren.map { BookmarkTreeNode.unknown(guid: $0) }
let root = BookmarkTreeNode.folder(guid: BookmarkRoots.RootGUID, children: children)
let tree = MergedTree(mirrorRoot: root)
XCTAssertTrue(tree.root.hasDecidedChildren)
if case let .folder(guid, unmergedChildren) = tree.root.asUnmergedTreeNode() {
XCTAssertEqual(guid, BookmarkRoots.RootGUID)
XCTAssertEqual(unmergedChildren, children)
} else {
XCTFail("Root should start as Folder.")
}
// We haven't processed the children.
XCTAssertNil(tree.root.mergedChildren)
XCTAssertTrue(tree.root.asMergedTreeNode().isUnknown)
// Simulate a merge.
let mergedRoots = children.map { MergedTreeNode(guid: $0.recordGUID, mirror: $0, structureState: MergeState.unchanged) }
tree.root.mergedChildren = mergedRoots
// Now we have processed children.
XCTAssertNotNil(tree.root.mergedChildren)
XCTAssertFalse(tree.root.asMergedTreeNode().isUnknown)
}
}
private extension MergedSQLiteBookmarks {
func populateMirrorViaBuffer(items: [BookmarkMirrorItem], atDate mirrorDate: Timestamp) {
self.applyRecords(items).succeeded()
// … and add the root relationships that will be missing (we don't do those for the buffer,
// so we need to manually add them and move them across).
let sql = """
INSERT INTO bookmarksBufferStructure
(parent, child, idx)
VALUES
('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MenuFolderGUID)', 0),
('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.ToolbarFolderGUID)', 1),
('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.UnfiledFolderGUID)', 2),
('\(BookmarkRoots.RootGUID)', '\(BookmarkRoots.MobileFolderGUID)', 3)
"""
self.buffer.db.run(sql).succeeded()
// Move it all to the mirror.
self.local.db.moveBufferToMirrorForTesting()
}
func wipeLocal() {
self.local.db.run(["DELETE FROM bookmarksLocalStructure", "DELETE FROM bookmarksLocal"]).succeeded()
}
}