/** * Original iOS version by Jens Alfke * Ported to Android by Marty Schoch * <p/> * Copyright (c) 2012 Couchbase, Inc. All rights reserved. * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ package com.couchbase.lite; import com.couchbase.lite.internal.RevisionInternal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class RevTreeTest extends LiteTestCaseWithDB { public static final String TAG = "RevTree"; public void testForceInsertEmptyHistory() throws CouchbaseLiteException { List<String> revHistory = null; RevisionInternal rev = new RevisionInternal("FakeDocId", "1-1111", false); Map<String, Object> revProperties = new HashMap<String, Object>(); revProperties.put("_id", rev.getDocID()); revProperties.put("_rev", rev.getRevID()); revProperties.put("message", "hi"); rev.setProperties(revProperties); database.forceInsert(rev, revHistory, null); } /** * in DatabaseInternal_Tests.m * - (void) test06_RevTree */ public void testRevTree() throws CouchbaseLiteException { RevisionInternal rev = new RevisionInternal("MyDocId", "4-4444", false); Map<String, Object> revProperties = new HashMap<String, Object>(); revProperties.put("_id", rev.getDocID()); revProperties.put("_rev", rev.getRevID()); revProperties.put("message", "hi"); rev.setProperties(revProperties); List<String> revHistory = new ArrayList<String>(); revHistory.add(rev.getRevID()); revHistory.add("3-3333"); revHistory.add("2-2222"); revHistory.add("1-1111"); database.forceInsert(rev, revHistory, null); assertEquals(1, database.getDocumentCount()); verifyHistory(database, rev, revHistory); RevisionInternal conflict = new RevisionInternal("MyDocId", "5-5555", false); Map<String, Object> conflictProperties = new HashMap<String, Object>(); conflictProperties.put("_id", conflict.getDocID()); conflictProperties.put("_rev", conflict.getRevID()); conflictProperties.put("message", "yo"); conflict.setProperties(conflictProperties); List<String> conflictHistory = new ArrayList<String>(); conflictHistory.add(conflict.getRevID()); conflictHistory.add("4-4545"); conflictHistory.add("3-3030"); conflictHistory.add("2-2222"); conflictHistory.add("1-1111"); final List wasInConflict = new ArrayList(); final Database.ChangeListener listener = new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { if (event.getChanges().get(0).isConflict()) { wasInConflict.add(new Object()); } } }; database.addChangeListener(listener); database.forceInsert(conflict, conflictHistory, null); assertTrue(wasInConflict.size() > 0); database.removeChangeListener(listener); assertEquals(1, database.getDocumentCount()); verifyHistory(database, conflict, conflictHistory); // Add an unrelated document: RevisionInternal other = new RevisionInternal("AnotherDocID", "1-1010", false); Map<String, Object> otherProperties = new HashMap<String, Object>(); otherProperties.put("language", "jp"); other.setProperties(otherProperties); List<String> otherHistory = new ArrayList<String>(); otherHistory.add(other.getRevID()); database.forceInsert(other, otherHistory, null); // Fetch one of those phantom revisions with no body: RevisionInternal rev2 = database.getDocument(rev.getDocID(), "2-2222", true); assertNull(rev2); // NOT FOUND // Make sure no duplicate rows were inserted for the common revisions: assertEquals(isSQLiteDB()?8:3, database.getLastSequenceNumber()); // Make sure the revision with the higher revID wins the conflict: RevisionInternal current = database.getDocument(rev.getDocID(), null, true); assertEquals(conflict, current); // Get the _changes feed and verify only the winner is in it: ChangesOptions options = new ChangesOptions(); RevisionList changes = database.changesSince(0, options, null, null); RevisionList expectedChanges = new RevisionList(); expectedChanges.add(conflict); expectedChanges.add(other); assertEquals(expectedChanges, changes); options.setIncludeConflicts(true); changes = database.changesSince(0, options, null, null); expectedChanges = new RevisionList(); if(isSQLiteDB()) { expectedChanges.add(rev); expectedChanges.add(conflict); expectedChanges.add(other); } else{ expectedChanges.add(conflict); expectedChanges.add(rev); expectedChanges.add(other); } assertEquals(expectedChanges, changes); } /** * Test that the public API works as expected in change notifications after a rev tree * insertion. See https://github.com/couchbase/couchbase-lite-android-core/pull/27 */ public void testRevTreeChangeNotifications() throws CouchbaseLiteException { final String DOCUMENT_ID = "MyDocId"; // add a document with a single (first) revision final RevisionInternal rev = new RevisionInternal(DOCUMENT_ID, "1-1111", false); Map<String, Object> revProperties = new HashMap<String, Object>(); revProperties.put("_id", rev.getDocID()); revProperties.put("_rev", rev.getRevID()); revProperties.put("message", "hi"); rev.setProperties(revProperties); List<String> revHistory = Arrays.asList(rev.getRevID()); Database.ChangeListener listener = new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { assertEquals(1, event.getChanges().size()); DocumentChange change = event.getChanges().get(0); assertEquals(DOCUMENT_ID, change.getDocumentId()); assertEquals(rev.getRevID(), change.getRevisionId()); assertTrue(change.isCurrentRevision()); assertFalse(change.isConflict()); SavedRevision current = database.getDocument( change.getDocumentId()).getCurrentRevision(); assertEquals(rev.getRevID(), current.getId()); } }; database.addChangeListener(listener); database.forceInsert(rev, revHistory, null); database.removeChangeListener(listener); // add two more revisions to the document final RevisionInternal rev3 = new RevisionInternal(DOCUMENT_ID, "3-3333", false); Map<String, Object> rev3Properties = new HashMap<String, Object>(); rev3Properties.put("_id", rev3.getDocID()); rev3Properties.put("_rev", rev3.getRevID()); rev3Properties.put("message", "hi again"); rev3.setProperties(rev3Properties); List<String> rev3History = Arrays.asList(rev3.getRevID(), "2-2222", rev.getRevID()); listener = new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { assertEquals(1, event.getChanges().size()); DocumentChange change = event.getChanges().get(0); assertEquals(DOCUMENT_ID, change.getDocumentId()); assertEquals(rev3.getRevID(), change.getRevisionId()); assertTrue(change.isCurrentRevision()); assertFalse(change.isConflict()); Document doc = database.getDocument(change.getDocumentId()); assertEquals(rev3.getRevID(), doc.getCurrentRevisionId()); try { assertEquals(3, doc.getRevisionHistory().size()); } catch (CouchbaseLiteException ex) { fail("CouchbaseLiteException in change listener: " + ex.toString()); } } }; database.addChangeListener(listener); database.forceInsert(rev3, rev3History, null); database.removeChangeListener(listener); // add a conflicting revision, with the same history length as the last revision we // inserted. Since this new revision's revID has a higher ASCII sort, it should become the // new winning revision. final RevisionInternal conflictRev = new RevisionInternal(DOCUMENT_ID, "3-4444", false); Map<String, Object> conflictProperties = new HashMap<String, Object>(); conflictProperties.put("_id", conflictRev.getDocID()); conflictProperties.put("_rev", conflictRev.getRevID()); conflictProperties.put("message", "winner"); conflictRev.setProperties(conflictProperties); List<String> conflictRevHistory = Arrays.asList(conflictRev.getRevID(), "2-2222", rev.getRevID()); listener = new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { assertEquals(1, event.getChanges().size()); DocumentChange change = event.getChanges().get(0); assertEquals(DOCUMENT_ID, change.getDocumentId()); assertEquals(conflictRev.getRevID(), change.getRevisionId()); assertTrue(change.isCurrentRevision()); assertTrue(change.isConflict()); Document doc = database.getDocument(change.getDocumentId()); assertEquals(conflictRev.getRevID(), doc.getCurrentRevisionId()); try { assertEquals(2, doc.getConflictingRevisions().size()); assertEquals(3, doc.getRevisionHistory().size()); } catch (CouchbaseLiteException ex) { fail("CouchbaseLiteException in change listener: " + ex.toString()); } } }; database.addChangeListener(listener); database.forceInsert(conflictRev, conflictRevHistory, null); database.removeChangeListener(listener); } private static void verifyHistory(Database db, RevisionInternal rev, List<String> history) { RevisionInternal gotRev = db.getDocument(rev.getDocID(), null, true); assertEquals(rev, gotRev); assertEquals(rev.getProperties(), gotRev.getProperties()); List<RevisionInternal> revHistory = db.getRevisionHistory(gotRev); assertEquals(history.size(), revHistory.size()); for (int i = 0; i < history.size(); i++) { RevisionInternal hrev = revHistory.get(i); assertEquals(rev.getDocID(), hrev.getDocID()); assertEquals(history.get(i), hrev.getRevID()); assertFalse(rev.isDeleted()); } } }