/** * Copyright (c) 2016 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.Body; import com.couchbase.lite.internal.RevisionInternal; import com.couchbase.lite.mockserver.MockDispatcher; import com.couchbase.lite.mockserver.MockHelper; import com.couchbase.lite.replicator.Replication; import com.couchbase.lite.support.FileDirUtils; import com.couchbase.lite.support.RevisionUtils; import com.couchbase.lite.util.IOUtils; import com.couchbase.lite.util.Log; import com.couchbase.lite.util.TextUtils; import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.mockwebserver.MockWebServer; public class DatabaseTest extends LiteTestCaseWithDB { private RevisionInternal putDoc(Map<String, Object> props) throws CouchbaseLiteException { RevisionInternal rev = new RevisionInternal(props); RevisionInternal result = database.putRevision(rev, (String) props.get("_rev"), false); assertNotNull(result.getRevID()); return result; } /** * in Database_Tests.m * - (void) test26_DocumentExpiry */ public void test26_DocumentExpiry() throws CouchbaseLiteException, InterruptedException { Calendar cal = new GregorianCalendar(); cal.add(Calendar.SECOND, 60);// +60 sec final Date future = cal.getTime(); Log.v(TAG, "Now is %s", new Date()); Map<String, Object> props = new HashMap<String, Object>(); props.put("foo", 17); props.put("_id", "12345"); Document doc = createDocWithProperties(props); assertNotNull(doc); assertNull(doc.getExpirationDate()); doc.setExpirationDate(future); Date exp = doc.getExpirationDate(); Log.v(TAG, "Doc expiration is %s", exp); assertNotNull(exp); long interval = exp.getTime() - future.getTime(); assertTrue(interval < 1 * 1000); // 1sec Date next = new Date(database.getStore().nextDocumentExpiry()); Log.v(TAG, "Next expiration at %s", next); doc.setExpirationDate(null); assertNull(doc.getExpirationDate()); assertEquals(0, database.getStore().nextDocumentExpiry()); // Can a nonexistent document have an expiration date? doc = database.getDocument("foo"); assertNull(doc.getExpirationDate()); doc.setExpirationDate(future); exp = doc.getExpirationDate(); Log.v(TAG, "Nonexistent doc expiration is %s", exp); assertEquals(1, database.getDocumentCount()); Log.v(TAG, "Creating documents"); createDocuments(database, 100); // 100 docs (Note 10K or 1K docs are too much for slow devices) assertEquals(101, database.getDocumentCount()); Log.v(TAG, "Marking docs for expiration"); final AtomicInteger total = new AtomicInteger(); final AtomicInteger marked = new AtomicInteger(); assertTrue(database.runInTransaction(new TransactionalTask() { @Override public boolean run() { try { QueryEnumerator e = database.createAllDocumentsQuery().run(); for (QueryRow row : e) { Document doc = row.getDocument(); if (doc.getProperties().containsKey("sequence")) { int sequence = (Integer) doc.getProperties().get("sequence"); if (sequence % 10 == 6) { Calendar time = new GregorianCalendar(); time.add(Calendar.SECOND, 2); // 2sec from now doc.setExpirationDate(time.getTime()); marked.incrementAndGet(); } else if (sequence % 10 == 3) { doc.setExpirationDate(future); // 30 sec from now } } total.incrementAndGet(); } return true; } catch (CouchbaseLiteException e) { Log.e(TAG, "Failed Database.createAllDocumentsQuery()", e); return false; } } })); assertEquals(101, total.get()); assertEquals(10, marked.get()); next = new Date(database.getStore().nextDocumentExpiry()); long diff = (next.getTime() - System.currentTimeMillis()) / 1000; Log.v(TAG, "Next expiration at %s (in %d sec)", next, diff); assertTrue(diff <= 2); // 2 sec assertTrue(diff >= -15); // -15 sec (-10sec could fail with slow machine) final CountDownLatch latch = new CountDownLatch(10); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); for (DocumentChange change : changes) { if (change.getRevisionId() == null) { latch.countDown(); } } } }); Log.v(TAG, "Waiting for auto expiration"); assertTrue(latch.await(15, TimeUnit.SECONDS)); total.set(0); int counter = 0; QueryEnumerator e = database.createAllDocumentsQuery().run(); for (QueryRow row : e) { Document d = row.getDocument(); if (d.getProperties() != null && d.getProperties().containsKey("sequence")) { int sequence = (Integer) d.getProperties().get("sequence"); assertTrue(sequence % 10 != 6); } total.incrementAndGet(); counter++; } assertEquals(91, total.get()); assertEquals(91, counter); next = new Date(database.getStore().nextDocumentExpiry()); Log.v(TAG, "Next expiration at %s", next); assertTrue(Math.abs(next.getTime() - future.getTime()) < 1 * 1000); // 1 sec } /** * in Database_Tests.m * - (void) test27_AbortedCommit */ public void test27_AbortedCommit() throws CouchbaseLiteException { // For https://github.com/couchbase/couchbase-lite-ios/issues/1437 // Test ported from https://github.com/couchbase/couchbase-lite-net/issues/732 database.runInTransaction(new TransactionalTask() { @Override public boolean run() { // Create a "rogue" document, then abort the transaction so it doesn't get saved: Document doc = database.getDocument("rogue"); Map<String, Object> props = new HashMap<String, Object>(); props.put("exists", false); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { fail(e.getMessage()); } return false; // Cancel the transaction! } }); // Create a doc for real: Document doc = database.getDocument("proper"); Map<String, Object> props = new HashMap<String, Object>(); props.put("exists", true); SavedRevision rev = doc.putProperties(props); assertNotNull(rev); // Verify the rogue doc doesn't exist: assertNull(database.getExistingDocument("rogue")); // Try to create it: Document doc2 = database.getDocument("rogue"); Map<String, Object> props2 = new HashMap<String, Object>(); props2.put("exists", 3); SavedRevision rev2 = doc2.putProperties(props2); assertNotNull(rev2); } /** * in DatabaseInternal_Tests.m * -(void) test18_FindMissingRevisions */ public void test18_FindMissingRevisions() throws CouchbaseLiteException { RevisionList revs = new RevisionList(); assertEquals(0, database.getStore().findMissingRevisions(revs)); Map<String, Object> prop1 = new HashMap<String, Object>(); prop1.put("_id", "11111"); prop1.put("key", "one"); RevisionInternal doc1r1 = putDoc(prop1); Map<String, Object> prop2 = new HashMap<String, Object>(); prop2.put("_id", "22222"); prop2.put("key", "two"); RevisionInternal doc2r1 = putDoc(prop2); Map<String, Object> prop3 = new HashMap<String, Object>(); prop3.put("_id", "33333"); prop3.put("key", "three"); RevisionInternal doc3r1 = putDoc(prop3); Map<String, Object> prop4 = new HashMap<String, Object>(); prop4.put("_id", "44444"); prop4.put("key", "four"); RevisionInternal doc4r1 = putDoc(prop4); Map<String, Object> prop5 = new HashMap<String, Object>(); prop5.put("_id", "55555"); prop5.put("key", "five"); RevisionInternal doc5r1 = putDoc(prop5); Map<String, Object> prop1r2 = new HashMap<String, Object>(); prop1r2.put("_id", "11111"); prop1r2.put("_rev", doc1r1.getRevID()); prop1r2.put("key", "one+"); RevisionInternal doc1r2 = putDoc(prop1r2); Map<String, Object> prop2r2 = new HashMap<String, Object>(); prop2r2.put("_id", "22222"); prop2r2.put("_rev", doc2r1.getRevID()); prop2r2.put("key", "two+"); RevisionInternal doc2r2 = putDoc(prop2r2); Map<String, Object> prop1r3 = new HashMap<String, Object>(); prop1r3.put("_id", "11111"); prop1r3.put("_rev", doc1r2.getRevID()); prop1r3.put("_deleted", true); RevisionInternal doc1r3 = putDoc(prop1r3); // Now call -findMissingRevisions: RevisionInternal revToFind1 = new RevisionInternal("11111", "3-6060", false); RevisionInternal revToFind2 = new RevisionInternal("22222", doc2r2.getRevID(), false); RevisionInternal revToFind3 = new RevisionInternal("99999", "9-4141", false); revs = new RevisionList(); revs.add(revToFind1); revs.add(revToFind2); revs.add(revToFind3); assertEquals(1, database.getStore().findMissingRevisions(revs)); assertEquals(2, revs.size()); assertTrue(revs.contains(revToFind1)); assertFalse(revs.contains(revToFind2)); assertTrue(revs.contains(revToFind3)); // Check the possible ancestors: AtomicBoolean haveBodies = new AtomicBoolean(); List<String> revIDs = database.getStore().getPossibleAncestorRevisionIDs(revToFind1, 0, haveBodies); assertEquals(2, revIDs.size()); assertTrue(revIDs.contains(doc1r2.getRevID())); assertTrue(revIDs.contains(doc1r1.getRevID())); revIDs = database.getStore().getPossibleAncestorRevisionIDs(revToFind1, 1, haveBodies); assertEquals(1, revIDs.size()); assertTrue(revIDs.contains(doc1r2.getRevID())); revIDs = database.getStore().getPossibleAncestorRevisionIDs(revToFind3, 0, haveBodies); assertNull(revIDs); } /** * in DatabaseInternal_Tests.m * -(void) test26_ReAddAfterPurge */ public void test26_ReAddAfterPurge() throws CouchbaseLiteException { String docId = "test26-ReAddAfterPurge"; RevisionInternal rev = new RevisionInternal(docId, "1-1111", false); Map<String, Object> props = new HashMap<String, Object>(); props.put("_id", rev.getDocID()); props.put("_rev", rev.getRevID()); props.put("testName", "test26_ReAddAfterPurge"); rev.setProperties(props); database.forceInsert(rev, null, null); Document redoc = database.getExistingDocument(docId); assertNotNull(redoc); Log.i(TAG, "Before purge, lastSequence = %d", database.getLastSequenceNumber()); Log.i(TAG, "PURGE"); redoc.purge(); Log.i(TAG, "After purge, lastSequence = %d", database.getLastSequenceNumber()); assertNull(database.getExistingDocument(docId)); reopenTestDB(); Log.i(TAG, "After reopen, lastSequence = %d", database.getLastSequenceNumber()); assertNull(database.getExistingDocument(docId)); RevisionInternal revAfterPurge = new RevisionInternal(docId, "1-1111", false); Map<String, Object> props2 = new HashMap<String, Object>(); props2.put("_id", revAfterPurge.getDocID()); props2.put("_rev", revAfterPurge.getRevID()); props2.put("testName", "test26_ReAddAfterPurge"); revAfterPurge.setProperties(props2); database.forceInsert(revAfterPurge, null, null); } /** * in DatabaseInternal_Tests.m * - (void) test27_ChangesSinceSequence */ public void test27_ChangesSinceSequence() throws CouchbaseLiteException { // Create 10 docs: createDocuments(database, 10); // Create a new doc with a conflict: RevisionInternal rev = new RevisionInternal("MyDocID", "1-1111", false); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); properties.put("message", "hi"); rev.setProperties(properties); List<String> history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); rev = new RevisionInternal("MyDocID", "1-ffff", false); properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); properties.put("message", "bye"); rev.setProperties(properties); history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); // Create another doc with a merged conflict: rev = new RevisionInternal("MyDocID2", "1-1111", false); properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); properties.put("message", "hi"); rev.setProperties(properties); history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); rev = new RevisionInternal("MyDocID2", "1-ffff", true); properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); rev.setProperties(properties); history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); // Get changes, testing all combinations of includeConflicts and includeDocs: for (int conflicts = 0; conflicts <= 1; conflicts++) { for (int bodies = 0; bodies <= 1; bodies++) { ChangesOptions options = new ChangesOptions(); options.setIncludeConflicts(conflicts != 0); options.setIncludeDocs(bodies != 0); RevisionList changes = database.changesSince(0, options, null, null); assertEquals(12 + 2 * conflicts, changes.size()); for (RevisionInternal change : changes) { if (bodies != 0) assertNotNull(change.getBody()); else assertNull(change.getBody()); } } } } /** * in DatabaseInternal_Tests.m * - (void) test28_enableAutoCompact */ public void test28_enableAutoCompact() throws CouchbaseLiteException { // Ensure that a database created without auto-compact (by CBL 1.1, or prior to 10/5/15) can // still be opened, since it has to be switched to auto-compact mode. Database.setAutoCompact(false); Database manualDB = manager.getDatabase("manualcompact"); assertNotNull(manualDB); this.createDocWithProperties(new HashMap<String, Object>(), manualDB); manualDB.close(); Database.setAutoCompact(true); manualDB = manager.getDatabase("manualcompact"); assertNotNull(manualDB); manualDB.close(); } public void testPruneRevsToMaxDepthViaCompact() throws Exception { Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testDatabaseCompaction"); properties.put("tag", 1337); Document doc = createDocumentWithProperties(database, properties); SavedRevision rev = doc.getCurrentRevision(); database.setMaxRevTreeDepth(2); for (int i = 0; i < 10; i++) { Map<String, Object> properties2 = new HashMap<String, Object>(properties); properties2.put("tag", i); rev = rev.createRevision(properties2); } database.compact(); Document fetchedDoc = database.getDocument(doc.getId()); List<SavedRevision> revisions = fetchedDoc.getRevisionHistory(); assertEquals(2, revisions.size()); RevisionInternal curRev = database.getDocument(doc.getId(), null, true); assertNotNull(curRev); List<RevisionInternal> revs = database.getRevisionHistory(curRev); assertNotNull(revs); assertEquals(2, revs.size()); } /** * When making inserts in a transaction, the change notifications should * be batched into a single change notification (rather than a change notification * for each insert) */ public void testChangeListenerNotificationBatching() throws Exception { final int numDocs = 50; final AtomicInteger atomicInteger = new AtomicInteger(0); final CountDownLatch countDownLatch = new CountDownLatch(1); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { atomicInteger.incrementAndGet(); } }); database.runInTransaction(new TransactionalTask() { @Override public boolean run() { createDocuments(database, numDocs); countDownLatch.countDown(); return true; } }); boolean success = countDownLatch.await(30, TimeUnit.SECONDS); assertTrue(success); assertEquals(1, atomicInteger.get()); } /** * When making inserts outside of a transaction, there should be a change notification * for each insert (no batching) */ public void testChangeListenerNotification() throws Exception { final int numDocs = 50; final AtomicInteger atomicInteger = new AtomicInteger(0); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { atomicInteger.incrementAndGet(); } }); createDocuments(database, numDocs, false); assertEquals(numDocs, atomicInteger.get()); } /** * With transaction */ public void testChangeListenerNotificationWithTransaction() throws Exception { final int numDocs = 50; final AtomicInteger atomicInteger = new AtomicInteger(0); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); if (changes != null) atomicInteger.addAndGet(changes.size()); } }); createDocuments(database, numDocs); assertEquals(numDocs, atomicInteger.get()); } /** * Change listeners should only be called once no matter how many times they're added. */ public void testAddChangeListenerIsIdempotent() throws Exception { final AtomicInteger count = new AtomicInteger(0); Database.ChangeListener listener = new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { count.incrementAndGet(); } }; database.addChangeListener(listener); database.addChangeListener(listener); createDocuments(database, 1); assertEquals(1, count.intValue()); } public void testGetActiveReplications() throws Exception { // create mock sync gateway that will serve as a pull target and return random docs int numMockDocsToServe = 0; MockDispatcher dispatcher = new MockDispatcher(); MockWebServer server = MockHelper.getPreloadedPullTargetMockCouchDB(dispatcher, numMockDocsToServe, 1); dispatcher.setServerType(MockDispatcher.ServerType.COUCHDB); server.setDispatcher(dispatcher); try { server.start(); final Replication replication = database.createPullReplication(server.url("/db").url()); assertEquals(0, database.getAllReplications().size()); assertEquals(0, database.getActiveReplications().size()); final CountDownLatch replicationRunning = new CountDownLatch(1); replication.addChangeListener(new ReplicationRunningObserver(replicationRunning)); replication.start(); boolean success = replicationRunning.await(30, TimeUnit.SECONDS); assertTrue(success); assertEquals(1, database.getAllReplications().size()); assertEquals(1, database.getActiveReplications().size()); final CountDownLatch replicationDoneSignal = new CountDownLatch(1); replication.addChangeListener(new ReplicationFinishedObserver(replicationDoneSignal)); success = replicationDoneSignal.await(60, TimeUnit.SECONDS); assertTrue(success); // workaround race condition. Since our replication change listener will get triggered // _before_ the internal change listener that updates the activeReplications map, we // need to pause briefly to let the internal change listener to update activeReplications. Thread.sleep(500); assertEquals(1, database.getAllReplications().size()); assertEquals(0, database.getActiveReplications().size()); } finally { server.shutdown(); } } public void testGetDatabaseNameFromPath() throws Exception { assertEquals("baz", FileDirUtils.getDatabaseNameFromPath("foo/bar/baz.cblite")); } public void testEncodeDocumentJSON() throws Exception { Map<String, Object> props = new HashMap<String, Object>(); props.put("_local_seq", ""); RevisionInternal revisionInternal = new RevisionInternal(props); byte[] encoded = RevisionUtils.asCanonicalJSON(revisionInternal); assertNotNull(encoded); } /** * in Database_Tests.m * - (void) test075_UpdateDocInTransaction */ public void testUpdateDocInTransaction() throws InterruptedException { // Test for #256, "Conflict error when updating a document multiple times in transaction block" // https://github.com/couchbase/couchbase-lite-ios/issues/256 Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testUpdateDocInTransaction"); properties.put("count", 1); final Document doc = createDocumentWithProperties(database, properties); final CountDownLatch latch = new CountDownLatch(1); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { Log.i(TAG, "-- changed() --"); latch.countDown(); } }); assertTrue(database.runInTransaction(new TransactionalTask() { @Override public boolean run() { // Update doc. The currentRevision should update, but no notification be posted (yet). Map<String, Object> props1 = new HashMap<String, Object>(); props1.putAll(doc.getProperties()); props1.put("count", 2); SavedRevision rev1 = null; try { rev1 = doc.putProperties(props1); } catch (CouchbaseLiteException e) { Log.e(Log.TAG_DATABASE, e.toString()); return false; } assertNotNull(rev1); assertEquals(doc.getCurrentRevision(), rev1); assertEquals(1, latch.getCount()); // Update doc again; this should succeed, in the same manner. Map<String, Object> props2 = new HashMap<String, Object>(); props2.putAll(doc.getProperties()); props2.put("count", 3); SavedRevision rev2 = null; try { rev2 = doc.putProperties(props2); } catch (CouchbaseLiteException e) { Log.e(Log.TAG_DATABASE, e.toString()); return false; } assertNotNull(rev2); assertEquals(doc.getCurrentRevision(), rev2); assertEquals(1, latch.getCount()); return true; } })); assertTrue(latch.await(0, TimeUnit.SECONDS)); } public void testClose() throws Exception { // Get the database: Database db = manager.getDatabase(DEFAULT_TEST_DB); assertNotNull(db); // Test that the database is remembered by the manager: assertEquals(db, manager.getDatabase(DEFAULT_TEST_DB)); assertTrue(manager.allOpenDatabases().contains(db)); // Create a new document: Document doc = db.getDocument("doc1"); assertNotNull(doc.putProperties(new HashMap<String, Object>())); // Test that the document is remebered by the database: assertEquals(doc, db.getCachedDocument("doc1")); // Close database: database.close(); // The cache should be clear: assertNull(db.getCachedDocument("doc1")); // This that the database is forgotten: assertFalse(manager.allOpenDatabases().contains(db)); } public void testAndroid2MLimit() throws Exception { final String content; { // NOTE: Java internally uses Unicode, following characters consumes 2.1M * 2Bytes -> 4.2MB in memory. char[] chars = new char[2 * 1024 * 1024 + 10 * 1024]; // 2.1K characters Arrays.fill(chars, 'a'); // NOTE: public String(char value[]) copies characters. // http://hg.openjdk.java.net/jdk7u/jdk7u6/jdk/file/8c2c5d63a17e/src/share/classes/java/lang/String.java#l168 content = new String(chars); // make GC free data chars = null; } // Add a 2M+ document into the database: Map<String, Object> props = new HashMap<String, Object>(); props.put("content", content); Document doc = database.createDocument(); assertNotNull(doc.putProperties(props)); String docId = doc.getId(); // Close and reopen the database: database.close(); database = manager.getDatabase(DEFAULT_TEST_DB); // Try to read the document: doc = database.getDocument(docId); assertNotNull(doc); Map<String, Object> properties = doc.getProperties(); assertNotNull(properties); assertEquals(content, properties.get("content")); } // Database_Tests.m : test18_Attachments public void test18_Attachments() throws Exception { Map<String, Object> properties = new HashMap<String, Object>(); properties.put("testName", "testAttachments"); properties.put("count", 1); final Document doc = createDocumentWithProperties(database, properties); SavedRevision rev = doc.getCurrentRevision(); assertEquals(0, rev.getAttachments().size()); assertEquals(0, rev.getAttachmentNames().size()); assertNull(rev.getAttachment("index.html")); String content = "This is a test attachments!"; ByteArrayInputStream body = new ByteArrayInputStream(content.getBytes()); UnsavedRevision rev2 = doc.createRevision(); rev2.setAttachment("index.html", "text/plain; charset=utf-8", body); assertEquals(1, rev2.getAttachments().size()); assertEquals(1, rev2.getAttachmentNames().size()); assertEquals("index.html", rev2.getAttachmentNames().get(0)); Attachment attach = rev2.getAttachment("index.html"); assertNotNull(attach); assertNull(attach.getRevision()); // No revision set assertNull(attach.getDocument()); // No revision set assertEquals("index.html", attach.getName()); assertEquals("text/plain; charset=utf-8", attach.getContentType()); SavedRevision rev3 = rev2.save(); assertNotNull(rev3); assertEquals(1, rev3.getAttachments().size()); assertEquals(1, rev3.getAttachmentNames().size()); assertEquals("index.html", rev3.getAttachmentNames().get(0)); attach = rev3.getAttachment("index.html"); assertNotNull(attach); assertNotNull(attach.getRevision()); assertNotNull(attach.getDocument()); assertEquals(doc, attach.getDocument()); assertEquals("index.html", attach.getName()); assertEquals("text/plain; charset=utf-8", attach.getContentType()); InputStream in = attach.getContent(); try { assertTrue(Arrays.equals(content.getBytes(), TextUtils.read(in))); } finally { in.close(); } // Look at the attachment's file: URL bodyURL = attach.getContentURL(); if (isEncryptedAttachmentStore()) { assertNull(bodyURL); } else { assertNotNull(bodyURL); assertTrue(Arrays.equals(content.getBytes(), TextUtils.read(new File(bodyURL.toURI())).getBytes())); } UnsavedRevision newRev = rev3.createRevision(); newRev.removeAttachment(attach.getName()); SavedRevision rev4 = newRev.save(); assertNotNull(rev4); assertEquals(0, rev4.getAttachments().size()); assertEquals(0, rev4.getAttachmentNames().size()); // Add an attachment with revpos=0 (see #1200) Map<String, Object> props = new HashMap<String, Object>(rev3.getProperties()); Map<String, Object> atts = new HashMap<String, Object>((Map<String, Object>) props.get("_attachments")); props.put("_attachments", atts); Map<String, Object> att = new HashMap<String, Object>(); att.put("content_type", "text/plain"); att.put("revpos", 0); att.put("following", true); atts.put("zero.txt", att); Map<String, Object> attachment = new HashMap<String, Object>(); attachment.put("zero.txt", "zero".getBytes()); List<String> history = Arrays.asList("3-0000", rev3.getId(), rev.getId()); assertTrue(doc.putExistingRevision(props, attachment, history, null)); Revision rev5 = doc.getRevision("3-0000"); assertNotNull(rev5.getAttachment("zero.txt")); } public void testAttachmentsWithEncryption() throws Exception { setEncryptedAttachmentStore(true); try { test18_Attachments(); } finally { setEncryptedAttachmentStore(false); } } // https://github.com/couchbase/couchbase-lite-android/issues/783 public void testNonStringForTypeField() throws CouchbaseLiteException { // Non String as type List<Integer> type1 = new ArrayList(); type1.add(0); type1.add(1); Map<String, Object> props1 = new HashMap<String, Object>(); props1.put("key", "value"); props1.put("type", type1); Document doc1 = database.createDocument(); doc1.putProperties(props1); // String as type String type2 = "STRING"; Map<String, Object> props2 = new HashMap<String, Object>(); props1.put("key", "value"); props1.put("type", type2); Document doc2 = database.createDocument(); doc2.putProperties(props1); } // ClassCastException when upgrading from 1.1 to 1.2 // https://github.com/couchbase/couchbase-lite-android/issues/790 public void testForceInsertWithNonStringForTypeField() throws CouchbaseLiteException { // Non String as type RevisionInternal rev = new RevisionInternal("MyDocID", "1-1111", false); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); List<Integer> type1 = Arrays.asList(0, 1); properties.put("type", type1); rev.setProperties(properties); List<String> history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); // String as type rev = new RevisionInternal("MyDocID", "1-ffff", false); properties = new HashMap<String, Object>(); properties.put("_id", rev.getDocID()); properties.put("_rev", rev.getRevID()); properties.put("type", "STRING"); rev.setProperties(properties); history = Arrays.asList(rev.getRevID()); database.forceInsert(rev, history, null); } /** * Missing changes in Database Change Notification * https://github.com/couchbase/couchbase-lite-java-core/issues/1147 */ public void testDatabaseChangeNotification() throws Exception { if (!this.isSQLiteDB()) return; final int numDocs = 1000; final int batchSize = 20; final AtomicInteger totalChangesCount = new AtomicInteger(0); final CountDownLatch changesCountDownLatch = new CountDownLatch(1); final CountDownLatch createDocsCountDownLatch = new CountDownLatch(2); database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { synchronized (totalChangesCount) { int total = totalChangesCount.addAndGet(event.getChanges().size()); Log.w(TAG, "Total changes : " + total + " > " + Thread.currentThread().getName()); if (total == numDocs * 2) { changesCountDownLatch.countDown(); } } } }); final Thread t1 = new Thread(new Runnable() { @Override public void run() { int numRounds = numDocs / batchSize; for (int i = 0; i < numRounds; i++) { database.runInTransaction(new TransactionalTask() { @Override public boolean run() { for (int j = 0; j < batchSize; j++) { Document doc = database.createDocument(); Map<String, Object> props = new HashMap<String, Object>(); props.put("foo", "bar"); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error creating a document", e); return false; } } return true; } }); } synchronized (createDocsCountDownLatch) { createDocsCountDownLatch.countDown(); } } }, "T1"); t1.start(); final Thread t2 = new Thread(new Runnable() { @Override public void run() { int numRounds = numDocs / batchSize; for (int i = 0; i < numRounds; i++) { database.runInTransaction(new TransactionalTask() { @Override public boolean run() { for (int j = 0; j < batchSize; j++) { Document doc = database.createDocument(); Map<String, Object> props = new HashMap<String, Object>(); props.put("foo", "bar"); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error creating a document", e); return false; } } return true; } }); } synchronized (createDocsCountDownLatch) { createDocsCountDownLatch.countDown(); } } }, "T2"); t2.start(); createDocsCountDownLatch.await(); Log.i(TAG, "Both T1 and T2 are done creating docs : " + database.getDocumentCount()); assertTrue(changesCountDownLatch.await(60, TimeUnit.SECONDS)); assertEquals(numDocs * 2, totalChangesCount.get()); // If not sleeping, sometimes not get all logging messages after the unit test got tear down. Thread.sleep(5000); } // https://github.com/couchbase/couchbase-lite-android/issues/742 public void testDocumentUpdate() throws CouchbaseLiteException { final AtomicBoolean latch = new AtomicBoolean(false); // create initial document Document doc = database.getDocument("11111"); Map<String, Object> dict = new HashMap<String, Object>(); dict.put("X", "Y"); doc.putProperties(dict); // set changeListener database.addChangeListener(new Database.ChangeListener() { @Override public void changed(Database.ChangeEvent event) { List<DocumentChange> changes = event.getChanges(); assertEquals(1, changes.size()); DocumentChange change = changes.get(0); assertNotNull(change); assertEquals("11111", change.getDocumentId()); Document doc = database.getDocument("11111"); Map<String, Object> props = doc.getUserProperties(); assertEquals("Z", props.get("X")); synchronized (latch) { latch.set("Z".equals(props.get("X"))); latch.notifyAll(); } } }); // update document dict = new HashMap<String, Object>(doc.getProperties()); dict.put("X", "Z"); doc.putProperties(dict); // wait till ChangeListener is called. timeout -> 10sec synchronized (latch) { try { latch.wait(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } assertTrue(latch.get()); } /** * in DatabaseInternal_Tests.m * - (void) test29_autoPruneOnPut * https://github.com/couchbase/couchbase-lite-ios/issues/1165 */ public void test29_autoPruneOnPut() throws CouchbaseLiteException { database.setMaxRevTreeDepth(5); RevisionInternal lastRev = null; List<RevisionInternal> revs = new ArrayList<RevisionInternal>(); for (int gen = 1; gen <= 10; gen++) { Map<String, Object> props = new HashMap<String, Object>(); props.put("_id", "foo"); props.put("gen", gen); RevisionInternal newRev = new RevisionInternal(props); RevisionInternal rev = database.putRevision(newRev, lastRev == null ? null : lastRev.getRevID(), false); revs.add(rev); lastRev = rev; } // Verify that the first five revs are no longer available: for (int gen = 1; gen <= 10; gen++) { RevisionInternal rev = database.getDocument("foo", revs.get(gen - 1).getRevID(), true); if (gen <= 5) assertNull(rev); else assertNotNull(rev); } } /** * in DatabaseInternal_Tests.m * - (void) test29_autoPruneOnForceInsert * https://github.com/couchbase/couchbase-lite-ios/issues/1165 */ public void test29_autoPruneOnForceInsert() throws CouchbaseLiteException { database.setMaxRevTreeDepth(5); List<RevisionInternal> revs = new ArrayList<RevisionInternal>(); List<String> history = new ArrayList<String>(); for (int gen = 1; gen <= 10; gen++) { Map<String, Object> props = new HashMap<String, Object>(); props.put("_id", "foo"); props.put("_rev", String.format(Locale.ENGLISH, "%d-cafebabe", gen)); props.put("gen", gen); RevisionInternal rev = new RevisionInternal(props); database.forceInsert(rev, history, null); history.add(0, rev.getRevID()); revs.add(rev); } // Verify that the first five revs are no longer available: for (int gen = 1; gen <= 10; gen++) { RevisionInternal rev = database.getDocument("foo", revs.get(gen - 1).getRevID(), true); if (gen <= 5) assertNull(rev); else assertNotNull(rev); } } /** * in DatabaseInternal_Tests.m * - (void) test30_conflictAfterPrune * https://github.com/couchbase/couchbase-lite-ios/issues/1217 */ public void test30_conflictAfterPrune() throws CouchbaseLiteException { // Create a conflict where one branch is more than maxRevTreeDepth generations deeper than // the other: (#1217) database.setMaxRevTreeDepth(5); Status status = new Status(); RevisionInternal base = database.put("robin", new HashMap<String, Object>(), null, false, null, status); assertNotNull(base); Map<String, Object> props = new HashMap<String, Object>(); props.put("branch", "short"); RevisionInternal shortBranch = database.put("robin", props, base.getRevID(), false, null, status); assertNotNull(shortBranch); RevisionInternal longBranch = base; for (int i = 0; i < 8; i++) { props = new HashMap<String, Object>(); props.put("branch", "long"); longBranch = database.put("robin", props, longBranch.getRevID(), i == 0, null, status); assertNotNull(longBranch); } Log.i(TAG, "All revisions = %s", database.getStore().getAllRevisions("robin", false)); List<SavedRevision> all = database.getDocument("robin").getConflictingRevisions(); Log.i(TAG, "Conflicts = %s", all); assertEquals(2, all.size()); List<String> allRevIDs = new ArrayList<String>(); for (SavedRevision rev : all) allRevIDs.add(rev.getId()); assertTrue(allRevIDs.contains(shortBranch.getRevID())); assertTrue(allRevIDs.contains(longBranch.getRevID())); RevisionInternal shortConflict = shortBranch; RevisionInternal longConflict = longBranch; // Resolve the conflict by adding to the long branch and deleting the short one: List<RevisionInternal> shortHistory = database.getRevisionHistory(shortBranch); List<RevisionInternal> longHistory = database.getRevisionHistory(longBranch); props = new HashMap<String, Object>(); props.put("_deleted", true); shortBranch = database.put("robin", props, shortBranch.getRevID(), false, null, status); assertNotNull(shortBranch); props = new HashMap<String, Object>(); props.put("branch", "merged"); longBranch = database.put("robin", props, longBranch.getRevID(), false, null, status); assertNotNull(longBranch); Log.i(TAG, "After merge, all revisions = %s", database.getStore().getAllRevisions("robin", false)); all = database.getDocument("robin").getConflictingRevisions(); Log.i(TAG, "After merge, conflicts = %s", all); assertEquals(1, all.size()); assertEquals(longBranch.getRevID(), all.get(0).getId()); // Add the conflicting revisions back, as a pull replication might do: List<String> history = new ArrayList<String>(); for (RevisionInternal rev : shortHistory) history.add(rev.getRevID()); database.forceInsert(shortConflict, history, null); history.clear(); for (RevisionInternal rev : longHistory) history.add(rev.getRevID()); database.forceInsert(longConflict, history, null); // Make sure this doesn't re-create the conflict: all = database.getDocument("robin").getConflictingRevisions(); Log.i(TAG, "After pull, conflicts = %s", all); assertEquals(1, all.size()); assertEquals(longBranch.getRevID(), longBranch.getRevID()); } /** * - (void) test071_PutExistingRevision in Unit-Tests/Database_Tests.m */ public void test071_PutExistingRevision() throws CouchbaseLiteException { Map<String, Object> props = new HashMap<String, Object>(); props.put("foo", 1); Document doc = createDocWithProperties(props); props.clear(); props.put("foo", 2); List<String> history = Arrays.asList("3-cafebabe", "2-feedba95", doc.getCurrentRevisionId()); assertTrue(doc.putExistingRevision(props, null, history, null)); Map<String, Object> expected = new HashMap<String, Object>(); expected.put("_id", doc.getId()); expected.put("_rev", "3-cafebabe"); expected.put("foo", 2); Revision rev = doc.getRevision("3-cafebabe"); assertNotNull(rev); assertFalse(rev.isDeletion()); assertEquals(expected, rev.getProperties()); // Repeat; should be no error: assertTrue(doc.putExistingRevision(props, null, history, null)); // Add a deleted revision: props.clear(); props.put("foo", -1); props.put("_deleted", true); history = Arrays.asList("3-deadbeef", "2-feedba95", doc.getCurrentRevisionId()); assertTrue(doc.putExistingRevision(props, null, history, null)); expected.clear(); expected.put("_id", doc.getId()); expected.put("_rev", "3-deadbeef"); expected.put("_deleted", true); expected.put("foo", -1); rev = doc.getRevision("3-deadbeef"); assertNotNull(rev); assertTrue(rev.isDeletion()); assertEquals(expected, rev.getProperties()); } /** * - (void) test072_PutExistingRevisionWithAttachment in Unit-Tests/Database_Tests.m */ public void test072_PutExistingRevisionWithAttachment() throws Exception { Document doc = database.getDocument("some-doc"); byte[] content = "hi there".getBytes("UTF-8"); Map<String, Object> foo = new HashMap<String, Object>(); foo.put("content_type", "text/plain"); Map<String, Object> bar = new HashMap<String, Object>(); bar.put("content_type", "text/plain"); bar.put("stub", true); Map<String, Map<String, Object>> atts = new HashMap<String, Map<String, Object>>(); atts.put("foo.txt", foo); atts.put("bar.txt", bar); Map<String, Object> props = new HashMap<String, Object>(); props.put("_attachments", atts); Map<String, Object> attachments = new HashMap<String, Object>(); attachments.put("foo.txt", content); attachments.put("bar.txt", content); assertTrue(doc.putExistingRevision(props, attachments, Arrays.asList("1-cafebabe"), null)); Revision rev = doc.getCurrentRevision(); assertNotNull(rev); assertFalse(rev.isDeletion()); assertEquals("1-cafebabe", rev.getId()); Attachment a = rev.getAttachment("foo.txt"); assertNotNull(a); assertEquals("text/plain", a.getContentType()); assertEquals("hi there", new String(IOUtils.toByteArray(a.getContent()), "UTF-8")); a = rev.getAttachment("bar.txt"); assertNotNull(a); assertEquals("text/plain", a.getContentType()); assertEquals("hi there", new String(IOUtils.toByteArray(a.getContent()), "UTF-8")); } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/1298 * * Insert 10 4MB/Doc into database */ public void manualTestPutLargeDataSet() throws CouchbaseLiteException { // Test Scenario: // - Add 10 4mb docs // - Crash with OOM Map<String, Object> body = new HashMap<String, Object>(); char[] chars = new char[4 * 1024 * 1024]; // 4 million chars Arrays.fill(chars, 'a'); final String content = new String(chars); body.put("content", content); Body revBody = new Body(body); for (int i = 0; i < 10; i++) { // do_PUT_Document uses Database.putRevision() String docID = String.format(Locale.ENGLISH, "DocID=%d", i); RevisionInternal rev = new RevisionInternal(docID, null, false); rev.setBody(revBody); database.putRevision(rev, null, false); } assertEquals(10, database.getDocumentCount()); assertEquals(content, database.getDocument("DocID=1").getProperties().get("content")); } }