This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
1104 lines (912 sloc)
56.6 KB
| /* 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() | |
| } | |
| } |