syncbase/vsync: tests for SyncGroup storage layer.
Change-Id: Ie58b2627fd17eb9e0a8990286f74b976edd19ef7
diff --git a/services/syncbase/vsync/syncgroup_test.go b/services/syncbase/vsync/syncgroup_test.go
new file mode 100644
index 0000000..aa49455
--- /dev/null
+++ b/services/syncbase/vsync/syncgroup_test.go
@@ -0,0 +1,438 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Tests for SyncGroup management and storage in Syncbase.
+
+import (
+ "reflect"
+ "testing"
+
+ "v.io/syncbase/v23/services/syncbase/nosql"
+ "v.io/syncbase/x/ref/services/syncbase/store"
+)
+
+// checkSGStats verifies SyncGroup stats.
+func checkSGStats(t *testing.T, svc *mockService, which string, numSG, numMembers int) {
+ memberViewTTL = 0 // Always recompute the SyncGroup membership view.
+ svc.sync.refreshMembersIfExpired(nil)
+
+ view := svc.sync.allMembers
+ if num := len(view.members); num != numMembers {
+ t.Errorf("num-members (%s): got %v instead of %v", which, num, numMembers)
+ }
+
+ sgids := make(map[GroupId]struct{})
+ for _, info := range view.members {
+ for gid := range info.gid2info {
+ sgids[gid] = struct{}{}
+ }
+ }
+
+ if num := len(sgids); num != numSG {
+ t.Errorf("num-syncgroups (%s): got %v instead of %v", which, num, numSG)
+ }
+}
+
+// TestAddSyncGroup tests adding SyncGroups.
+func TestAddSyncGroup(t *testing.T) {
+ svc := createService(t)
+ st := svc.St()
+
+ checkSGStats(t, svc, "add-1", 0, 0)
+
+ // Add a SyncGroup.
+
+ sgName := "foobar"
+ sgId := GroupId(1234)
+
+ sg := &SyncGroup{
+ Name: sgName,
+ Id: sgId,
+ Version: "etag-0",
+ Spec: nosql.SyncGroupSpec{
+ Prefixes: []string{"foo", "bar"},
+ },
+ Joiners: map[string]nosql.SyncGroupMemberInfo{
+ "phone": nosql.SyncGroupMemberInfo{SyncPriority: 10},
+ "tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+ "cloud": nosql.SyncGroupMemberInfo{SyncPriority: 1},
+ },
+ }
+
+ tx := st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg); err != nil {
+ t.Errorf("cannot add SyncGroup ID %d: %v", sg.Id, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit adding SyncGroup ID %d: %v", sg.Id, err)
+ }
+
+ // Verify SyncGroup ID, name, and data.
+
+ if id, err := getSyncGroupId(nil, st, sgName); err != nil || id != sgId {
+ t.Errorf("cannot get ID of SyncGroup %s: got %d instead of %d; err: %v", sgName, id, sgId, err)
+ }
+ if name, err := getSyncGroupName(nil, st, sgId); err != nil || name != sgName {
+ t.Errorf("cannot get name of SyncGroup %d: got %s instead of %s; err: %v",
+ sgId, name, sgName, err)
+ }
+
+ sgOut, err := getSyncGroupById(nil, st, sgId)
+ if err != nil {
+ t.Errorf("cannot get SyncGroup by ID %d: %v", sgId, err)
+ }
+ if !reflect.DeepEqual(sgOut, sg) {
+ t.Errorf("invalid SyncGroup data for group ID %d: got %v instead of %v", sgId, sgOut, sg)
+ }
+
+ sgOut, err = getSyncGroupByName(nil, st, sgName)
+ if err != nil {
+ t.Errorf("cannot get SyncGroup by Name %s: %v", sgName, err)
+ }
+ if !reflect.DeepEqual(sgOut, sg) {
+ t.Errorf("invalid SyncGroup data for group name %s: got %v instead of %v", sgName, sgOut, sg)
+ }
+
+ // Verify membership data.
+
+ expMembers := map[string]uint32{"phone": 1, "tablet": 1, "cloud": 1}
+
+ members := svc.sync.getMembers(nil)
+ if !reflect.DeepEqual(members, expMembers) {
+ t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+ }
+
+ view := svc.sync.allMembers
+ for mm := range members {
+ mi := view.members[mm]
+ if mi == nil {
+ t.Errorf("cannot get info for SyncGroup member %s", mm)
+ }
+ if len(mi.gid2info) != 1 {
+ t.Errorf("invalid info for SyncGroup member %s: %v", mm, mi)
+ }
+ expJoinerInfo := sg.Joiners[mm]
+ joinerInfo := mi.gid2info[sgId]
+ if !reflect.DeepEqual(joinerInfo, expJoinerInfo) {
+ t.Errorf("invalid Info for SyncGroup member %s in group ID %d: got %v instead of %v",
+ mm, sgId, joinerInfo, expJoinerInfo)
+ }
+ }
+
+ checkSGStats(t, svc, "add-2", 1, 3)
+
+ // Adding a SyncGroup for a pre-existing group ID or name should fail.
+
+ sg.Name = "another-name"
+
+ tx = st.NewTransaction()
+ if err = addSyncGroup(nil, tx, sg); err == nil {
+ t.Errorf("re-adding SyncGroup %d did not fail", sgId)
+ }
+ tx.Abort()
+
+ sg.Name = sgName
+ sg.Id = GroupId(5555)
+
+ tx = st.NewTransaction()
+ if err = addSyncGroup(nil, tx, sg); err == nil {
+ t.Errorf("adding SyncGroup %s with a different ID did not fail", sgName)
+ }
+ tx.Abort()
+
+ checkSGStats(t, svc, "add-3", 1, 3)
+
+ // Fetch a non-existing SyncGroup by ID or name should fail.
+
+ badName := "not-available"
+ badId := GroupId(999)
+ if id, err := getSyncGroupId(nil, st, badName); err == nil {
+ t.Errorf("found non-existing SyncGroup %s: got ID %d", badName, id)
+ }
+ if name, err := getSyncGroupName(nil, st, badId); err == nil {
+ t.Errorf("found non-existing SyncGroup %d: got name %s", badId, name)
+ }
+ if sg, err := getSyncGroupByName(nil, st, badName); err == nil {
+ t.Errorf("found non-existing SyncGroup %s: got %v", badName, sg)
+ }
+ if sg, err := getSyncGroupById(nil, st, badId); err == nil {
+ t.Errorf("found non-existing SyncGroup %d: got %v", badId, sg)
+ }
+}
+
+// TestInvalidAddSyncGroup tests adding SyncGroups.
+func TestInvalidAddSyncGroup(t *testing.T) {
+ svc := createService(t)
+ st := svc.St()
+
+ checkBadAddSyncGroup := func(t *testing.T, st store.Store, sg *SyncGroup, msg string) {
+ tx := st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg); err == nil {
+ t.Errorf("checkBadAddSyncGroup: adding bad SyncGroup (%s) did not fail", msg)
+ }
+ tx.Abort()
+ }
+
+ checkBadAddSyncGroup(t, st, nil, "nil SG")
+
+ sg := &SyncGroup{Id: 1234}
+ checkBadAddSyncGroup(t, st, sg, "SG w/o name")
+
+ sg = &SyncGroup{Name: "foobar"}
+ checkBadAddSyncGroup(t, st, sg, "SG w/o Id")
+
+ sg.Id = 1234
+ checkBadAddSyncGroup(t, st, sg, "SG w/o Version")
+
+ sg.Version = "v1"
+ checkBadAddSyncGroup(t, st, sg, "SG w/o Joiners")
+
+ sg.Joiners = map[string]nosql.SyncGroupMemberInfo{
+ "phone": nosql.SyncGroupMemberInfo{SyncPriority: 10},
+ }
+ checkBadAddSyncGroup(t, st, sg, "SG w/o Prefixes")
+}
+
+// TestDeleteSyncGroup tests deleting a SyncGroup.
+func TestDeleteSyncGroup(t *testing.T) {
+ svc := createService(t)
+ st := svc.St()
+
+ sgName := "foobar"
+ sgId := GroupId(1234)
+
+ // Delete non-existing SyncGroups.
+
+ tx := st.NewTransaction()
+ if err := delSyncGroupById(nil, tx, sgId); err == nil {
+ t.Errorf("deleting a non-existing SyncGroup ID did not fail")
+ }
+ if err := delSyncGroupByName(nil, tx, sgName); err == nil {
+ t.Errorf("deleting a non-existing SyncGroup name did not fail")
+ }
+ tx.Abort()
+
+ checkSGStats(t, svc, "del-1", 0, 0)
+
+ // Create the SyncGroup to delete later.
+
+ sg := &SyncGroup{
+ Name: sgName,
+ Id: sgId,
+ Version: "etag-0",
+ Spec: nosql.SyncGroupSpec{
+ Prefixes: []string{"foo", "bar"},
+ },
+ Joiners: map[string]nosql.SyncGroupMemberInfo{
+ "phone": nosql.SyncGroupMemberInfo{SyncPriority: 10},
+ "tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+ "cloud": nosql.SyncGroupMemberInfo{SyncPriority: 1},
+ },
+ }
+
+ tx = st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg); err != nil {
+ t.Errorf("creating SyncGroup ID %d failed: %v", sgId, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId, err)
+ }
+
+ checkSGStats(t, svc, "del-2", 1, 3)
+
+ // Delete it by ID.
+
+ tx = st.NewTransaction()
+ if err := delSyncGroupById(nil, tx, sgId); err != nil {
+ t.Errorf("deleting SyncGroup ID %d failed: %v", sgId, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit deleting SyncGroup ID %d: %v", sgId, err)
+ }
+
+ checkSGStats(t, svc, "del-3", 0, 0)
+
+ // Create it again then delete it by name.
+
+ tx = st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg); err != nil {
+ t.Errorf("creating SyncGroup ID %d after delete failed: %v", sgId, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit adding SyncGroup ID %d after delete: %v", sgId, err)
+ }
+
+ checkSGStats(t, svc, "del-4", 1, 3)
+
+ tx = st.NewTransaction()
+ if err := delSyncGroupByName(nil, tx, sgName); err != nil {
+ t.Errorf("deleting SyncGroup name %s failed: %v", sgName, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit deleting SyncGroup name %s: %v", sgName, err)
+ }
+
+ checkSGStats(t, svc, "del-5", 0, 0)
+}
+
+// TestMultiSyncGroups tests creating multiple SyncGroups.
+func TestMultiSyncGroups(t *testing.T) {
+ svc := createService(t)
+ st := svc.St()
+
+ sgName1, sgName2 := "foo", "bar"
+ sgId1, sgId2 := GroupId(1234), GroupId(8888)
+
+ // Add two SyncGroups.
+
+ sg1 := &SyncGroup{
+ Name: sgName1,
+ Id: sgId1,
+ Version: "etag-1",
+ Spec: nosql.SyncGroupSpec{
+ Prefixes: []string{"foo"},
+ },
+ Joiners: map[string]nosql.SyncGroupMemberInfo{
+ "phone": nosql.SyncGroupMemberInfo{SyncPriority: 10},
+ "tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25},
+ "cloud": nosql.SyncGroupMemberInfo{SyncPriority: 1},
+ },
+ }
+ sg2 := &SyncGroup{
+ Name: sgName2,
+ Id: sgId2,
+ Version: "etag-2",
+ Spec: nosql.SyncGroupSpec{
+ Prefixes: []string{"bar"},
+ },
+ Joiners: map[string]nosql.SyncGroupMemberInfo{
+ "tablet": nosql.SyncGroupMemberInfo{SyncPriority: 111},
+ "door": nosql.SyncGroupMemberInfo{SyncPriority: 33},
+ "lamp": nosql.SyncGroupMemberInfo{SyncPriority: 9},
+ },
+ }
+
+ tx := st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg1); err != nil {
+ t.Errorf("creating SyncGroup ID %d failed: %v", sgId1, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId1, err)
+ }
+
+ checkSGStats(t, svc, "multi-1", 1, 3)
+
+ tx = st.NewTransaction()
+ if err := addSyncGroup(nil, tx, sg2); err != nil {
+ t.Errorf("creating SyncGroup ID %d failed: %v", sgId2, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit adding SyncGroup ID %d: %v", sgId2, err)
+ }
+
+ checkSGStats(t, svc, "multi-2", 2, 5)
+
+ // Verify membership data.
+
+ expMembers := map[string]uint32{"phone": 1, "tablet": 2, "cloud": 1, "door": 1, "lamp": 1}
+
+ members := svc.sync.getMembers(nil)
+ if !reflect.DeepEqual(members, expMembers) {
+ t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+ }
+
+ expMemberInfo := map[string]*memberInfo{
+ "phone": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId1: sg1.Joiners["phone"],
+ },
+ },
+ "tablet": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId1: sg1.Joiners["tablet"],
+ sgId2: sg2.Joiners["tablet"],
+ },
+ },
+ "cloud": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId1: sg1.Joiners["cloud"],
+ },
+ },
+ "door": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId2: sg2.Joiners["door"],
+ },
+ },
+ "lamp": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId2: sg2.Joiners["lamp"],
+ },
+ },
+ }
+
+ view := svc.sync.allMembers
+ for mm := range members {
+ mi := view.members[mm]
+ if mi == nil {
+ t.Errorf("cannot get info for SyncGroup member %s", mm)
+ }
+ expInfo := expMemberInfo[mm]
+ if !reflect.DeepEqual(mi, expInfo) {
+ t.Errorf("invalid Info for SyncGroup member %s: got %v instead of %v", mm, mi, expInfo)
+ }
+ }
+
+ // Delete the 1st SyncGroup.
+
+ tx = st.NewTransaction()
+ if err := delSyncGroupById(nil, tx, sgId1); err != nil {
+ t.Errorf("deleting SyncGroup ID %d failed: %v", sgId1, err)
+ }
+ if err := tx.Commit(); err != nil {
+ t.Errorf("cannot commit deleting SyncGroup ID %d: %v", sgId1, err)
+ }
+
+ checkSGStats(t, svc, "multi-3", 1, 3)
+
+ // Verify SyncGroup membership data.
+
+ expMembers = map[string]uint32{"tablet": 1, "door": 1, "lamp": 1}
+
+ members = svc.sync.getMembers(nil)
+ if !reflect.DeepEqual(members, expMembers) {
+ t.Errorf("invalid SyncGroup members: got %v instead of %v", members, expMembers)
+ }
+
+ expMemberInfo = map[string]*memberInfo{
+ "tablet": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId2: sg2.Joiners["tablet"],
+ },
+ },
+ "door": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId2: sg2.Joiners["door"],
+ },
+ },
+ "lamp": &memberInfo{
+ gid2info: map[GroupId]nosql.SyncGroupMemberInfo{
+ sgId2: sg2.Joiners["lamp"],
+ },
+ },
+ }
+
+ view = svc.sync.allMembers
+ for mm := range members {
+ mi := view.members[mm]
+ if mi == nil {
+ t.Errorf("cannot get info for SyncGroup member %s", mm)
+ }
+ expInfo := expMemberInfo[mm]
+ if !reflect.DeepEqual(mi, expInfo) {
+ t.Errorf("invalid Info for SyncGroup member %s: got %v instead of %v", mm, mi, expInfo)
+ }
+ }
+}
diff --git a/services/syncbase/vsync/util_test.go b/services/syncbase/vsync/util_test.go
new file mode 100644
index 0000000..7db1552
--- /dev/null
+++ b/services/syncbase/vsync/util_test.go
@@ -0,0 +1,93 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+// Utilities for testing sync.
+
+import (
+ "testing"
+
+ "v.io/syncbase/x/ref/services/syncbase/server/interfaces"
+ "v.io/syncbase/x/ref/services/syncbase/store"
+ "v.io/syncbase/x/ref/services/syncbase/store/memstore"
+ "v.io/v23/context"
+ "v.io/v23/rpc"
+ "v.io/v23/security/access"
+ "v.io/v23/verror"
+)
+
+// mockService emulates a Syncbase service that includes store and sync.
+// It is used to access a mock application.
+type mockService struct {
+ st store.Store
+ sync *syncService
+}
+
+func (s *mockService) St() store.Store {
+ return s.st
+}
+
+func (s *mockService) App(ctx *context.T, call rpc.ServerCall, appName string) (interfaces.App, error) {
+ return &mockApp{st: s.st}, nil
+}
+
+func (s *mockService) AppNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+ return []string{"mockapp"}, nil
+}
+
+// mockApp emulates a Syncbase App. It is used to access a mock database.
+type mockApp struct {
+ st store.Store
+}
+
+func (a *mockApp) NoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) (interfaces.Database, error) {
+ return &mockDatabase{st: a.st}, nil
+}
+
+func (a *mockApp) NoSQLDatabaseNames(ctx *context.T, call rpc.ServerCall) ([]string, error) {
+ return []string{"mockdb"}, nil
+}
+
+func (a *mockApp) CreateNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions) error {
+ return verror.NewErrNotImplemented(ctx)
+}
+
+func (a *mockApp) DeleteNoSQLDatabase(ctx *context.T, call rpc.ServerCall, dbName string) error {
+ return verror.NewErrNotImplemented(ctx)
+}
+
+func (a *mockApp) SetDatabasePerms(ctx *context.T, call rpc.ServerCall, dbName string, perms access.Permissions, version string) error {
+ return verror.NewErrNotImplemented(ctx)
+}
+
+// mockDatabase emulates a Syncbase Database. It is used to test sync functionality.
+type mockDatabase struct {
+ st store.Store
+}
+
+func (d *mockDatabase) St() store.Store {
+ return d.st
+}
+
+func (d *mockDatabase) CheckPermsInternal(ctx *context.T, call rpc.ServerCall) error {
+ return verror.NewErrNotImplemented(ctx)
+}
+
+func (d *mockDatabase) SetPermsInternal(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
+ return verror.NewErrNotImplemented(ctx)
+}
+
+// createService creates a mock Syncbase service used for testing sync functionality.
+// At present it relies on the underlying Memstore engine.
+func createService(t *testing.T) *mockService {
+ var err error
+ s := &mockService{
+ st: memstore.New(),
+ }
+ if s.sync, err = New(nil, nil, s); err != nil {
+ t.Fatalf("cannot create sync service: %v", err)
+ }
+ return s
+}