feat(cli): dynamically generate query CLI commands (#11725)

* WIP on auto-generating CLi

* WIP

* WIP

* WIP

* add pagination.go

* handle more flag types

* WIP on refactoring

* WIP

* working tests

* add docs

* echo all flags

* add repeated tests

* remove comment

* fix compositeListValue issue

Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Aaron Craelius
2022-04-27 18:24:42 -04:00
committed by GitHub
co-authored by Anil Kumar Kammari mergify[bot]
parent e44a4a9d80
commit 1c8a2d9069
27 changed files with 5069 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
package cli
import (
"context"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/client/v2/cli/flag"
)
// Builder manages options for building CLI commands.
type Builder struct {
// flag.Builder embeds the flag builder and its options.
flag.Builder
// GetClientConn specifies how CLI commands will resolve a grpc.ClientConnInterface
// from a given context.
GetClientConn func(context.Context) grpc.ClientConnInterface
}
+40
View File
@@ -0,0 +1,40 @@
package flag
import (
"context"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
)
type addressStringType struct{}
func (a addressStringType) NewValue(_ context.Context, _ *Builder) pflag.Value {
return &addressValue{}
}
func (a addressStringType) DefaultValue() string {
return ""
}
type addressValue struct {
value string
}
func (a addressValue) Get() protoreflect.Value {
return protoreflect.ValueOfString(a.value)
}
func (a addressValue) String() string {
return a.value
}
func (a *addressValue) Set(s string) error {
a.value = s
// TODO handle bech32 validation
return nil
}
func (a addressValue) Type() string {
return "bech32 account address key name"
}
+47
View File
@@ -0,0 +1,47 @@
package flag
import (
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
// Builder manages options for building pflag flags for protobuf messages.
type Builder struct {
// TypeResolver specifies how protobuf types will be resolved. If it is
// nil protoregistry.GlobalTypes will be used.
TypeResolver interface {
protoregistry.MessageTypeResolver
protoregistry.ExtensionTypeResolver
}
// FileResolver specifies how protobuf file descriptors will be resolved. If it is
// nil protoregistry.GlobalFiles will be used.
FileResolver protodesc.Resolver
messageFlagTypes map[protoreflect.FullName]Type
scalarFlagTypes map[string]Type
}
func (b *Builder) init() {
if b.messageFlagTypes == nil {
b.messageFlagTypes = map[protoreflect.FullName]Type{}
b.messageFlagTypes["google.protobuf.Timestamp"] = timestampType{}
b.messageFlagTypes["google.protobuf.Duration"] = durationType{}
}
if b.scalarFlagTypes == nil {
b.scalarFlagTypes = map[string]Type{}
b.scalarFlagTypes["cosmos.AddressString"] = addressStringType{}
}
}
func (b *Builder) DefineMessageFlagType(messageName protoreflect.FullName, flagType Type) {
b.init()
b.messageFlagTypes[messageName] = flagType
}
func (b *Builder) DefineScalarFlagType(scalarName string, flagType Type) {
b.init()
b.scalarFlagTypes[scalarName] = flagType
}
+52
View File
@@ -0,0 +1,52 @@
package flag
import (
"context"
"time"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
)
type durationType struct{}
func (t durationType) NewValue(context.Context, *Builder) pflag.Value {
return &durationValue{}
}
func (t durationType) DefaultValue() string {
return ""
}
type durationValue struct {
value *durationpb.Duration
}
func (t durationValue) Get() protoreflect.Value {
if t.value == nil {
return protoreflect.Value{}
}
return protoreflect.ValueOfMessage(t.value.ProtoReflect())
}
func (v durationValue) String() string {
if v.value == nil {
return ""
}
return v.value.AsDuration().String()
}
func (v *durationValue) Set(s string) error {
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
v.value = durationpb.New(dur)
return nil
}
func (v durationValue) Type() string {
return "duration"
}
+74
View File
@@ -0,0 +1,74 @@
package flag
import (
"context"
"fmt"
"strings"
"github.com/iancoleman/strcase"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
)
type enumType struct {
enum protoreflect.EnumDescriptor
}
func (b enumType) NewValue(context.Context, *Builder) pflag.Value {
val := &enumValue{
enum: b.enum,
valMap: map[string]protoreflect.EnumValueDescriptor{},
}
n := b.enum.Values().Len()
for i := 0; i < n; i++ {
valDesc := b.enum.Values().Get(i)
val.valMap[enumValueName(b.enum, valDesc)] = valDesc
}
return val
}
func (b enumType) DefaultValue() string {
defValue := ""
if def := b.enum.Values().ByNumber(0); def != nil {
defValue = enumValueName(b.enum, def)
}
return defValue
}
type enumValue struct {
enum protoreflect.EnumDescriptor
value protoreflect.EnumNumber
valMap map[string]protoreflect.EnumValueDescriptor
}
func (e enumValue) Get() protoreflect.Value {
return protoreflect.ValueOfEnum(e.value)
}
func enumValueName(enum protoreflect.EnumDescriptor, enumValue protoreflect.EnumValueDescriptor) string {
name := string(enumValue.Name())
name = strings.TrimPrefix(name, strcase.ToScreamingSnake(string(enum.Name()))+"_")
return strcase.ToKebab(name)
}
func (e enumValue) String() string {
return enumValueName(e.enum, e.enum.Values().ByNumber(e.value))
}
func (e *enumValue) Set(s string) error {
valDesc, ok := e.valMap[s]
if !ok {
return fmt.Errorf("%s is not a valid value for enum %s", s, e.enum.FullName())
}
e.value = valDesc.Number()
return nil
}
func (e enumValue) Type() string {
var vals []string
n := e.enum.Values().Len()
for i := 0; i < n; i++ {
vals = append(vals, enumValueName(e.enum, e.enum.Values().Get(i)))
}
return fmt.Sprintf("%s (%s)", e.enum.Name(), strings.Join(vals, " | "))
}
+129
View File
@@ -0,0 +1,129 @@
package flag
import (
"context"
"fmt"
cosmos_proto "github.com/cosmos/cosmos-proto"
"github.com/spf13/pflag"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
)
// FieldValueBinder wraps a flag value in a way that allows it to be bound
// to a particular field in a protobuf message.
type FieldValueBinder interface {
Bind(message protoreflect.Message, field protoreflect.FieldDescriptor)
}
// Options specifies options for specific flags.
type Options struct {
// Prefix is a prefix to prepend to all flags.
Prefix string
}
// AddFieldFlag adds a flag for the provided field to the flag set.
func (b *Builder) AddFieldFlag(ctx context.Context, flagSet *pflag.FlagSet, field protoreflect.FieldDescriptor, options Options) FieldValueBinder {
if field.Kind() == protoreflect.MessageKind && field.Message().FullName() == "cosmos.base.query.v1beta1.PageRequest" {
return b.bindPageRequest(ctx, flagSet, field)
}
name := options.Prefix + util.DescriptorKebabName(field)
usage := util.DescriptorDocs(field)
shorthand := ""
if typ := b.resolveFlagType(field); typ != nil {
val := typ.NewValue(ctx, b)
flagSet.AddFlag(&pflag.Flag{
Name: name,
Shorthand: shorthand,
Usage: usage,
DefValue: typ.DefaultValue(),
Value: val,
})
switch val := val.(type) {
case SimpleValue:
return simpleValueBinder{val}
case ListValue:
return listValueBinder{val}
default:
panic(fmt.Errorf("%T does not implement SimpleValue or ListValue", val))
}
}
if field.IsList() {
if value := bindSimpleListFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
return listValueBinder{value}
}
return nil
}
if value := bindSimpleFlag(flagSet, field.Kind(), name, shorthand, usage); value != nil {
return simpleValueBinder{value}
}
return nil
}
func (b *Builder) resolveFlagType(field protoreflect.FieldDescriptor) Type {
typ := b.resolveFlagTypeBasic(field)
if field.IsList() {
if typ != nil {
return compositeListType{simpleType: typ}
}
return nil
}
return typ
}
func (b *Builder) resolveFlagTypeBasic(field protoreflect.FieldDescriptor) Type {
scalar := proto.GetExtension(field.Options(), cosmos_proto.E_Scalar)
if scalar != nil {
b.init()
if typ, ok := b.scalarFlagTypes[scalar.(string)]; ok {
return typ
}
}
switch field.Kind() {
case protoreflect.EnumKind:
return enumType{enum: field.Enum()}
case protoreflect.MessageKind:
b.init()
if flagType, ok := b.messageFlagTypes[field.Message().FullName()]; ok {
return flagType
}
return jsonMessageFlagType{
messageDesc: field.Message(),
}
default:
return nil
}
}
type simpleValueBinder struct {
SimpleValue
}
func (s simpleValueBinder) Bind(message protoreflect.Message, field protoreflect.FieldDescriptor) {
val := s.Get()
if val.IsValid() {
message.Set(field, val)
} else {
message.Clear(field)
}
}
type listValueBinder struct {
ListValue
}
func (s listValueBinder) Bind(message protoreflect.Message, field protoreflect.FieldDescriptor) {
s.AppendTo(message.NewField(field).List())
}
+107
View File
@@ -0,0 +1,107 @@
package flag
import (
"context"
"fmt"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
)
func bindSimpleListFlag(flagSet *pflag.FlagSet, kind protoreflect.Kind, name, shorthand, usage string) ListValue {
switch kind {
case protoreflect.StringKind:
val := flagSet.StringSliceP(name, shorthand, nil, usage)
return listValue(func(list protoreflect.List) {
for _, x := range *val {
list.Append(protoreflect.ValueOfString(x))
}
})
case protoreflect.BytesKind:
// TODO
return nil
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
val := flagSet.UintSliceP(name, shorthand, nil, usage)
return listValue(func(list protoreflect.List) {
for _, x := range *val {
list.Append(protoreflect.ValueOfUint64(uint64(x)))
}
})
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
val := flagSet.IntSliceP(name, shorthand, nil, usage)
return listValue(func(list protoreflect.List) {
for _, x := range *val {
list.Append(protoreflect.ValueOfInt64(int64(x)))
}
})
case protoreflect.BoolKind:
val := flagSet.BoolSliceP(name, shorthand, nil, usage)
return listValue(func(list protoreflect.List) {
for _, x := range *val {
list.Append(protoreflect.ValueOfBool(x))
}
})
default:
return nil
}
}
type listValue func(protoreflect.List)
func (f listValue) AppendTo(list protoreflect.List) {
f(list)
}
type compositeListType struct {
simpleType Type
}
func (t compositeListType) NewValue(ctx context.Context, opts *Builder) pflag.Value {
return &compositeListValue{
simpleType: t.simpleType,
values: nil,
ctx: ctx,
opts: opts,
}
}
func (t compositeListType) DefaultValue() string {
return ""
}
type compositeListValue struct {
simpleType Type
values []protoreflect.Value
ctx context.Context
opts *Builder
}
func (c compositeListValue) AppendTo(list protoreflect.List) {
for _, value := range c.values {
list.Append(value)
}
}
func (c compositeListValue) String() string {
if len(c.values) == 0 {
return ""
}
return fmt.Sprintf("%+v", c.values)
}
func (c *compositeListValue) Set(val string) error {
simpleVal := c.simpleType.NewValue(c.ctx, c.opts)
err := simpleVal.Set(val)
if err != nil {
return err
}
c.values = append(c.values, simpleVal.(SimpleValue).Get())
return nil
}
func (c compositeListValue) Type() string {
return fmt.Sprintf("%s (repeated)", c.simpleType.NewValue(c.ctx, c.opts).Type())
}
+64
View File
@@ -0,0 +1,64 @@
package flag
import (
"context"
"fmt"
"github.com/spf13/pflag"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
)
type jsonMessageFlagType struct {
messageDesc protoreflect.MessageDescriptor
}
func (j jsonMessageFlagType) NewValue(_ context.Context, builder *Builder) pflag.Value {
return &jsonMessageFlagValue{
messageType: util.ResolveMessageType(builder.TypeResolver, j.messageDesc),
jsonMarshalOptions: protojson.MarshalOptions{Resolver: builder.TypeResolver},
jsonUnmarshalOptions: protojson.UnmarshalOptions{Resolver: builder.TypeResolver},
}
}
func (j jsonMessageFlagType) DefaultValue() string {
return ""
}
type jsonMessageFlagValue struct {
jsonMarshalOptions protojson.MarshalOptions
jsonUnmarshalOptions protojson.UnmarshalOptions
messageType protoreflect.MessageType
message proto.Message
}
func (j jsonMessageFlagValue) Get() protoreflect.Value {
if j.message == nil {
return protoreflect.Value{}
}
return protoreflect.ValueOfMessage(j.message.ProtoReflect())
}
func (j jsonMessageFlagValue) String() string {
if j.message == nil {
return ""
}
bz, err := j.jsonMarshalOptions.Marshal(j.message)
if err != nil {
return err.Error()
}
return string(bz)
}
func (j *jsonMessageFlagValue) Set(s string) error {
j.message = j.messageType.New().Interface()
return j.jsonUnmarshalOptions.Unmarshal([]byte(s), j.message)
}
func (j jsonMessageFlagValue) Type() string {
return fmt.Sprintf("%s (json)", j.messageType.Descriptor().FullName())
}
+20
View File
@@ -0,0 +1,20 @@
package flag
import (
"context"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
)
func (b *Builder) bindPageRequest(ctx context.Context, flagSet *pflag.FlagSet, field protoreflect.FieldDescriptor) FieldValueBinder {
handler := b.AddMessageFlags(
ctx,
flagSet,
util.ResolveMessageType(b.TypeResolver, field.Message()),
Options{Prefix: "page-"},
)
return simpleValueBinder{handler}
}
+59
View File
@@ -0,0 +1,59 @@
package flag
import (
"context"
"fmt"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
)
// AddMessageFlags adds flags for each field in the message to the flag set.
func (b *Builder) AddMessageFlags(ctx context.Context, set *pflag.FlagSet, messageType protoreflect.MessageType, options Options) *MessageBinder {
fields := messageType.Descriptor().Fields()
numFields := fields.Len()
handler := &MessageBinder{
messageType: messageType,
}
for i := 0; i < numFields; i++ {
field := fields.Get(i)
binder := b.AddFieldFlag(ctx, set, field, options)
if binder == nil {
fmt.Printf("unable to bind field %s to a flag, support will be added soon\n", field)
continue
}
handler.flagFieldPairs = append(handler.flagFieldPairs, struct {
binder FieldValueBinder
field protoreflect.FieldDescriptor
}{binder: binder, field: field})
}
return handler
}
// MessageBinder binds multiple flags in a flag set to a protobuf message.
type MessageBinder struct {
flagFieldPairs []struct {
binder FieldValueBinder
field protoreflect.FieldDescriptor
}
messageType protoreflect.MessageType
}
// BuildMessage builds and returns a new message for the bound flags.
func (m MessageBinder) BuildMessage() protoreflect.Message {
msg := m.messageType.New()
m.Bind(msg)
return msg
}
// Bind binds the flag values to an existing protobuf message.
func (m MessageBinder) Bind(msg protoreflect.Message) {
for _, pair := range m.flagFieldPairs {
pair.binder.Bind(msg, pair.field)
}
}
// Get calls BuildMessage and wraps the result in a protoreflect.Value.
func (m MessageBinder) Get() protoreflect.Value {
return protoreflect.ValueOfMessage(m.BuildMessage())
}
+54
View File
@@ -0,0 +1,54 @@
package flag
import (
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
)
func bindSimpleFlag(flagSet *pflag.FlagSet, kind protoreflect.Kind, name, shorthand, usage string) SimpleValue {
switch kind {
case protoreflect.BytesKind:
val := flagSet.BytesBase64P(name, shorthand, nil, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfBytes(*val)
})
case protoreflect.StringKind:
val := flagSet.StringP(name, shorthand, "", usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfString(*val)
})
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
val := flagSet.Uint32P(name, shorthand, 0, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfUint32(*val)
})
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
val := flagSet.Uint64P(name, shorthand, 0, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfUint64(*val)
})
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
val := flagSet.Int32P(name, shorthand, 0, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfInt32(*val)
})
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
val := flagSet.Int64P(name, shorthand, 0, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfInt64(*val)
})
case protoreflect.BoolKind:
val := flagSet.BoolP(name, shorthand, false, usage)
return simpleValue(func() protoreflect.Value {
return protoreflect.ValueOfBool(*val)
})
default:
return nil
}
}
type simpleValue func() protoreflect.Value
func (f simpleValue) Get() protoreflect.Value {
return f()
}
+51
View File
@@ -0,0 +1,51 @@
package flag
import (
"context"
"time"
"github.com/spf13/pflag"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
)
type timestampType struct{}
func (t timestampType) NewValue(context.Context, *Builder) pflag.Value {
return &timestampValue{}
}
func (t timestampType) DefaultValue() string {
return ""
}
type timestampValue struct {
value *timestamppb.Timestamp
}
func (t timestampValue) Get() protoreflect.Value {
if t.value == nil {
return protoreflect.Value{}
}
return protoreflect.ValueOfMessage(t.value.ProtoReflect())
}
func (v timestampValue) String() string {
if v.value == nil {
return ""
}
return v.value.AsTime().Format(time.RFC3339)
}
func (v *timestampValue) Set(s string) error {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return err
}
v.value = timestamppb.New(t)
return nil
}
func (v timestampValue) Type() string {
return "timestamp (RFC 3339)"
}
+18
View File
@@ -0,0 +1,18 @@
package flag
import (
"context"
"github.com/spf13/pflag"
)
// Type specifies a custom flag type.
type Type interface {
// NewValue returns a new pflag.Value which must also implement either
// SimpleValue or ListValue.
NewValue(context.Context, *Builder) pflag.Value
// DefaultValue is the default value for this type.
DefaultValue() string
}
+19
View File
@@ -0,0 +1,19 @@
package flag
import (
"google.golang.org/protobuf/reflect/protoreflect"
)
// SimpleValue wraps a simple (non-list and non-map) protobuf value.
type SimpleValue interface {
// Get returns the value.
Get() protoreflect.Value
}
// ListValue wraps a protobuf list/repeating value.
type ListValue interface {
// AppendTo appends the values to the provided list.
AppendTo(protoreflect.List)
}
+87
View File
@@ -0,0 +1,87 @@
package cli
import (
"fmt"
"github.com/iancoleman/strcase"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"github.com/cosmos/cosmos-sdk/client/v2/cli/flag"
"github.com/cosmos/cosmos-sdk/client/v2/internal/util"
)
// AddQueryServiceCommands adds a sub-command to the provided command for each
// method in the specified service and returns the command.
func (b *Builder) AddQueryServiceCommands(command *cobra.Command, serviceName protoreflect.FullName) *cobra.Command {
resolver := b.FileResolver
if resolver == nil {
resolver = protoregistry.GlobalFiles
}
descriptor, err := resolver.FindDescriptorByName(serviceName)
if err != nil {
panic(err)
}
service := descriptor.(protoreflect.ServiceDescriptor)
methods := service.Methods()
n := methods.Len()
for i := 0; i < n; i++ {
cmd := b.CreateQueryMethodCommand(methods.Get(i))
command.AddCommand(cmd)
}
return command
}
// CreateQueryMethodCommand creates a gRPC query command for the given service method.
func (b *Builder) CreateQueryMethodCommand(descriptor protoreflect.MethodDescriptor) *cobra.Command {
serviceDescriptor := descriptor.Parent().(protoreflect.ServiceDescriptor)
docs := util.DescriptorDocs(descriptor)
getClientConn := b.GetClientConn
methodName := fmt.Sprintf("/%s/%s", serviceDescriptor.FullName(), descriptor.Name())
inputDesc := descriptor.Input()
inputType := util.ResolveMessageType(b.TypeResolver, inputDesc)
outputType := util.ResolveMessageType(b.TypeResolver, descriptor.Output())
cmd := &cobra.Command{
Use: protoNameToCliName(descriptor.Name()),
Long: docs,
}
binder := b.AddMessageFlags(cmd.Context(), cmd.Flags(), inputType, flag.Options{})
jsonMarshalOptions := protojson.MarshalOptions{
Indent: " ",
UseProtoNames: true,
UseEnumNumbers: false,
EmitUnpopulated: true,
Resolver: b.TypeResolver,
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clientConn := getClientConn(ctx)
input := binder.BuildMessage()
output := outputType.New()
err := clientConn.Invoke(ctx, methodName, input.Interface(), output.Interface())
if err != nil {
return err
}
bz, err := jsonMarshalOptions.Marshal(output.Interface())
if err != nil {
return err
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(bz))
return err
}
return cmd
}
func protoNameToCliName(name protoreflect.Name) string {
return strcase.ToKebab(string(name))
}
+117
View File
@@ -0,0 +1,117 @@
package cli
import (
"bytes"
"context"
"net"
"testing"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
"github.com/cosmos/cosmos-sdk/client/v2/internal/testpb"
)
func testExec(t *testing.T, args ...string) *testClientConn {
server := grpc.NewServer()
testpb.RegisterQueryServer(server, &testEchoServer{})
listener, err := net.Listen("tcp", "127.0.0.1:0")
assert.NilError(t, err)
go server.Serve(listener)
defer server.GracefulStop()
clientConn, err := grpc.Dial(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.NilError(t, err)
defer clientConn.Close()
conn := &testClientConn{
ClientConn: clientConn,
t: t,
out: &bytes.Buffer{},
}
b := &Builder{
GetClientConn: func(ctx context.Context) grpc.ClientConnInterface {
return conn
},
}
cmd := b.AddQueryServiceCommands(&cobra.Command{Use: "test"}, protoreflect.FullName(testpb.Query_ServiceDesc.ServiceName))
cmd.SetArgs(args)
cmd.SetOut(conn.out)
assert.NilError(t, cmd.Execute())
return conn
}
func TestEcho(t *testing.T) {
conn := testExec(t,
"echo",
"--a-bool",
"--an-enum", "one",
"--a-message", `{"bar":"abc", "baz":-3}`,
"--duration", "4h3s",
"--u-32", "27",
"--u-64", "3267246890",
"--i-32", "-253",
"--i-64", "-234602347",
"--str", "def",
"--timestamp", "2019-01-02T00:01:02Z",
"--a-coin", `{"denom":"foo","amount":"100000"}`,
"--an-address", "cosmossdghdsfoi2134sdgh",
"--bz", "c2RncXdlZndkZ3NkZw==",
"--page-count-total",
"--page-key", "MTIzNTQ4N3NnaGRhcw==",
"--page-limit", "1000",
"--page-offset", "10",
"--page-reverse",
"--bools", "true",
"--bools", "false,false,true",
"--enums", "one",
"--enums", "five",
"--enums", "two",
"--strings", "abc",
"--strings", "xyz",
"--strings", "xyz,qrs",
"--durations", "3s",
"--durations", "5s",
"--durations", "10h",
"--some-messages", "{}",
"--some-messages", `{"bar":"baz"}`,
"--some-messages", `{"baz":-1}`,
"--uints", "1,2,3",
"--uints", "4",
)
assert.DeepEqual(t, conn.lastRequest, conn.lastResponse.(*testpb.EchoResponse).Request, protocmp.Transform())
}
func TestHelp(t *testing.T) {
conn := testExec(t, "echo", "-h")
golden.Assert(t, conn.out.String(), "help.golden")
}
type testClientConn struct {
*grpc.ClientConn
t *testing.T
lastRequest interface{}
lastResponse interface{}
out *bytes.Buffer
}
func (t *testClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
err := t.ClientConn.Invoke(ctx, method, args, reply, opts...)
t.lastRequest = args
t.lastResponse = reply
return err
}
type testEchoServer struct {
testpb.UnimplementedQueryServer
}
func (t testEchoServer) Echo(_ context.Context, request *testpb.EchoRequest) (*testpb.EchoResponse, error) {
return &testpb.EchoResponse{Request: request}, nil
}
var _ testpb.QueryServer = testEchoServer{}
+29
View File
@@ -0,0 +1,29 @@
Usage:
test echo [flags]
Flags:
--a-bool
--a-coin cosmos.base.v1beta1.Coin (json)
--a-message testpb.AMessage (json)
--an-address bech32 account address key name
--an-enum Enum (unspecified | one | two | five | neg-three) (default unspecified)
--bools bools (default [])
--bz bytesBase64
--duration duration
--durations duration (repeated)
--enums Enum (unspecified | one | two | five | neg-three) (repeated)
-h, --help help for echo
--i-32 int32
--i-64 int
--page-count-total
--page-key bytesBase64
--page-limit uint
--page-offset uint
--page-reverse
--some-messages testpb.AMessage (json) (repeated)
--str string
--strings strings
--timestamp timestamp (RFC 3339)
--u-32 uint32
--u-64 uint
--uints uints (default [])