syncbase java high-level: add a few more tests

Also, a few related changes:
- migrate various constants into DatabaseOptions
  (note, many of these will need to be changed as we polish things)
- add disableSyncgroupPublishing option, so that collection+syncgroup
  creation can be fast (needed since publish is currently synchronous
  and times out after 60s)
- change "userdata" cx/sg name to "userdata__" to reduce chance of
  collisions with developer-specified names
- change get() to return null on does-not-exist
- tweak notes in get{Collection,Syncgroup} about potentially returning
  null or throwing an exception
- remove unnecessary other != null check in Id.java
- add Id.toString(), for easier debugging

And a few unrelated changes:
- add /src/main/jniLibs to gitignore (build artifacts)
- add note about race in Syncgroup.updateAccessList

Change-Id: I2f20b84ebe81fc710dfbe04fca060d23f4316c9a
diff --git a/syncbase/.gitignore b/syncbase/.gitignore
index bbc7f4c..da3561e 100644
--- a/syncbase/.gitignore
+++ b/syncbase/.gitignore
@@ -1,5 +1,6 @@
 /.idea
 /build
 /local.properties
+/src/main/jniLibs
 .gradle
 *.iml
diff --git a/syncbase/build.gradle b/syncbase/build.gradle
index 5953a04..035fec1 100644
--- a/syncbase/build.gradle
+++ b/syncbase/build.gradle
@@ -130,8 +130,9 @@
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.android.support:appcompat-v7:23.4.0'
-    compile 'io.v:vanadium-android:2.1.4'
+    compile 'io.v:vanadium-android:2.1.7'
     testCompile 'junit:junit:4.12'
+    testCompile group: 'com.google.truth', name: 'truth', version: '0.28'
     androidTestCompile 'com.android.support.test:runner:0.4.1'
     androidTestCompile 'com.android.support.test:rules:0.4.1'
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Collection.java b/syncbase/src/main/java/io/v/syncbase/Collection.java
index 41d33d3..29efd82 100644
--- a/syncbase/src/main/java/io/v/syncbase/Collection.java
+++ b/syncbase/src/main/java/io/v/syncbase/Collection.java
@@ -7,6 +7,7 @@
 import io.v.v23.VFutures;
 import io.v.v23.security.access.Permissions;
 import io.v.v23.verror.ExistException;
+import io.v.v23.verror.NoExistException;
 import io.v.v23.verror.VException;
 
 /**
@@ -50,11 +51,13 @@
         return ((Database) mDatabaseHandle).getSyncgroup(getId());
     }
 
+    // TODO(sadovsky): Add deleteRange API.
     // TODO(sadovsky): Maybe add scan API, if developers aren't satisfied with watch.
 
     // TODO(sadovsky): Revisit the get API:
     // - Is the Class<T> argument necessary?
-    // - What does it do if there is no value for the given key?
+    // - Should we take the target Object as an argument, to avoid allocations?
+    // - What should it do if there is no value for the given key? (Currently, it returns null.)
 
     /**
      * Returns the value associated with {@code key}.
@@ -62,6 +65,8 @@
     public <T> T get(String key, Class<T> cls) {
         try {
             return VFutures.sync(mVCollection.getRow(key).get(Syncbase.getVContext(), cls));
+        } catch (NoExistException e) {
+            return null;
         } catch (VException e) {
             throw new RuntimeException("get failed: " + key, e);
         }
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index 3a97b4f..ed9aaa0 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -111,7 +111,8 @@
      */
     public Syncgroup getSyncgroup(Id id) {
         // TODO(sadovsky): Consider throwing an exception or returning null if the syncgroup does
-        // not exist.
+        // not exist. But note, a syncgroup can get destroyed via sync after a client obtains a
+        // handle for it, so perhaps we should instead add an 'exists' method.
         return new Syncgroup(mVDatabase.getSyncgroup(id.toVId()), this, id);
     }
 
diff --git a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
index a3ec836..18cdc00 100644
--- a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
+++ b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
@@ -61,7 +61,8 @@
      */
     public Collection getCollection(Id id) {
         // TODO(sadovsky): Consider throwing an exception or returning null if the collection does
-        // not exist.
+        // not exist. But note, a collection can get destroyed via sync after a client obtains a
+        // handle for it, so perhaps we should instead add an 'exists' method.
         return new Collection(mVDatabaseCore.getCollection(id.toVId()), this);
     }
 
diff --git a/syncbase/src/main/java/io/v/syncbase/Id.java b/syncbase/src/main/java/io/v/syncbase/Id.java
index 0f8efa3..f60a6bb 100644
--- a/syncbase/src/main/java/io/v/syncbase/Id.java
+++ b/syncbase/src/main/java/io/v/syncbase/Id.java
@@ -41,7 +41,7 @@
 
     @Override
     public boolean equals(Object other) {
-        if (other instanceof Id && other != null) {
+        if (other instanceof Id) {
             Id otherId = (Id) other;
             return mBlessing.equals(otherId.getBlessing()) && mName.equals(otherId.getName());
         }
@@ -61,6 +61,11 @@
         return result;
     }
 
+    @Override
+    public String toString() {
+        return "Id(" + encode() + ")";
+    }
+
     // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
 
     protected Id(io.v.v23.services.syncbase.Id id) {
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index 0971e53..f886baa 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -10,6 +10,7 @@
 import com.google.common.collect.ImmutableMap;
 
 import java.io.File;
+import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
 
@@ -44,25 +45,44 @@
      * Options for opening a database.
      */
     public static class DatabaseOptions {
-        // TODO(sadovsky): Fill this in further.
+        // Where data should be persisted.
         public String rootDir;
+        // TODO(sadovsky): Figure out what this should default to.
+        public List<String> mountPoints = ImmutableList.of("/ns.dev.v.io:8101/tmp/todos/users/");
+        // TODO(sadovsky): Figure out how developers should specify this.
+        public String adminUserId = "alexfandrianto@google.com";
+        // TODO(sadovsky): Figure out how developers should specify this.
+        public String defaultBlessingStringPrefix = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:";
+        // FOR ADVANCED USERS. If true, syncgroups will not be published to the cloud peer.
+        public boolean disableSyncgroupPublishing;
         // FOR ADVANCED USERS. If true, the user's data will not be synced across their devices.
         public boolean disableUserdataSyncgroup;
         // TODO(sadovsky): Drop this once we switch from io.v.v23.syncbase to io.v.syncbase.core.
         public VContext vContext;
+
+        protected String getPublishSyncbaseName() {
+            if (disableSyncgroupPublishing) {
+                return null;
+            }
+            return mountPoints.get(0) + "cloud";
+        }
+
+        protected String getCloudBlessingString() {
+            return "dev.v.io:u:" + adminUserId;
+        }
     }
 
-    private static DatabaseOptions sOpts;
+    protected static DatabaseOptions sOpts;
     private static Database sDatabase;
 
-    // TODO(sadovsky): Maybe select values for DB_NAME and USERDATA_SYNCGROUP_NAME that are less
-    // likely to collide with developer-specified names.
+    // TODO(sadovsky): Maybe set DB_NAME to "db__" so that it is less likely to collide with
+    // developer-specified names.
 
     protected static final String
             TAG = "syncbase",
             DIR_NAME = "syncbase",
             DB_NAME = "db",
-            USERDATA_SYNCGROUP_NAME = "userdata";
+            USERDATA_SYNCGROUP_NAME = "userdata__";
 
     protected static void enqueue(final Runnable r) {
         // Note, we use Timer rather than Handler because the latter must be mocked out for tests,
@@ -145,14 +165,6 @@
         return sOpts.vContext;
     }
 
-    // TODO(sadovsky): Some of these constants should become fields in DatabaseOptions.
-    protected static final String
-            PROXY = "proxy",
-            DEFAULT_BLESSING_STRING_PREFIX = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:",
-            MOUNT_POINT = "/ns.dev.v.io:8101/tmp/todos/users/",
-            CLOUD_BLESSING_STRING = "dev.v.io:u:alexfandrianto@google.com",
-            CLOUD_NAME = MOUNT_POINT + "cloud";
-
     private static Database startSyncbaseAndInitDatabase(VContext ctx) {
         SyncbaseService s;
         try {
@@ -169,7 +181,8 @@
     private static String startSyncbase(VContext vContext, String rootDir)
             throws SyncbaseServer.StartException {
         try {
-            vContext = V.withListenSpec(vContext, V.getListenSpec(vContext).withProxy(PROXY));
+            // TODO(sadovsky): Make proxy configurable?
+            vContext = V.withListenSpec(vContext, V.getListenSpec(vContext).withProxy("proxy"));
         } catch (VException e) {
             Log.w(TAG, "Failed to set up Vanadium proxy", e);
         }
@@ -206,7 +219,7 @@
     }
 
     private static String getBlessingStringFromEmail(String email) {
-        return DEFAULT_BLESSING_STRING_PREFIX + email;
+        return sOpts.defaultBlessingStringPrefix + email;
     }
 
     protected static BlessingPattern getBlessingPatternFromEmail(String email) {
@@ -238,7 +251,7 @@
                 new io.v.v23.security.access.AccessList(
                         ImmutableList.of(
                                 new BlessingPattern(getPersonalBlessingString()),
-                                new BlessingPattern(CLOUD_BLESSING_STRING)),
+                                new BlessingPattern(sOpts.getCloudBlessingString())),
                         ImmutableList.<String>of());
         return new Permissions(ImmutableMap.of(
                 Constants.RESOLVE.getValue(), anyone,
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
index 50fae8c..07aa9f0 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
@@ -4,8 +4,6 @@
 
 package io.v.syncbase;
 
-import com.google.common.collect.ImmutableList;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -39,8 +37,8 @@
             cxVIds.add(cx.getId().toVId());
         }
         SyncgroupSpec spec = new SyncgroupSpec(
-                "", Syncbase.CLOUD_NAME, Syncbase.defaultPerms(), cxVIds,
-                ImmutableList.of(Syncbase.MOUNT_POINT), false);
+                "", Syncbase.sOpts.getPublishSyncbaseName(), Syncbase.defaultPerms(), cxVIds,
+                Syncbase.sOpts.mountPoints, false);
         try {
             VFutures.sync(mVSyncgroup.create(Syncbase.getVContext(), spec, newSyncgroupMemberInfo()));
         } catch (ExistException e) {
@@ -162,6 +160,8 @@
         } catch (VException e) {
             throw new RuntimeException("setSpec failed", e);
         }
+        // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
+        // after spec.getCollections() but before db.getCollection().
         final List<io.v.v23.services.syncbase.Id> cxVIds = spec.getCollections();
         mDatabase.runInBatch(new Database.BatchOperation() {
             @Override
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index a48f52a..8e24228 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -4,20 +4,35 @@
 
 package io.v.syncbase;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.SettableFuture;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import io.v.v23.V;
 import io.v.v23.context.VContext;
 import io.v.v23.rpc.ListenSpec;
 
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 public class SyncbaseTest {
     private VContext ctx;
 
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
+
     // To run these tests from Android Studio, add the following VM option to the default JUnit
     // build configuration, via Run > Edit Configurations... > Defaults > JUnit > VM options:
     // -Djava.library.path=/Users/sadovsky/vanadium/release/java/syncbase/build/libs
@@ -28,13 +43,21 @@
                 new ListenSpec.Address("tcp", "localhost:0")));
     }
 
-    @Test
-    public void createDatabase() throws Exception {
+    private Syncbase.DatabaseOptions newDatabaseOptions() {
         Syncbase.DatabaseOptions opts = new Syncbase.DatabaseOptions();
-        opts.rootDir = "/tmp";
+        // Use a fresh rootDir for each test run.
+        try {
+            opts.rootDir = folder.newFolder().getAbsolutePath();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
         opts.disableUserdataSyncgroup = true;
+        opts.disableSyncgroupPublishing = true;
         opts.vContext = ctx;
+        return opts;
+    }
 
+    private Database createDatabase() throws Exception {
         final SettableFuture<Database> future = SettableFuture.create();
 
         Syncbase.database(new Syncbase.DatabaseCallback() {
@@ -47,8 +70,117 @@
             public void onError(Throwable e) {
                 future.setException(e);
             }
-        }, opts);
+        }, newDatabaseOptions());
 
-        future.get(5, TimeUnit.SECONDS);
+        return future.get(5, TimeUnit.SECONDS);
+    }
+
+    private static Iterable<Id> getCollectionIds(Database db) {
+        List<Id> res = new ArrayList<>();
+        for (Iterator<Collection> it = db.getCollections(); it.hasNext(); ) {
+            res.add(it.next().getId());
+        }
+        return res;
+    }
+
+    private static Iterable<Id> getSyncgroupIds(Database db) {
+        List<Id> res = new ArrayList<>();
+        for (Iterator<Syncgroup> it = db.getSyncgroups(); it.hasNext(); ) {
+            res.add(it.next().getId());
+        }
+        return res;
+    }
+
+    @Test
+    public void testCreateDatabase() throws Exception {
+        createDatabase();
+    }
+
+    @Test
+    public void testCreateAndGetCollections() throws Exception {
+        Database db = createDatabase();
+        DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
+        opts.withoutSyncgroup = true;
+        Collection cxA = db.collection("a", opts);
+        // TODO(sadovsky): Should we omit the userdata collection?
+        assertThat(getCollectionIds(db)).containsExactly(
+                new Id(Syncbase.getPersonalBlessingString(), "a"),
+                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
+        db.collection("b", opts);
+        assertThat(getCollectionIds(db)).containsExactly(
+                new Id(Syncbase.getPersonalBlessingString(), "a"),
+                new Id(Syncbase.getPersonalBlessingString(), "b"),
+                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
+        // Note, createDatabase() sets disableSyncgroupPublishing to true, so db.collection(name) is
+        // a purely local operation.
+        db.collection("c");
+        assertThat(getCollectionIds(db)).containsExactly(
+                new Id(Syncbase.getPersonalBlessingString(), "a"),
+                new Id(Syncbase.getPersonalBlessingString(), "b"),
+                new Id(Syncbase.getPersonalBlessingString(), "c"),
+                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
+        Collection secondCxA = db.collection("a", opts);
+        assertEquals(cxA.getId(), secondCxA.getId());
+    }
+
+    @Test
+    public void testRowCrudMethods() throws Exception {
+        Database db = createDatabase();
+        Collection cx = db.collection("cx");
+        assertFalse(cx.exists("foo"));
+        assertEquals(cx.get("foo", String.class), null);
+        cx.put("foo", "bar");
+        assertTrue(cx.exists("foo"));
+        assertEquals(cx.get("foo", String.class), "bar");
+        cx.put("foo", "baz");
+        assertTrue(cx.exists("foo"));
+        assertEquals(cx.get("foo", String.class), "baz");
+        cx.delete("foo");
+        assertFalse(cx.exists("foo"));
+        assertEquals(cx.get("foo", String.class), null);
+        cx.put("foo", 5);
+        assertEquals(cx.get("foo", Integer.class), Integer.valueOf(5));
+
+        // This time, with a POJO.
+        class MyObject {
+            String str;
+            int num;
+        }
+        MyObject putObj = new MyObject();
+        putObj.str = "hello";
+        putObj.num = 7;
+        cx.put("foo", putObj);
+        MyObject getObj = cx.get("foo", MyObject.class);
+        assertEquals(putObj.str, getObj.str);
+        assertEquals(putObj.num, getObj.num);
+    }
+
+    @Test
+    public void testCreateAndGetSyncgroups() throws Exception {
+        Database db = createDatabase();
+        DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
+        opts.withoutSyncgroup = true;
+        Collection cxA = db.collection("a", opts);
+        Collection cxB = db.collection("b", opts);
+        Collection cxC = db.collection("c");
+        // Note, there's no userdata syncgroup since we set disableUserdataSyncgroup to true.
+        assertThat(getSyncgroupIds(db)).containsExactly(
+                new Id(Syncbase.getPersonalBlessingString(), "c"));
+        db.syncgroup("sg1", ImmutableList.of(cxA));
+        db.syncgroup("sg2", ImmutableList.of(cxA, cxB, cxC));
+        assertThat(getSyncgroupIds(db)).containsExactly(
+                new Id(Syncbase.getPersonalBlessingString(), "c"),
+                new Id(Syncbase.getPersonalBlessingString(), "sg1"),
+                new Id(Syncbase.getPersonalBlessingString(), "sg2"));
+    }
+
+    @Test
+    public void testWatch() throws Exception {
+        // TODO(sadovsky): Implement.
+    }
+
+    @Test
+    public void testRunInBatch() throws Exception {
+        // TODO(sadovsky): Implement.
     }
 }
\ No newline at end of file