internal/grpcrr: record/replay for gRPC
Add the grpcrr package, a wrapper around a gRPC replayer
with an API similar to httprr.
There are no tests, because this is a thin wrapper around
a tested package, and because it is hard to write tests for gRPC.
Change-Id: Iffb44856f62b44d842a65bac0da95c7a2414c60b
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/599696
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/go.mod b/go.mod
index 01e8dfc..80fbe5e 100644
--- a/go.mod
+++ b/go.mod
@@ -36,6 +36,7 @@
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/go-replayers/grpcreplay v1.2.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
@@ -66,6 +67,6 @@
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
- google.golang.org/grpc v1.64.0 // indirect
+ google.golang.org/grpc v1.64.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
diff --git a/go.sum b/go.sum
index d74f0a4..01b7262 100644
--- a/go.sum
+++ b/go.sum
@@ -161,6 +161,8 @@
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-replayers/grpcreplay v1.2.0 h1:JlxRH0a1d8lLTXq6Xl+CD5aqMwHNhMyMxvFjqa3EAvg=
+github.com/google/go-replayers/grpcreplay v1.2.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -558,6 +560,8 @@
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/internal/grpcrr/rr.go b/internal/grpcrr/rr.go
new file mode 100644
index 0000000..c9ac391
--- /dev/null
+++ b/internal/grpcrr/rr.go
@@ -0,0 +1,107 @@
+// Copyright 2024 The Go 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 grpcrr implements gRPC record and replay, mainly for use in tests.
+// The client using gRPC must accept options of type
+// [google.golang.org/api/option.ClientOption].
+//
+// [Open] creates a new [RecordReplay]. Whether it is recording or replaying
+// is controlled by the -grpcrecord flag, which is defined by this package
+// only in test programs (built by “go test”).
+// See the [Open] documentation for more details.
+package grpcrr
+
+import (
+ "flag"
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/google/go-replayers/grpcreplay"
+ "google.golang.org/api/option"
+)
+
+var record = new(string)
+
+func init() {
+ if testing.Testing() {
+ record = flag.String("grpcrecord", "", "re-record traces for files matching `regexp`")
+ }
+}
+
+// A RecordReplay can operate in two modes: record and replay.
+//
+// In record mode, the RecordReplay intercepts gRPC calls
+// and logs the requests and responses to a file.
+//
+// In replay mode, the RecordReplay responds to requests by finding an identical
+// request in the log and sending the logged response.
+type RecordReplay struct {
+ recorder *grpcreplay.Recorder
+ replayer *grpcreplay.Replayer
+}
+
+// Open opens a new record/replay log in the named file and
+// returns a [RecordReplay] backed by that file.
+//
+// By default Open expects the file to exist and contain a
+// previously-recorded log of RPCs, which are consulted for replies.
+//
+// If the command-line flag -grpcrecord is set to a non-empty regular expression
+// that matches file, then Open creates the file as a new log.
+// In that mode, actual RPCs are made and also logged to the file for replaying in
+// a future run.
+//
+// After Open succeeds, pass the return value of [RecordReplay.ClientOptions] to
+// a NewClient function to enable record/replay.
+func Open(file string) (*RecordReplay, error) {
+ if *record != "" {
+ re, err := regexp.Compile(*record)
+ if err != nil {
+ return nil, fmt.Errorf("invalid -grpcrecord flag: %v", err)
+ }
+ if re.MatchString(file) {
+ rec, err := grpcreplay.NewRecorder(file, &grpcreplay.RecorderOptions{Text: true})
+ if err != nil {
+ return nil, err
+ }
+ return &RecordReplay{recorder: rec}, nil
+ }
+ }
+ rep, err := grpcreplay.NewReplayer(file, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &RecordReplay{replayer: rep}, nil
+}
+
+// ClientOptions returns options to pass to a gRPC client
+// that accepts them.
+func (r *RecordReplay) ClientOptions() []option.ClientOption {
+ if r.recorder != nil {
+ var opts []option.ClientOption
+ for _, gopt := range r.recorder.DialOptions() {
+ opts = append(opts, option.WithGRPCDialOption(gopt))
+ }
+ return opts
+ }
+ conn, err := r.replayer.Connection()
+ if err != nil {
+ panic("replayer could not create connection")
+ }
+ return []option.ClientOption{option.WithGRPCConn(conn)}
+}
+
+// Close closes the RecordReplay.
+func (rr *RecordReplay) Close() error {
+ if rr.recorder != nil {
+ return rr.recorder.Close()
+ }
+ return rr.replayer.Close()
+}
+
+// Recording reports whether rr is in recording mode.
+func (rr *RecordReplay) Recording() bool {
+ return rr.recorder != nil
+}