Skip to content

Commit

Permalink
Osmocli: Fix lines of code overhead + speed of CLI tests (#3647)
Browse files Browse the repository at this point in the history
* Initial commit for cli test fixing

* Finish replacing tx tests

* Start query modification for testability

* Testing scaffolding now in place

* sync

* Fix cobra not resetting flag values

* Delete more cli tests

* Migrate incentives modules tests

* WIP on gamm deletion

* Finish converting all gamm tx tests

* Cleanup gamm index command

* pflag -> flag (alias, not import)

* Delete tests for 0-arg fns

* Convert simple query tests

* Finish adapting gamm tests

* Changelog

* Delete test suite that was never being ran
  • Loading branch information
ValarDragon authored Dec 12, 2022
1 parent 18eda0e commit 1986ad2
Show file tree
Hide file tree
Showing 18 changed files with 794 additions and 2,918 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Misc Improvements

* [#3611](https://github.com/osmosis-labs/osmosis/pull/3611) Introduce osmocli, to automate thousands of lines of CLI boilerplate
* [#3611](https://github.com/osmosis-labs/osmosis/pull/3611),[#3647](https://github.com/osmosis-labs/osmosis/pull/3647) Introduce osmocli, to automate thousands of lines of CLI boilerplate
* [#3634](https://github.com/osmosis-labs/osmosis/pull/3634) (Makefile) Ensure correct golang version in make build and make install. (Thank you @jhernandezb )

## v13.0.0
Expand Down
12 changes: 12 additions & 0 deletions osmoutils/cli_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/crypto/ed25519"
)

func DefaultFeeString(cfg network.Config) string {
Expand Down Expand Up @@ -72,3 +73,14 @@ func ParseSdkValAddressFromString(s string, separator string) []sdk.ValAddress {

return parsedAddr
}

// CreateRandomAccounts is a function return a list of randomly generated AccAddresses
func CreateRandomAccounts(numAccts int) []sdk.AccAddress {
testAddrs := make([]sdk.AccAddress, numAccts)
for i := 0; i < numAccts; i++ {
pk := ed25519.GenPrivKey().PubKey()
testAddrs[i] = sdk.AccAddress(pk.Address())
}

return testAddrs
}
111 changes: 111 additions & 0 deletions osmoutils/osmocli/cli_tester.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package osmocli

import (
"strings"
"testing"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/gogo/protobuf/proto"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
)

type TxCliTestCase[M sdk.Msg] struct {
Cmd string
ExpectedMsg M
ExpectedErr bool
OnlyCheckValidateBasic bool
}

type QueryCliTestCase[Q proto.Message] struct {
Cmd string
ExpectedQuery Q
ExpectedErr bool
}

func RunTxTestCases[M sdk.Msg](t *testing.T, desc *TxCliDesc, testcases map[string]TxCliTestCase[M]) {
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
RunTxTestCase(t, desc, &tc)
})
}
}

func RunQueryTestCases[Q proto.Message](t *testing.T, desc *QueryDescriptor, testcases map[string]QueryCliTestCase[Q]) {
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
RunQueryTestCase(t, desc, &tc)
})
}
}

func RunTxTestCase[M sdk.Msg](t *testing.T, desc *TxCliDesc, tc *TxCliTestCase[M]) {
cmd := BuildTxCli[M](desc)
err := resetCommandFlagValues(cmd)
require.NoError(t, err, "error in resetCommandFlagValues")
args := strings.Split(tc.Cmd, " ")
err = cmd.Flags().Parse(args)
require.NoError(t, err, "error in cmd.Flags().Parse(args)")
clientCtx := newClientContextWithFrom(t, cmd.Flags())

msg, err := desc.ParseAndBuildMsg(clientCtx, args, cmd.Flags())
if tc.ExpectedErr {
require.Error(t, err)
return
}
require.NoError(t, err, "error in desc.ParseAndBuildMsg")
if tc.OnlyCheckValidateBasic {
require.NoError(t, msg.ValidateBasic())
return
}

require.Equal(t, tc.ExpectedMsg, msg)
}

func RunQueryTestCase[Q proto.Message](t *testing.T, desc *QueryDescriptor, tc *QueryCliTestCase[Q]) {
cmd := BuildQueryCli[Q, int](desc, nil)
err := resetCommandFlagValues(cmd)
require.NoError(t, err, "error in resetCommandFlagValues")
args := strings.Split(tc.Cmd, " ")
err = cmd.Flags().Parse(args)
require.NoError(t, err, "error in cmd.Flags().Parse(args)")

req, err := desc.ParseQuery(args, cmd.Flags())
if tc.ExpectedErr {
require.Error(t, err)
return
}
require.NoError(t, err, "error in desc.ParseQuery")
require.Equal(t, tc.ExpectedQuery, req)
}

// This logic is copied from the SDK, it should've just been publicly exposed.
// But instead its buried within a mega-method.
func newClientContextWithFrom(t *testing.T, fs *pflag.FlagSet) client.Context {
clientCtx := client.Context{}
from, _ := fs.GetString(flags.FlagFrom)
fromAddr, fromName, _, err := client.GetFromFields(nil, from, true)
require.NoError(t, err)

clientCtx = clientCtx.WithFrom(from).WithFromAddress(fromAddr).WithFromName(fromName)
return clientCtx
}

// taken from https://github.com/golang/debug/pull/8,
// due to no cobra command for resetting flag value
func resetCommandFlagValues(cmd *cobra.Command) error {
var retErr error = nil
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if f.Changed {
err := f.Value.Set(f.DefValue)
if err != nil {
retErr = err
}
f.Changed = false
}
})
return retErr
}
97 changes: 69 additions & 28 deletions osmoutils/osmocli/query_cmd_wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import (
grpc1 "github.com/gogo/protobuf/grpc"
"github.com/gogo/protobuf/proto"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func QueryIndexCmd(moduleName string) *cobra.Command {
cmd := IndexCmd(moduleName)
cmd.Short = fmt.Sprintf("Querying commands for the %s module", moduleName)
return cmd
}
// global variable set on index command.
// helps populate Longs, when not set in QueryDescriptor.
var lastQueryModuleName string

type QueryDescriptor struct {
Use string
Expand All @@ -33,25 +32,70 @@ type QueryDescriptor struct {
CustomFlagOverrides map[string]string
// Map of FieldName -> CustomParseFn
CustomFieldParsers map[string]CustomFieldParserFn

ParseQuery func(args []string, flags *pflag.FlagSet) (proto.Message, error)

ModuleName string
numArgs int
}

func SimpleQueryFromDescriptor[reqP proto.Message, querier any](desc QueryDescriptor, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
numArgs := ParseNumFields[reqP]() - len(desc.CustomFlagOverrides)
func QueryIndexCmd(moduleName string) *cobra.Command {
cmd := IndexCmd(moduleName)
cmd.Short = fmt.Sprintf("Querying commands for the %s module", moduleName)
lastQueryModuleName = moduleName
return cmd
}

func AddQueryCmd[Q proto.Message, querier any](cmd *cobra.Command, newQueryClientFn func(grpc1.ClientConn) querier, f func() (*QueryDescriptor, Q)) {
desc, _ := f()
prepareDescriptor[Q](desc)
subCmd := BuildQueryCli[Q](desc, newQueryClientFn)
cmd.AddCommand(subCmd)
}

func (desc *QueryDescriptor) FormatLong(moduleName string) {
desc.Long = FormatLongDesc(desc.Long, NewLongMetadata(moduleName).WithShort(desc.Short))
}

func prepareDescriptor[reqP proto.Message](desc *QueryDescriptor) {
if !desc.HasPagination {
desc.HasPagination = ParseHasPagination[reqP]()
}
if desc.QueryFnName == "" {
desc.QueryFnName = ParseExpectedQueryFnName[reqP]()
}
if strings.Contains(desc.Long, "{") {
if desc.ModuleName == "" {
desc.ModuleName = lastQueryModuleName
}
desc.FormatLong(desc.ModuleName)
}

desc.numArgs = ParseNumFields[reqP]() - len(desc.CustomFlagOverrides)
if desc.HasPagination {
numArgs = numArgs - 1
desc.numArgs = desc.numArgs - 1
}
}

func BuildQueryCli[reqP proto.Message, querier any](desc *QueryDescriptor, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
prepareDescriptor[reqP](desc)
if desc.ParseQuery == nil {
desc.ParseQuery = func(args []string, fs *pflag.FlagSet) (proto.Message, error) {
flagAdvice := FlagAdvice{
HasPagination: desc.HasPagination,
CustomFlagOverrides: desc.CustomFlagOverrides,
CustomFieldParsers: desc.CustomFieldParsers,
}.Sanitize()
return ParseFieldsFromFlagsAndArgs[reqP](flagAdvice, fs, args)
}
}
flagAdvice := FlagAdvice{
HasPagination: desc.HasPagination,
CustomFlagOverrides: desc.CustomFlagOverrides,
CustomFieldParsers: desc.CustomFieldParsers,
}.Sanitize()

cmd := &cobra.Command{
Use: desc.Use,
Short: desc.Short,
Long: desc.Long,
Args: cobra.ExactArgs(numArgs),
RunE: NewQueryLogicAllFieldsAsArgs[reqP](
flagAdvice, desc.QueryFnName, newQueryClientFn),
Args: cobra.ExactArgs(desc.numArgs),
RunE: queryLogic(desc, newQueryClientFn),
}
flags.AddQueryFlagsToCmd(cmd)
AddFlags(cmd, desc.Flags)
Expand All @@ -70,25 +114,23 @@ func SimpleQueryFromDescriptor[reqP proto.Message, querier any](desc QueryDescri
func SimpleQueryCmd[reqP proto.Message, querier any](use string, short string, long string,
moduleName string, newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
desc := QueryDescriptor{
Use: use,
Short: short,
Long: FormatLongDesc(long, NewLongMetadata(moduleName).WithShort(short)),
HasPagination: ParseHasPagination[reqP](),
QueryFnName: ParseExpectedQueryFnName[reqP](),
Use: use,
Short: short,
Long: FormatLongDesc(long, NewLongMetadata(moduleName).WithShort(short)),
}
return SimpleQueryFromDescriptor[reqP](desc, newQueryClientFn)
return BuildQueryCli[reqP](&desc, newQueryClientFn)
}

func GetParams[reqP proto.Message, querier any](moduleName string,
newQueryClientFn func(grpc1.ClientConn) querier) *cobra.Command {
return SimpleQueryFromDescriptor[reqP](QueryDescriptor{
return BuildQueryCli[reqP](&QueryDescriptor{
Use: "params [flags]",
Short: fmt.Sprintf("Get the params for the x/%s module", moduleName),
QueryFnName: "Params",
}, newQueryClientFn)
}

func callQueryClientFn[reqP proto.Message, querier any](ctx context.Context, fnName string, req reqP, q querier) (res proto.Message, err error) {
func callQueryClientFn(ctx context.Context, fnName string, req proto.Message, q any) (res proto.Message, err error) {
qVal := reflect.ValueOf(q)
method := qVal.MethodByName(fnName)
args := []reflect.Value{
Expand All @@ -109,22 +151,21 @@ func callQueryClientFn[reqP proto.Message, querier any](ctx context.Context, fnN
return res, nil
}

func NewQueryLogicAllFieldsAsArgs[reqP proto.Message, querier any](flagAdvice FlagAdvice, keeperFnName string,
func queryLogic[querier any](desc *QueryDescriptor,
newQueryClientFn func(grpc1.ClientConn) querier) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := newQueryClientFn(clientCtx)
var req reqP

req, err = ParseFieldsFromFlagsAndArgs[reqP](flagAdvice, cmd.Flags(), args)
req, err := desc.ParseQuery(args, cmd.Flags())
if err != nil {
return err
}

res, err := callQueryClientFn(cmd.Context(), keeperFnName, req, queryClient)
res, err := callQueryClientFn(cmd.Context(), desc.QueryFnName, req, queryClient)
if err != nil {
return err
}
Expand Down
26 changes: 17 additions & 9 deletions osmoutils/osmocli/tx_cmd_wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,29 @@ type TxCliDesc struct {
CustomFieldParsers map[string]CustomFieldParserFn
}

func AddTxCmd[M sdk.Msg](cmd *cobra.Command, f func() (*TxCliDesc, M)) {
desc, _ := f()
subCmd := BuildTxCli[M](desc)
cmd.AddCommand(subCmd)
}

func BuildTxCli[M sdk.Msg](desc *TxCliDesc) *cobra.Command {
desc.TxSignerFieldName = strings.ToLower(desc.TxSignerFieldName)
if desc.NumArgs == 0 {
// NumArgs = NumFields - 1, since 1 field is from the msg
desc.NumArgs = ParseNumFields[M]() - 1 - len(desc.CustomFlagOverrides) - len(desc.CustomFieldParsers)
}
desc.ParseAndBuildMsg = func(clientCtx client.Context, args []string, flags *pflag.FlagSet) (sdk.Msg, error) {
flagAdvice := FlagAdvice{
IsTx: true,
TxSenderFieldName: desc.TxSignerFieldName,
FromValue: clientCtx.GetFromAddress().String(),
CustomFlagOverrides: desc.CustomFlagOverrides,
CustomFieldParsers: desc.CustomFieldParsers,
}.Sanitize()
return ParseFieldsFromFlagsAndArgs[M](flagAdvice, flags, args)
if desc.ParseAndBuildMsg == nil {
desc.ParseAndBuildMsg = func(clientCtx client.Context, args []string, flags *pflag.FlagSet) (sdk.Msg, error) {
flagAdvice := FlagAdvice{
IsTx: true,
TxSenderFieldName: desc.TxSignerFieldName,
FromValue: clientCtx.GetFromAddress().String(),
CustomFlagOverrides: desc.CustomFlagOverrides,
CustomFieldParsers: desc.CustomFieldParsers,
}.Sanitize()
return ParseFieldsFromFlagsAndArgs[M](flagAdvice, flags, args)
}
}
return desc.BuildCommandCustomFn()
}
Expand Down
Loading

0 comments on commit 1986ad2

Please sign in to comment.