blob: 42b4c93200cfc6f87614e8242a6db93bbffd9be2 [file] [log] [blame]
// 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 featuretests_test
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
"testing"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/security/access"
wire "v.io/v23/services/syncbase"
"v.io/v23/syncbase"
"v.io/x/ref/services/syncbase/syncbaselib"
tu "v.io/x/ref/services/syncbase/testutil"
"v.io/x/ref/test/v23test"
)
const (
testSbName = "syncbase" // Name that syncbase mounts itself at.
)
var (
appExtension = "o:app"
clientExtension = appExtension + ":client"
testDb = wire.Id{Blessing: "root:" + appExtension, Name: "d"}
testCx = wire.Id{Blessing: "root:" + clientExtension, Name: "c"}
)
////////////////////////////////////////////////////////////
// Helpers for setting up Syncbases, dbs, and collections
func setupHierarchy(ctx *context.T, syncbaseName string, perms access.Permissions) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
return d.Create(ctx, perms)
}
func createCollection(ctx *context.T, syncbaseName, collectionName string) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
return d.CollectionForId(collectionId).Create(ctx, nil)
}
type testSyncbase struct {
sbName string
sbCreds *v23test.Credentials
rootDir string
clientId string
clientCtx *context.T
cleanup func(sig os.Signal)
}
// Spawns "num" Syncbase instances and returns handles to them.
func setupSyncbases(t testing.TB, sh *v23test.Shell, num int, devMode, skipPublishInNh bool) []*testSyncbase {
sbs := make([]*testSyncbase, num)
for i, _ := range sbs {
sbName, clientId := fmt.Sprintf("s%d", i), fmt.Sprintf("%s:c%d", clientExtension, i)
clientCtx := sh.ForkContext(clientId)
sbCreds := sh.ForkCredentialsFromPrincipal(v23.GetPrincipal(clientCtx), sbName)
if sh.Err != nil {
tu.Fatal(t, sh.Err)
}
sbs[i] = &testSyncbase{
sbName: sbName,
sbCreds: sbCreds,
rootDir: sh.MakeTempDir(),
clientId: clientId,
clientCtx: clientCtx,
}
// Give XRWA permissions to this Syncbase's client.
clientBlessing := fmt.Sprintf("root:%s", clientId)
acl := fmt.Sprintf(`{"Resolve":{"In":["%s"]},"Read":{"In":["%s"]},"Write":{"In":["%s"]},"Admin":{"In":["%s"]}}`, clientBlessing, clientBlessing, clientBlessing, clientBlessing)
sbs[i].cleanup = sh.StartSyncbase(sbs[i].sbCreds, syncbaselib.Opts{Name: sbs[i].sbName, RootDir: sbs[i].rootDir, DevMode: devMode, SkipPublishInNh: skipPublishInNh}, acl)
// Call setupHierarchy on each Syncbase.
dbPerms := tu.DefaultPerms(wire.AllDatabaseTags, clientBlessing)
ok(t, setupHierarchy(sbs[i].clientCtx, sbs[i].sbName, dbPerms))
}
return sbs
}
// Returns a ";"-separated list of Syncbase clients blessing names.
func clBlessings(sbs []*testSyncbase) string {
names := make([]string, len(sbs))
for i, sb := range sbs {
names[i] = "root:" + sb.clientId
}
return strings.Join(names, ";")
}
////////////////////////////////////////////////////////////
// Helpers for adding or updating data
func populateData(ctx *context.T, syncbaseName, collectionName, keyPrefix string, start, end int) error {
if end <= start {
return fmt.Errorf("end (%d) <= start (%d)", end, start)
}
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
c := d.CollectionForId(collectionId)
for i := start; i < end; i++ {
key := fmt.Sprintf("%s%d", keyPrefix, i)
if err := c.Put(ctx, key, "testkey"+key); err != nil {
return fmt.Errorf("c.Put() failed: %v", err)
}
}
return nil
}
// Shared by updateData and updateBatchData.
// TODO(sadovsky): This is eerily similar to populateData. We should strive to
// avoid such redundancies by implementing common helpers and avoiding spurious
// differences.
func updateDataImpl(ctx *context.T, d syncbase.DatabaseHandle, syncbaseName string, start, end int, valuePrefix string) error {
if end <= start {
return fmt.Errorf("end (%d) <= start (%d)", end, start)
}
if valuePrefix == "" {
valuePrefix = "testkey"
}
c := d.CollectionForId(testCx)
for i := start; i < end; i++ {
key := fmt.Sprintf("foo%d", i)
if err := c.Put(ctx, key, valuePrefix+syncbaseName+key); err != nil {
return fmt.Errorf("c.Put() failed: %v", err)
}
}
return nil
}
func updateData(ctx *context.T, syncbaseName string, start, end int, valuePrefix string) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
return updateDataImpl(ctx, d, syncbaseName, start, end, valuePrefix)
}
// Signal key tells the module to send an end signal, using the signalKey
// provided as the key, once the module finishes its execution. This is useful
// if the module is run as a goroutine and the parent needs to wait for it to
// end.
func updateDataInBatch(ctx *context.T, syncbaseName string, start, end int, valuePrefix, signalKey string) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
batch, err := d.BeginBatch(ctx, wire.BatchOptions{})
if err != nil {
return fmt.Errorf("BeginBatch failed: %v", err)
}
if err = updateDataImpl(ctx, batch, syncbaseName, start, end, valuePrefix); err != nil {
return err
}
if err = batch.Commit(ctx); err != nil {
return fmt.Errorf("Commit failed: %v", err)
}
if signalKey != "" {
return sendSignal(ctx, d, signalKey)
}
return nil
}
// TODO(ivanpi): Remove sendSignal now that all functions using it are in the
// same process.
func sendSignal(ctx *context.T, d syncbase.Database, signalKey string) error {
c := d.CollectionForId(testCx)
r := c.Row(signalKey)
if err := r.Put(ctx, true); err != nil {
return fmt.Errorf("r.Put() failed: %v", err)
}
return nil
}
////////////////////////////////////////////////////////////
// Helpers for verifying data
func verifySyncgroupData(ctx *context.T, syncbaseName, collectionName, keyPrefix, valuePrefix string, start, count int) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
collectionId := wire.Id{Blessing: testCx.Blessing, Name: collectionName}
c := d.CollectionForId(collectionId)
// Wait a bit (up to 10 seconds) for the last key to appear.
lastKey := fmt.Sprintf("%s%d", keyPrefix, start+count-1)
for i := 0; i < 20; i++ {
var value string
if err := c.Get(ctx, lastKey, &value); err == nil {
break
}
time.Sleep(500 * time.Millisecond)
}
if valuePrefix == "" {
valuePrefix = "testkey"
}
// Verify that all keys and values made it over correctly.
for i := start; i < start+count; i++ {
key := fmt.Sprintf("%s%d", keyPrefix, i)
var got string
if err := c.Get(ctx, key, &got); err != nil {
return fmt.Errorf("c.Get() failed: %v", err)
}
want := valuePrefix + key
if got != want {
return fmt.Errorf("unexpected value: got %q, want %q", got, want)
}
}
return nil
}
////////////////////////////////////////////////////////////
// Helpers for managing syncgroups
// blessingPatterns is a ";"-separated list of blessing patterns.
func createSyncgroup(ctx *context.T, syncbaseName string, sgId wire.Id, sgColls, mtName string,
perms access.Permissions, clBlessings, publishSyncbaseName string, info wire.SyncgroupMemberInfo) error {
if mtName == "" {
roots := v23.GetNamespace(ctx).Roots()
if len(roots) == 0 {
return errors.New("no namespace roots")
}
mtName = roots[0]
}
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
if perms == nil {
perms = tu.DefaultPerms(wire.AllSyncgroupTags, strings.Split(clBlessings, ";")...)
}
collectionPerms := tu.DefaultPerms(wire.AllCollectionTags, strings.Split(clBlessings, ";")...)
spec := wire.SyncgroupSpec{
Description: "test syncgroup sg",
Perms: perms,
Collections: parseSgCollections(sgColls),
MountTables: []string{mtName},
PublishSyncbaseName: publishSyncbaseName,
}
// Change the collection ACLs to enable syncing.
for _, cId := range spec.Collections {
// TODO(ivanpi,hpucha): Switch to blessings of the form "idp:o:root:c<n>"
// (different users instead of delegates of one user) and get the collection
// id from context.
c := d.CollectionForId(cId)
// Ignore the error since sometimes a collection might already exist.
c.Create(ctx, nil)
if err := c.SetPermissions(ctx, collectionPerms); err != nil {
return fmt.Errorf("{%q, %v} c.SetPermissions failed: %v", syncbaseName, cId, err)
}
}
sg := d.SyncgroupForId(sgId)
if err := sg.Create(ctx, spec, info); err != nil {
return fmt.Errorf("{%q, %v} sg.Create() failed: %v", syncbaseName, sgId, err)
}
return nil
}
func joinSyncgroup(ctx *context.T, sbNameLocal, sbNameRemote string, sgId wire.Id, info wire.SyncgroupMemberInfo) error {
d := syncbase.NewService(sbNameLocal).DatabaseForId(testDb, nil)
sg := d.SyncgroupForId(sgId)
if _, err := sg.Join(ctx, sbNameRemote, nil, info); err != nil {
return fmt.Errorf("{%q, %q} sg.Join() failed: %v", sbNameRemote, sgId, err)
}
return nil
}
func verifySyncgroupMembers(ctx *context.T, syncbaseName string, sgId wire.Id, wantMembers int) error {
d := syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil)
sg := d.SyncgroupForId(sgId)
var gotMembers int
for i := 0; i < 8; i++ {
members, err := sg.GetMembers(ctx)
if err != nil {
return fmt.Errorf("{%q, %q} sg.GetMembers() failed: %v", syncbaseName, sgId, err)
}
gotMembers = len(members)
if gotMembers == wantMembers {
break
}
time.Sleep(500 * time.Millisecond)
}
if gotMembers != wantMembers {
return fmt.Errorf("{%q, %q} verifySyncgroupMembers failed: got %d members, want %d", syncbaseName, sgId, gotMembers, wantMembers)
}
return nil
}
func pauseSync(ctx *context.T, syncbaseName string) error {
return syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil).PauseSync(ctx)
}
func resumeSync(ctx *context.T, syncbaseName string) error {
return syncbase.NewService(syncbaseName).DatabaseForId(testDb, nil).ResumeSync(ctx)
}
////////////////////////////////////////////////////////////
// Syncbase-specific testing helpers
// parseSgCollections converts, for example, "a,c" to
// [Collection: {clientBlessing, "a"}, Collection: {clientBlessing, "c"}].
func parseSgCollections(csv string) []wire.Id {
// Note that "," is allowed to appear in syncgroup names in general.
// Restricting it in tests is acceptable.
strs := strings.Split(csv, ",")
res := make([]wire.Id, len(strs))
for i, v := range strs {
res[i] = wire.Id{testCx.Blessing, v}
}
return res
}
////////////////////////////////////////////////////////////
// Helpers to interact with the Syncbase service directly.
func sc(name string) wire.ServiceClientStub {
return wire.ServiceClient(name)
}
////////////////////////////////////////////////////////////
// Generic testing helpers
func ok(t testing.TB, err error) {
if err != nil {
tu.Fatal(t, err)
}
}
func nok(t testing.TB, err error) {
if err == nil {
tu.Fatal(t, "nil err")
}
}
func eq(t testing.TB, got, want interface{}) {
if !reflect.DeepEqual(got, want) {
tu.Fatalf(t, "got %v, want %v", got, want)
}
}
func neq(t testing.TB, got, notWant interface{}) {
if reflect.DeepEqual(got, notWant) {
tu.Fatalf(t, "got %v", got)
}
}