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