| // 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" |
| "time" |
| |
| "v.io/v23/services/syncbase/nosql" |
| _ "v.io/x/ref/runtime/factories/generic" |
| "v.io/x/ref/services/syncbase/server/interfaces" |
| "v.io/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[interfaces.GroupId]bool) |
| for _, info := range view.members { |
| for _, sgmi := range info.db2sg { |
| for gid := range sgmi { |
| sgids[gid] = true |
| } |
| } |
| } |
| |
| 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) { |
| // Set a large value to prevent the initiator from running. Since this |
| // test adds a fake SyncGroup, if the initiator runs, it will attempt |
| // to initiate using this fake and partial SyncGroup data. |
| peerSyncInterval = 1 * time.Hour |
| svc := createService(t) |
| defer destroyService(t, svc) |
| st := svc.St() |
| s := svc.sync |
| |
| checkSGStats(t, svc, "add-1", 0, 0) |
| |
| // Add a SyncGroup. |
| |
| sgName := "foobar" |
| sgId := interfaces.GroupId(1234) |
| version := "v111" |
| |
| sg := &interfaces.SyncGroup{ |
| Name: sgName, |
| Id: sgId, |
| AppName: "mockApp", |
| DbName: "mockDB", |
| Creator: "mockCreator", |
| SpecVersion: "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 := s.addSyncGroup(nil, tx, version, true, "", nil, s.id, 1, 1, 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) |
| } |
| |
| 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.db2sg) != 1 { |
| t.Errorf("invalid info for SyncGroup member %s: %v", mm, mi) |
| } |
| var sgmi sgMemberInfo |
| for _, v := range mi.db2sg { |
| sgmi = v |
| break |
| } |
| if len(sgmi) != 1 { |
| t.Errorf("invalid member info for SyncGroup member %s: %v", mm, sgmi) |
| } |
| expJoinerInfo := sg.Joiners[mm] |
| joinerInfo := sgmi[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 = s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 2, 2, sg); err == nil { |
| t.Errorf("re-adding SyncGroup %d did not fail", sgId) |
| } |
| tx.Abort() |
| |
| sg.Name = sgName |
| sg.Id = interfaces.GroupId(5555) |
| |
| tx = st.NewTransaction() |
| if err = s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 3, 3, 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 := interfaces.GroupId(999) |
| if id, err := getSyncGroupId(nil, st, badName); err == nil { |
| t.Errorf("found non-existing SyncGroup %s: got ID %d", badName, id) |
| } |
| 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) { |
| // Set a large value to prevent the threads from firing. |
| peerSyncInterval = 1 * time.Hour |
| svc := createService(t) |
| defer destroyService(t, svc) |
| st := svc.St() |
| s := svc.sync |
| |
| checkBadAddSyncGroup := func(t *testing.T, st store.Store, sg *interfaces.SyncGroup, msg string) { |
| tx := st.NewTransaction() |
| if err := s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 1, 1, sg); err == nil { |
| t.Errorf("checkBadAddSyncGroup: adding bad SyncGroup (%s) did not fail", msg) |
| } |
| tx.Abort() |
| } |
| |
| checkBadAddSyncGroup(t, st, nil, "nil SG") |
| |
| sg := &interfaces.SyncGroup{} |
| checkBadAddSyncGroup(t, st, sg, "SG w/o name") |
| |
| sg.Name = "foobar" |
| checkBadAddSyncGroup(t, st, sg, "SG w/o AppName") |
| |
| sg.AppName = "mockApp" |
| checkBadAddSyncGroup(t, st, sg, "SG w/o DbName") |
| |
| sg.DbName = "mockDb" |
| checkBadAddSyncGroup(t, st, sg, "SG w/o creator") |
| |
| sg.Creator = "haha" |
| checkBadAddSyncGroup(t, st, sg, "SG w/o ID") |
| |
| sg.Id = newSyncGroupId() |
| checkBadAddSyncGroup(t, st, sg, "SG w/o Version") |
| |
| sg.SpecVersion = "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") |
| |
| sg.Spec.Prefixes = []string{"foo", "bar", "foo"} |
| checkBadAddSyncGroup(t, st, sg, "SG with duplicate Prefixes") |
| } |
| |
| // TestDeleteSyncGroup tests deleting a SyncGroup. |
| func TestDeleteSyncGroup(t *testing.T) { |
| // Set a large value to prevent the threads from firing. |
| peerSyncInterval = 1 * time.Hour |
| svc := createService(t) |
| defer destroyService(t, svc) |
| st := svc.St() |
| s := svc.sync |
| |
| sgName := "foobar" |
| sgId := interfaces.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 := &interfaces.SyncGroup{ |
| Name: sgName, |
| Id: sgId, |
| AppName: "mockApp", |
| DbName: "mockDB", |
| Creator: "mockCreator", |
| SpecVersion: "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 := s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 1, 1, 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, update it, then delete it by name. |
| |
| tx = st.NewTransaction() |
| if err := s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 2, 2, 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) |
| } |
| |
| tx = st.NewTransaction() |
| if err := s.updateSyncGroupVersioning(nil, tx, NoVersion, true, s.id, 3, 3, sg); err != nil { |
| t.Errorf("updating SyncGroup ID %d version: %v", sgId, err) |
| } |
| if err := tx.Commit(); err != nil { |
| t.Errorf("cannot commit updating SyncGroup ID %d version: %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) { |
| // Set a large value to prevent the threads from firing. |
| peerSyncInterval = 1 * time.Hour |
| svc := createService(t) |
| defer destroyService(t, svc) |
| st := svc.St() |
| s := svc.sync |
| |
| sgName1, sgName2 := "foo", "bar" |
| sgId1, sgId2 := interfaces.GroupId(1234), interfaces.GroupId(8888) |
| |
| // Add two SyncGroups. |
| |
| sg1 := &interfaces.SyncGroup{ |
| Name: sgName1, |
| Id: sgId1, |
| AppName: "mockApp", |
| DbName: "mockDB", |
| Creator: "mockCreator", |
| SpecVersion: "etag-1", |
| Spec: nosql.SyncGroupSpec{ |
| MountTables: []string{"mt1"}, |
| Prefixes: []string{"foo"}, |
| }, |
| Joiners: map[string]nosql.SyncGroupMemberInfo{ |
| "phone": nosql.SyncGroupMemberInfo{SyncPriority: 10}, |
| "tablet": nosql.SyncGroupMemberInfo{SyncPriority: 25}, |
| "cloud": nosql.SyncGroupMemberInfo{SyncPriority: 1}, |
| }, |
| } |
| sg2 := &interfaces.SyncGroup{ |
| Name: sgName2, |
| Id: sgId2, |
| AppName: "mockApp", |
| DbName: "mockDB", |
| Creator: "mockCreator", |
| SpecVersion: "etag-2", |
| Spec: nosql.SyncGroupSpec{ |
| MountTables: []string{"mt2", "mt3"}, |
| 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 := s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 1, 1, 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 := s.addSyncGroup(nil, tx, NoVersion, true, "", nil, s.id, 2, 2, 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) |
| } |
| |
| mt2and3 := map[string]struct{}{ |
| "mt2": struct{}{}, |
| "mt3": struct{}{}, |
| } |
| |
| expMemberInfo := map[string]*memberInfo{ |
| "phone": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId1: sg1.Joiners["phone"], |
| }, |
| }, |
| mtTables: map[string]struct{}{"mt1": struct{}{}}, |
| }, |
| "tablet": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId1: sg1.Joiners["tablet"], |
| sgId2: sg2.Joiners["tablet"], |
| }, |
| }, |
| mtTables: map[string]struct{}{ |
| "mt1": struct{}{}, |
| "mt2": struct{}{}, |
| "mt3": struct{}{}, |
| }, |
| }, |
| "cloud": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId1: sg1.Joiners["cloud"], |
| }, |
| }, |
| mtTables: map[string]struct{}{"mt1": struct{}{}}, |
| }, |
| "door": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId2: sg2.Joiners["door"], |
| }, |
| }, |
| mtTables: mt2and3, |
| }, |
| "lamp": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId2: sg2.Joiners["lamp"], |
| }, |
| }, |
| mtTables: mt2and3, |
| }, |
| } |
| |
| 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{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId2: sg2.Joiners["tablet"], |
| }, |
| }, |
| mtTables: mt2and3, |
| }, |
| "door": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId2: sg2.Joiners["door"], |
| }, |
| }, |
| mtTables: mt2and3, |
| }, |
| "lamp": &memberInfo{ |
| db2sg: map[string]sgMemberInfo{ |
| "mockapp:mockdb": sgMemberInfo{ |
| sgId2: sg2.Joiners["lamp"], |
| }, |
| }, |
| mtTables: mt2and3, |
| }, |
| } |
| |
| 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) |
| } |
| } |
| } |
| |
| // TestPrefixCompare tests the prefix comparison utility. |
| func TestPrefixCompare(t *testing.T) { |
| check := func(t *testing.T, pfx1, pfx2 []string, want bool, msg string) { |
| if got := samePrefixes(pfx1, pfx2); got != want { |
| t.Errorf("samePrefixes: %s: got %t instead of %t", msg, got, want) |
| } |
| } |
| |
| check(t, nil, nil, true, "both nil") |
| check(t, []string{}, nil, true, "empty vs nil") |
| check(t, []string{"a", "b"}, []string{"b", "a"}, true, "different ordering") |
| check(t, []string{"a", "b", "c"}, []string{"b", "a"}, false, "p1 superset of p2") |
| check(t, []string{"a", "b"}, []string{"b", "a", "c"}, false, "p2 superset of p1") |
| check(t, []string{"a", "b", "c"}, []string{"b", "d", "a"}, false, "overlap") |
| check(t, []string{"a", "b", "c"}, []string{"x", "y"}, false, "no overlap") |
| check(t, []string{"a", "b"}, []string{"B", "a"}, false, "upper/lowercases") |
| } |