| // 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) |
| } |
| } |