/** * Copyright (c) 2016 Couchbase, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ package com.couchbase.lite; import com.couchbase.lite.internal.RevisionInternal; import com.couchbase.lite.support.RevisionUtils; import com.couchbase.lite.util.Log; import junit.framework.Assert; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class RevisionsTest extends LiteTestCaseWithDB { public void testParseRevID() { int num; String suffix; num = RevisionUtils.parseRevIDNumber("1-utiopturoewpt"); Assert.assertEquals(1, num); suffix = RevisionUtils.parseRevIDSuffix("1-utiopturoewpt"); Assert.assertEquals("utiopturoewpt", suffix); num = RevisionUtils.parseRevIDNumber("321-fdjfdsj-e"); Assert.assertEquals(321, num); suffix = RevisionUtils.parseRevIDSuffix("321-fdjfdsj-e"); Assert.assertEquals("fdjfdsj-e", suffix); num = RevisionUtils.parseRevIDNumber("0-fdjfdsj-e"); suffix = RevisionUtils.parseRevIDSuffix("0-fdjfdsj-e"); Assert.assertTrue(num == 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("-4-fdjfdsj-e"); suffix = RevisionUtils.parseRevIDSuffix("-4-fdjfdsj-e"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("5_fdjfdsj-e"); suffix = RevisionUtils.parseRevIDSuffix("5_fdjfdsj-e"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber(" 5-fdjfdsj-e"); suffix = RevisionUtils.parseRevIDSuffix(" 5-fdjfdsj-e"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("7 -foo"); suffix = RevisionUtils.parseRevIDSuffix("7 -foo"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("7-"); suffix = RevisionUtils.parseRevIDSuffix("7-"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("7"); suffix = RevisionUtils.parseRevIDSuffix("7"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber("eiuwtiu"); suffix = RevisionUtils.parseRevIDSuffix("eiuwtiu"); Assert.assertTrue(num < 0 || (suffix.length() == 0)); num = RevisionUtils.parseRevIDNumber(""); suffix = RevisionUtils.parseRevIDSuffix(""); Assert.assertTrue(num < 0 || (suffix.length() == 0)); } public void testCBLCompareRevIDs() { // Single Digit Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("1-foo", "1-foo") == 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("2-bar", "1-foo") > 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("1-foo", "2-bar") < 0); // Multi-digit: Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("123-bar", "456-foo") < 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("456-foo", "123-bar") > 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("456-foo", "456-foo") == 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("456-foo", "456-foofoo") < 0); // Different numbers of digits: Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("89-foo", "123-bar") < 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("123-bar", "89-foo") > 0); // Edge cases: Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("123-", "89-") > 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("123-a", "123-a") == 0); // Invalid rev IDs: Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("-a", "-b") < 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("-", "-") == 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("", "") == 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("", "-b") < 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("bogus", "yo") < 0); Assert.assertTrue(RevisionInternal.CBLCollateRevIDs("bogus-x", "yo-y") < 0); } public void testMakeRevisionHistoryDict() { List<RevisionInternal> revs = new ArrayList<RevisionInternal>(); revs.add(mkrev("4-jkl")); revs.add(mkrev("3-ghi")); revs.add(mkrev("2-def")); List<String> expectedSuffixes = new ArrayList<String>(); expectedSuffixes.add("jkl"); expectedSuffixes.add("ghi"); expectedSuffixes.add("def"); Map<String, Object> expectedHistoryDict = new HashMap<String, Object>(); expectedHistoryDict.put("start", 4); expectedHistoryDict.put("ids", expectedSuffixes); Map<String, Object> historyDict = RevisionUtils.makeRevisionHistoryDict(revs); Assert.assertEquals(expectedHistoryDict, historyDict); revs = new ArrayList<RevisionInternal>(); revs.add(mkrev("4-jkl")); revs.add(mkrev("2-def")); expectedSuffixes = new ArrayList<String>(); expectedSuffixes.add("4-jkl"); expectedSuffixes.add("2-def"); expectedHistoryDict = new HashMap<String, Object>(); expectedHistoryDict.put("ids", expectedSuffixes); historyDict = RevisionUtils.makeRevisionHistoryDict(revs); Assert.assertEquals(expectedHistoryDict, historyDict); revs = new ArrayList<RevisionInternal>(); revs.add(mkrev("12345")); revs.add(mkrev("6789")); expectedSuffixes = new ArrayList<String>(); expectedSuffixes.add("12345"); expectedSuffixes.add("6789"); expectedHistoryDict = new HashMap<String, Object>(); expectedHistoryDict.put("ids", expectedSuffixes); historyDict = RevisionUtils.makeRevisionHistoryDict(revs); Assert.assertEquals(expectedHistoryDict, historyDict); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/164 */ public void testRevisionIdDifferentRevisions() throws Exception { // two revisions with different json should have different rev-id's // because their content will have a different hash (even though // they have the same generation number) Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testCreateRevisions"); properties.put("tag", 1337); Document doc = database.createDocument(); UnsavedRevision newRev = doc.createRevision(); newRev.setUserProperties(properties); SavedRevision rev1 = newRev.save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); assertNotSame(rev2a.getId(), rev2b.getId()); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/164 */ public void testRevisionIdEquivalentRevisions() throws Exception { // This test causes crash with CBL Java on OSX // TODO: Github Ticket: https://github.com/couchbase/couchbase-lite-java/issues/55 if (System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik")) { // two revisions with the same content and the same json // should have the exact same revision id, because their content // will have an identical hash Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testCreateRevisions"); properties.put("tag", 1337); Map<String, Object> properties2 = new HashMap<String, Object>(); properties2.put("testName", "testCreateRevisions"); properties2.put("tag", 1338); Document doc = database.createDocument(); UnsavedRevision newRev = doc.createRevision(); newRev.setUserProperties(properties); SavedRevision rev1 = newRev.save(); UnsavedRevision newRev2a = rev1.createRevision(); newRev2a.setUserProperties(properties2); SavedRevision rev2a = newRev2a.save(); UnsavedRevision newRev2b = rev1.createRevision(); newRev2b.setUserProperties(properties2); SavedRevision rev2b = newRev2b.save(true); assertEquals(rev2a.getId(), rev2b.getId()); } } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/106 */ public void testResolveConflict() throws Exception { Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testCreateRevisions"); properties.put("tag", 1337); // Create a conflict on purpose Document doc = database.createDocument(); UnsavedRevision newRev1 = doc.createRevision(); newRev1.setUserProperties(properties); SavedRevision rev1 = newRev1.save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); SavedRevision winningRev = null; SavedRevision losingRev = null; if (doc.getCurrentRevisionId().equals(rev2a.getId())) { winningRev = rev2a; losingRev = rev2b; } else { winningRev = rev2b; losingRev = rev2a; } assertEquals(2, doc.getConflictingRevisions().size()); assertEquals(2, doc.getLeafRevisions().size()); // let's manually choose the losing rev as the winner. First, delete winner, which will // cause losing rev to be the current revision. SavedRevision deleteRevision = winningRev.deleteDocument(); List<SavedRevision> conflictingRevisions = doc.getConflictingRevisions(); assertEquals(1, conflictingRevisions.size()); assertEquals(2, doc.getLeafRevisions().size()); assertEquals(3, deleteRevision.getGeneration()); assertEquals(losingRev.getId(), doc.getCurrentRevision().getId()); // Finally create a new revision rev3 based on losing rev SavedRevision rev3 = createRevisionWithRandomProps(losingRev, true); assertEquals(rev3.getId(), doc.getCurrentRevisionId()); List<SavedRevision> conflictingRevisions1 = doc.getConflictingRevisions(); assertEquals(1, conflictingRevisions1.size()); assertEquals(2, doc.getLeafRevisions().size()); } public void testCorrectWinningRevisionTiebreaker() throws Exception { // Create a conflict on purpose Document doc = database.createDocument(); SavedRevision rev1 = doc.createRevision().save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); // the tiebreaker will happen based on which rev hash has lexicographically higher sort order SavedRevision expectedWinner = null; if (rev2a.getId().compareTo(rev2b.getId()) > 0) { expectedWinner = rev2a; } else if (rev2a.getId().compareTo(rev2b.getId()) < 0) { expectedWinner = rev2b; } RevisionInternal revFound = database.getDocument(doc.getId(), null, true); assertEquals(expectedWinner.getId(), revFound.getRevID()); } public void testCorrectWinningRevisionLongerBranch() throws Exception { // Create a conflict on purpose Document doc = database.createDocument(); SavedRevision rev1 = doc.createRevision().save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); SavedRevision rev3b = createRevisionWithRandomProps(rev2b, true); // rev3b should be picked as the winner since it has a longer branch SavedRevision expectedWinner = rev3b; RevisionInternal revFound = database.getDocument(doc.getId(), null, true); assertEquals(expectedWinner.getId(), revFound.getRevID()); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/135 */ public void testCorrectWinningRevisionHighRevisionNumber() throws Exception { // Create a conflict on purpose Document doc = database.createDocument(); SavedRevision rev1 = doc.createRevision().save(); SavedRevision rev2a = createRevisionWithRandomProps(rev1, false); SavedRevision rev2b = createRevisionWithRandomProps(rev1, true); SavedRevision rev3b = createRevisionWithRandomProps(rev2b, true); SavedRevision rev4b = createRevisionWithRandomProps(rev3b, true); SavedRevision rev5b = createRevisionWithRandomProps(rev4b, true); SavedRevision rev6b = createRevisionWithRandomProps(rev5b, true); SavedRevision rev7b = createRevisionWithRandomProps(rev6b, true); SavedRevision rev8b = createRevisionWithRandomProps(rev7b, true); SavedRevision rev9b = createRevisionWithRandomProps(rev8b, true); SavedRevision rev10b = createRevisionWithRandomProps(rev9b, true); RevisionInternal revFound = database.getDocument(doc.getId(), null, true); assertEquals(rev10b.getId(), revFound.getRevID()); } public void testDocumentChangeListener() throws Exception { Document doc = database.createDocument(); final CountDownLatch documentChanged = new CountDownLatch(1); doc.addChangeListener(new Document.ChangeListener() { @Override public void changed(Document.ChangeEvent event) { DocumentChange docChange = event.getChange(); String msg = "New revision added: %s. Conflict: %s"; msg = String.format(Locale.ENGLISH, msg, docChange.getAddedRevision(), docChange.isConflict()); Log.d(TAG, msg); documentChanged.countDown(); } }); doc.createRevision().save(); boolean success = documentChanged.await(30, TimeUnit.SECONDS); assertTrue(success); } public void testRevisionSequence() throws CouchbaseLiteException { Document doc = database.createDocument(); UnsavedRevision unsavedRev = doc.createRevision(); // An unsaved revision has no sequence number assertEquals(0, unsavedRev.getSequence()); // A new document has no parent and so there is no parent sequence number assertNull(unsavedRev.getParentId()); assertEquals(0, unsavedRev.getParentSequence()); SavedRevision rev = unsavedRev.save(); // The first revision of our database must have 1 has sequence number assertEquals(1, rev.getSequence()); // Since it has no parent rev, the parent sequence number is 0 assertNull(rev.getParentId()); assertEquals(0, rev.getParentSequence()); unsavedRev = doc.createRevision(); assertEquals(0, unsavedRev.getSequence()); assertEquals(1, unsavedRev.getParentSequence()); rev = unsavedRev.save(); assertEquals(2, rev.getSequence()); assertNotNull(rev.getParentId()); assertEquals(1, rev.getParentSequence()); } private static RevisionInternal mkrev(String revID) { return new RevisionInternal("docid", revID, false); } // https://github.com/couchbase/couchbase-lite-java-core/issues/878: public void testGenerateRevisionID() throws Exception { Map <String, Object> properties = new HashMap<String, Object>(); properties.put("_id", UUID.randomUUID()); properties.put("foo", "bar"); byte[] json1 = RevisionUtils.asCanonicalJSON(properties); assertNotNull(json1); assertEquals(13, json1.length); String revID1 = RevisionUtils.generateRevID(json1, false, null); assertEquals("1-aaa6c063924b64a141c98820efcc0022", revID1); properties = new HashMap<String, Object>(properties); properties.put("_rev", revID1); properties.put("tag", "1"); byte[] json2 = RevisionUtils.asCanonicalJSON(properties); assertNotNull(json2); assertEquals(23, json2.length); String revID2 = RevisionUtils.generateRevID(json2, false, revID1); assertEquals("2-cb1210b093cbdcdf5a353df2a898c891", revID2); properties = new HashMap<String, Object>(properties); properties.put("tag", "2"); byte[] json3 = RevisionUtils.asCanonicalJSON(properties); assertNotNull(json3); assertEquals(23, json3.length); String revID3 = RevisionUtils.generateRevID(json3, false, revID1); assertEquals("2-dc83321a829c8ae2849492b05478c9ed", revID3); } }