internal/runtimeconfig: add tests.
Allow user to set any minimum value for wait time > 0.
Change-Id: I61592707f840029c6f401e383d66fd9608b993f5
Reviewed-on: https://go-review.googlesource.com/85655
Reviewed-by: Ross Light <light@google.com>
diff --git a/internal/runtimeconfig/example_test.go b/internal/runtimeconfig/example_test.go
new file mode 100644
index 0000000..dcf0517
--- /dev/null
+++ b/internal/runtimeconfig/example_test.go
@@ -0,0 +1,45 @@
+package runtimeconfig_test
+
+import (
+ "context"
+ "log"
+
+ "github.com/golang/gddo/internal/runtimeconfig"
+)
+
+func Example() {
+ // Create a Client object.
+ ctx := context.Background()
+ client, err := runtimeconfig.NewClient(ctx)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer client.Close()
+
+ // Create a Watcher object.
+ w, err := client.NewWatcher(ctx, "project", "config-name", "food", nil)
+ // Use retrieved Variable and apply to configurations accordingly.
+ log.Printf("value: %s\n", string(w.Variable().Value))
+
+ // Optionally, get a Context with cancel func to stop the Watch call.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // Have a separate goroutine that waits for changes.
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ // Cancelled or timed out.
+ return
+ default:
+ if err := w.Watch(ctx); err != nil {
+ // Log or handle other errors
+ continue
+ }
+ // Use updated variable accordingly.
+ log.Printf("value: %s\n", string(w.Variable().Value))
+ }
+ }
+ }()
+}
diff --git a/internal/runtimeconfig/runtimeconfig.go b/internal/runtimeconfig/runtimeconfig.go
index a29d1f6..9cc8c61 100644
--- a/internal/runtimeconfig/runtimeconfig.go
+++ b/internal/runtimeconfig/runtimeconfig.go
@@ -23,8 +23,12 @@
"google.golang.org/grpc/status"
)
-// endpoint is the address of the GCP Runtime Configurator API.
-const endPoint = "runtimeconfig.googleapis.com:443"
+const (
+ // endpoint is the address of the GCP Runtime Configurator API.
+ endPoint = "runtimeconfig.googleapis.com:443"
+ // defaultWaitTimeout is the default value for WatchOptions.WaitTime if not set.
+ defaultWaitTimeout = 10 * time.Minute
+)
// List of authentication scopes required for using the Runtime Configurator API.
var authScopes = []string{
@@ -32,11 +36,6 @@
"https://www.googleapis.com/auth/cloudruntimeconfig",
}
-const (
- defaultWaitTimeout = 10 * time.Minute
- minWaitTimeout = 10 * time.Second
-)
-
// Client is a RuntimeConfigManager client. It wraps the gRPC client stub and currently exposes
// only a few APIs primarily for fetching and watching over configuration variables.
type Client struct {
@@ -82,8 +81,8 @@
switch {
case waitTime == 0:
waitTime = defaultWaitTimeout
- case waitTime < minWaitTimeout:
- waitTime = minWaitTimeout
+ case waitTime < 0:
+ return nil, fmt.Errorf("cannot have negative WaitTime option value: %v", waitTime)
}
// Make sure update time is valid before copying.
@@ -105,10 +104,10 @@
type WatchOptions struct {
// WaitTime controls the frequency of making RPC and checking for updates by the Watch method.
// A Watcher keeps track of the last time it made an RPC, when Watch is called, it waits for
- // configured WaitTime from the last RPC before making another RPC.
+ // configured WaitTime from the last RPC before making another RPC. The smaller the value, the
+ // higher the frequency of making RPCs, which also means faster rate of hitting the API quota.
//
- // If this option is not set, it defaults to defaultWaitTimeout. If option is set to a value
- // smaller than minWaitTimeout, it uses minWaitTimeout value instead.
+ // If this option is not set or set to 0, it uses defaultWaitTimeout value.
WaitTime time.Duration
}
diff --git a/internal/runtimeconfig/runtimeconfig_test.go b/internal/runtimeconfig/runtimeconfig_test.go
index ee8178e..35fb3e8 100644
--- a/internal/runtimeconfig/runtimeconfig_test.go
+++ b/internal/runtimeconfig/runtimeconfig_test.go
@@ -2,42 +2,267 @@
import (
"context"
- "log"
+ "fmt"
+ "net"
+ "strings"
+ "testing"
+ "time"
+
+ tspb "github.com/golang/protobuf/ptypes/timestamp"
+ "github.com/google/go-cmp/cmp"
+ pb "google.golang.org/genproto/googleapis/cloud/runtimeconfig/v1beta1"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
)
-func Example() {
- // Create a Client object.
- ctx := context.Background()
- client, err := NewClient(ctx)
- if err != nil {
- log.Fatal(err)
+// Set wait timeout used for tests.
+var watchOpt = &WatchOptions{
+ WaitTime: 500 * time.Millisecond,
+}
+
+// fakeServer partially implements RuntimeConfigManagerServer for Client to connect to. Prefill
+// responses field with the ordered list of responses to GetVariable calls.
+type fakeServer struct {
+ pb.RuntimeConfigManagerServer
+ responses []response
+ index int
+}
+
+type response struct {
+ vrbl *pb.Variable
+ err error
+}
+
+func (s *fakeServer) GetVariable(context.Context, *pb.GetVariableRequest) (*pb.Variable, error) {
+ if len(s.responses) == 0 {
+ return nil, fmt.Errorf("fakeClient missing responses")
}
- defer client.Close()
+ resp := s.responses[s.index]
+ // Adjust index to next response for next call till it gets to last one, then keep using the
+ // last one.
+ if s.index < len(s.responses)-1 {
+ s.index++
+ }
+ return resp.vrbl, resp.err
+}
- // Create a Watcher object.
- w, err := client.NewWatcher(ctx, "project", "config-name", "food", nil)
- // Use retrieved Variable and apply to configurations accordingly.
- log.Printf("value: %s\n", string(w.Variable().Value))
+func setUp(t *testing.T, fs *fakeServer) (*Client, func()) {
+ // TODO: Replace logic to use a port picker.
+ const address = "localhost:8888"
+ // Set up gRPC server.
+ lis, err := net.Listen("tcp", address)
+ if err != nil {
+ t.Fatalf("tcp listen on %s failed: %v", address, err)
+ }
+ s := grpc.NewServer()
+ pb.RegisterRuntimeConfigManagerServer(s, fs)
+ // Run gRPC server on a background goroutine.
+ go s.Serve(lis)
- // Optionally, get a Context with cancel func to stop the Watch call.
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
-
- // Have a separate goroutine that waits for changes.
- go func() {
- for {
- select {
- case <-ctx.Done():
- // Cancelled or timed out.
- return
- default:
- if err := w.Watch(ctx); err != nil {
- // Log or handle other errors
- continue
- }
- // Use updated variable accordingly.
- log.Printf("value: %s\n", string(w.Variable().Value))
- }
+ // Set up client.
+ conn, err := grpc.Dial(address, grpc.WithInsecure())
+ if err != nil {
+ t.Fatalf("did not connect: %v", err)
+ }
+ return &Client{
+ conn: conn,
+ client: pb.NewRuntimeConfigManagerClient(conn),
+ }, func() {
+ conn.Close()
+ s.Stop()
+ time.Sleep(time.Second * 1)
}
+}
+
+func pbToVariable(vpb *pb.Variable) (*Variable, error) {
+ vrbl := &Variable{}
+ tm, err := parseUpdateTime(vpb)
+ if err != nil {
+ return nil, err
+ }
+ copyFromProto(vpb, vrbl, tm)
+ return vrbl, nil
+}
+
+var (
+ startTime = time.Now().Unix()
+ vrbl1 = &pb.Variable{
+ Name: "greetings",
+ Contents: &pb.Variable_Text{"hello"},
+ UpdateTime: &tspb.Timestamp{Seconds: startTime},
+ }
+ vrbl2 = &pb.Variable{
+ Name: "greetings",
+ Contents: &pb.Variable_Text{"world"},
+ UpdateTime: &tspb.Timestamp{Seconds: startTime + 100},
+ }
+)
+
+func TestNewWatcher(t *testing.T) {
+ client, cleanUp := setUp(t, &fakeServer{
+ responses: []response{
+ {vrbl: vrbl1},
+ },
+ })
+ defer cleanUp()
+
+ ctx := context.Background()
+ w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+ if err != nil {
+ t.Fatalf("Client.NewWatcher() returned error: %v", err)
+ }
+
+ got := w.Variable()
+ want, err := pbToVariable(vrbl1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(&got, want); diff != "" {
+ t.Errorf("Watcher.Variable(): %s", diff)
+ }
+}
+
+func TestWatchUpdatesVariable(t *testing.T) {
+ client, cleanUp := setUp(t, &fakeServer{
+ responses: []response{
+ {vrbl: vrbl1},
+ {vrbl: vrbl2},
+ },
+ })
+ defer cleanUp()
+
+ ctx := context.Background()
+ w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+ if err != nil {
+ t.Fatalf("Client.NewWatcher() returned error: %v", err)
+ }
+
+ if err := w.Watch(ctx); err != nil {
+ t.Fatalf("Watcher.Watch() returned error: %v", err)
+ }
+ got := w.Variable()
+ want, err := pbToVariable(vrbl2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(&got, want); diff != "" {
+ t.Errorf("Watcher.Variable(): %s", diff)
+ }
+}
+
+func TestWatchVariableDeletedAndReset(t *testing.T) {
+ client, cleanUp := setUp(t, &fakeServer{
+ responses: []response{
+ {vrbl: vrbl1},
+ {err: status.Error(codes.NotFound, "deleted")},
+ {vrbl: vrbl2},
+ },
+ })
+ defer cleanUp()
+
+ ctx := context.Background()
+ w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+ if err != nil {
+ t.Fatalf("Client.NewWatcher() returned error: %v", err)
+ }
+
+ // Expect deleted error.
+ if err := w.Watch(ctx); err == nil {
+ t.Fatalf("Watcher.Watch() returned nil, want error")
+ } else {
+ if !IsDeleted(err) {
+ t.Fatalf("Watcher.Watch() returned error %v, want errDeleted", err)
+ }
+ }
+
+ // Variable Name and Value should be the same, IsDeleted and UpdateTime should be updated.
+ got := w.Variable()
+ prev, err := pbToVariable(vrbl1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Name != prev.Name {
+ t.Errorf("Variable.Name got %v, want %v", got.Name, prev.Name)
+ }
+ if diff := cmp.Diff(got.Value, prev.Value); diff != "" {
+ t.Errorf("Variable.Value: %s", diff)
+ }
+ if !got.IsDeleted {
+ t.Errorf("Variable.IsDeleted got %v, want true", got.IsDeleted)
+ }
+ if !got.UpdateTime.After(prev.UpdateTime) {
+ t.Errorf("Variable.UpdateTime is less than or equal to previous value")
+ }
+
+ // Calling Watch again will produce vrbl2.
+ if err := w.Watch(ctx); err != nil {
+ t.Fatalf("Watcher.Watch() returned error: %v", err)
+ }
+ got = w.Variable()
+ want, err := pbToVariable(vrbl2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(&got, want); diff != "" {
+ t.Errorf("Watcher.Variable(): %s", diff)
+ }
+}
+
+func TestWatchCancelled(t *testing.T) {
+ client, cleanUp := setUp(t, &fakeServer{
+ responses: []response{
+ {vrbl: vrbl1},
+ },
+ })
+ defer cleanUp()
+
+ ctx := context.Background()
+ w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+ if err != nil {
+ t.Fatalf("Client.NewWatcher() returned error: %v", err)
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ go func() {
+ time.Sleep(1 * time.Second)
+ cancel()
}()
+
+ if err := w.Watch(ctx); err != context.Canceled {
+ t.Fatalf("Watcher.Watch() returned %v, want context.Canceled", err)
+ }
+}
+
+func TestWatchRPCError(t *testing.T) {
+ rpcErr := status.Error(codes.Internal, "bad")
+ client, cleanUp := setUp(t, &fakeServer{
+ responses: []response{
+ {vrbl: vrbl1},
+ {err: rpcErr},
+ },
+ })
+ defer cleanUp()
+
+ ctx := context.Background()
+ w, err := client.NewWatcher(ctx, "projectID", "config", "greetings", watchOpt)
+ if err != nil {
+ t.Fatalf("Client.NewWatcher() returned error: %v", err)
+ }
+
+ // Expect RPC error.
+ err = w.Watch(ctx)
+ if !strings.Contains(err.Error(), "bad") {
+ t.Errorf("Watcher.Watch() returned %v, want %v", err, rpcErr)
+ }
+
+ // Variable should still be the same.
+ got := w.Variable()
+ want, err := pbToVariable(vrbl1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(&got, want); diff != "" {
+ t.Errorf("Watcher.Variable(): %s", diff)
+ }
}