github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/commands/commands_test.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"flag"
     7  	"fmt"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/peterbourgon/ff/v3/fftest"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  type configDelegate func(*flag.FlagSet)
    17  
    18  type mockConfig struct {
    19  	configFn configDelegate
    20  }
    21  
    22  func (c *mockConfig) RegisterFlags(fs *flag.FlagSet) {
    23  	if c.configFn != nil {
    24  		c.configFn(fs)
    25  	}
    26  }
    27  
    28  func TestCommandParseAndRun(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	type flags struct {
    32  		b bool
    33  		s string
    34  		x bool
    35  	}
    36  	tests := []struct {
    37  		name          string
    38  		osArgs        []string
    39  		expectedCmd   string
    40  		expectedArgs  []string
    41  		expectedFlags flags
    42  		expectedError string
    43  	}{
    44  		{
    45  			name:          "no args no flags",
    46  			expectedCmd:   "main",
    47  			osArgs:        []string{},
    48  			expectedArgs:  []string{},
    49  			expectedFlags: flags{},
    50  		},
    51  		{
    52  			name:          "only args",
    53  			expectedCmd:   "main",
    54  			osArgs:        []string{"bar", "baz"},
    55  			expectedArgs:  []string{"bar", "baz"},
    56  			expectedFlags: flags{},
    57  		},
    58  		{
    59  			name:          "only flags",
    60  			expectedCmd:   "main",
    61  			osArgs:        []string{"-b", "-s", "str"},
    62  			expectedArgs:  []string{},
    63  			expectedFlags: flags{b: true, s: "str"},
    64  		},
    65  		{
    66  			name:          "ignore all flags",
    67  			expectedCmd:   "main",
    68  			osArgs:        []string{"--", "-b", "-s", "str", "bar"},
    69  			expectedArgs:  []string{"-b", "-s", "str", "bar"},
    70  			expectedFlags: flags{},
    71  		},
    72  		{
    73  			name:          "ignore some flags",
    74  			expectedCmd:   "main",
    75  			osArgs:        []string{"-b", "--", "-s", "--", "str", "bar"},
    76  			expectedArgs:  []string{"-s", "--", "str", "bar"},
    77  			expectedFlags: flags{b: true},
    78  		},
    79  		{
    80  			name:          "unknow flag",
    81  			expectedCmd:   "main",
    82  			osArgs:        []string{"-y", "-s", "str"},
    83  			expectedArgs:  []string{},
    84  			expectedError: "error parsing commandline arguments: flag provided but not defined: -y",
    85  		},
    86  		{
    87  			name:          "flags before args",
    88  			expectedCmd:   "main",
    89  			osArgs:        []string{"-b", "-s", "str", "bar", "baz"},
    90  			expectedArgs:  []string{"bar", "baz"},
    91  			expectedFlags: flags{b: true, s: "str"},
    92  		},
    93  		{
    94  			name:          "flags after args",
    95  			expectedCmd:   "main",
    96  			osArgs:        []string{"bar", "baz", "-b", "-s", "str"},
    97  			expectedArgs:  []string{"bar", "baz"},
    98  			expectedFlags: flags{b: true, s: "str"},
    99  		},
   100  		{
   101  			name:          "flags around args",
   102  			expectedCmd:   "main",
   103  			osArgs:        []string{"-b", "bar", "baz", "-s", "str"},
   104  			expectedArgs:  []string{"bar", "baz"},
   105  			expectedFlags: flags{b: true, s: "str"},
   106  		},
   107  		{
   108  			name:          "flags between args",
   109  			expectedCmd:   "main",
   110  			osArgs:        []string{"bar", "-b", "-s", "str", "baz"},
   111  			expectedArgs:  []string{"bar", "baz"},
   112  			expectedFlags: flags{b: true, s: "str"},
   113  		},
   114  		{
   115  			name:          "ignore ending --",
   116  			expectedCmd:   "main",
   117  			osArgs:        []string{"bar", "-b", "-s", "str", "--"},
   118  			expectedArgs:  []string{"bar"},
   119  			expectedFlags: flags{b: true, s: "str"},
   120  		},
   121  		{
   122  			name:          "args and some ignored flags",
   123  			expectedCmd:   "main",
   124  			osArgs:        []string{"bar", "-b", "--", "-s", "--", "str", "baz"},
   125  			expectedArgs:  []string{"bar", "-s", "--", "str", "baz"},
   126  			expectedFlags: flags{b: true},
   127  		},
   128  		{
   129  			name:          "subcommand no flags no args",
   130  			expectedCmd:   "sub",
   131  			osArgs:        []string{"sub"},
   132  			expectedArgs:  []string{},
   133  			expectedFlags: flags{},
   134  		},
   135  		{
   136  			name:          "subcommand only args",
   137  			expectedCmd:   "sub",
   138  			osArgs:        []string{"sub", "bar", "baz"},
   139  			expectedArgs:  []string{"bar", "baz"},
   140  			expectedFlags: flags{},
   141  		},
   142  		{
   143  			name:          "subcommand flag before subcommand",
   144  			expectedCmd:   "sub",
   145  			osArgs:        []string{"-x", "sub"},
   146  			expectedError: "error parsing commandline arguments: flag provided but not defined: -x",
   147  		},
   148  		{
   149  			name:          "subcommand only flags",
   150  			expectedCmd:   "sub",
   151  			osArgs:        []string{"-b", "sub", "-x", "-s", "str"},
   152  			expectedArgs:  []string{},
   153  			expectedFlags: flags{b: true, s: "str", x: true},
   154  		},
   155  		{
   156  			name:          "subcommand ignore all flags after --",
   157  			expectedCmd:   "sub",
   158  			osArgs:        []string{"-b", "sub", "--", "-x", "-s", "str"},
   159  			expectedArgs:  []string{"-x", "-s", "str"},
   160  			expectedFlags: flags{b: true},
   161  		},
   162  		{
   163  			name:          "subcommand ignore some flags after --",
   164  			expectedCmd:   "sub",
   165  			osArgs:        []string{"-b", "sub", "-x", "--", "-s", "str"},
   166  			expectedArgs:  []string{"-s", "str"},
   167  			expectedFlags: flags{b: true, x: true},
   168  		},
   169  		{
   170  			name:          "subcommand ignored by --",
   171  			expectedCmd:   "main",
   172  			osArgs:        []string{"-b", "--", "sub", "-x", "-s", "str"},
   173  			expectedArgs:  []string{"sub", "-x", "-s", "str"},
   174  			expectedFlags: flags{b: true},
   175  		},
   176  		{
   177  			name:          "subcommand ignored by preceding arg",
   178  			expectedCmd:   "main",
   179  			osArgs:        []string{"-b", "bar", "sub", "-s", "str"},
   180  			expectedArgs:  []string{"bar", "sub"},
   181  			expectedFlags: flags{b: true, s: "str"},
   182  		},
   183  		{
   184  			name:          "subcommand flags before args",
   185  			expectedCmd:   "sub",
   186  			osArgs:        []string{"-b", "sub", "-x", "-s", "str", "bar", "baz"},
   187  			expectedArgs:  []string{"bar", "baz"},
   188  			expectedFlags: flags{b: true, s: "str", x: true},
   189  		},
   190  		{
   191  			name:          "subcommand flags after args",
   192  			expectedCmd:   "sub",
   193  			osArgs:        []string{"-b", "sub", "bar", "baz", "-x", "-s", "str"},
   194  			expectedArgs:  []string{"bar", "baz"},
   195  			expectedFlags: flags{b: true, s: "str", x: true},
   196  		},
   197  		{
   198  			name:          "subcommand flags around args",
   199  			expectedCmd:   "sub",
   200  			osArgs:        []string{"-b", "sub", "-x", "bar", "baz", "-s", "str"},
   201  			expectedArgs:  []string{"bar", "baz"},
   202  			expectedFlags: flags{b: true, s: "str", x: true},
   203  		},
   204  		{
   205  			name:          "subcommand flags between args",
   206  			expectedCmd:   "sub",
   207  			osArgs:        []string{"-b", "sub", "bar", "-x", "baz", "-s", "str"},
   208  			expectedArgs:  []string{"bar", "baz"},
   209  			expectedFlags: flags{b: true, s: "str", x: true},
   210  		},
   211  		{
   212  			name:          "subsubcommand with parent flags",
   213  			expectedCmd:   "subsub",
   214  			osArgs:        []string{"-b", "sub", "-x", "subsub", "bar"},
   215  			expectedArgs:  []string{"bar"},
   216  			expectedFlags: flags{b: true, x: true},
   217  		},
   218  	}
   219  	for _, tt := range tests {
   220  		tt := tt
   221  		t.Run(tt.name, func(t *testing.T) {
   222  			t.Parallel()
   223  
   224  			var (
   225  				invokedCmd string
   226  				args       []string
   227  				flags      flags
   228  			)
   229  			// Create a cmd main that takes 2 flags -b and -s
   230  			cmd := NewCommand(
   231  				Metadata{Name: "main"},
   232  				&mockConfig{
   233  					configFn: func(fs *flag.FlagSet) {
   234  						fs.BoolVar(&flags.b, "b", false, "a boolan")
   235  						fs.StringVar(&flags.s, "s", "", "a string")
   236  					},
   237  				},
   238  				func(_ context.Context, a []string) error {
   239  					invokedCmd = "main"
   240  					args = a
   241  					return nil
   242  				},
   243  			)
   244  			// Add a sub command to cmd with a single flag -x
   245  			subcmd := NewCommand(
   246  				Metadata{Name: "sub"},
   247  				&mockConfig{
   248  					configFn: func(fs *flag.FlagSet) {
   249  						fs.BoolVar(&flags.x, "x", false, "a boolan")
   250  					},
   251  				},
   252  				func(_ context.Context, a []string) error {
   253  					invokedCmd = "sub"
   254  					args = a
   255  					return nil
   256  				},
   257  			)
   258  			cmd.AddSubCommands(subcmd)
   259  			// Add a sub command to sub cmd
   260  			subcmd.AddSubCommands(
   261  				NewCommand(
   262  					Metadata{Name: "subsub"},
   263  					&mockConfig{
   264  						configFn: func(fs *flag.FlagSet) {},
   265  					},
   266  					func(_ context.Context, a []string) error {
   267  						invokedCmd = "subsub"
   268  						args = a
   269  						return nil
   270  					},
   271  				),
   272  			)
   273  
   274  			err := cmd.ParseAndRun(context.Background(), tt.osArgs)
   275  
   276  			if tt.expectedError != "" {
   277  				require.EqualError(t, err, tt.expectedError)
   278  				return
   279  			}
   280  			require.NoError(t, err)
   281  			require.Equal(t, tt.expectedCmd, invokedCmd, "wrong cmd")
   282  			require.Equal(t, tt.expectedArgs, args, "wrong args")
   283  			require.Equal(t, tt.expectedFlags, flags, "wrong flags")
   284  		})
   285  	}
   286  }
   287  
   288  func TestCommand_AddSubCommands(t *testing.T) {
   289  	t.Parallel()
   290  
   291  	// Test setup //
   292  
   293  	type testCmd struct {
   294  		cmd     *Command
   295  		subCmds []*testCmd
   296  	}
   297  
   298  	getSubcommands := func(t *testCmd) []*Command {
   299  		res := make([]*Command, len(t.subCmds))
   300  
   301  		for i, subCmd := range t.subCmds {
   302  			res[i] = subCmd.cmd
   303  		}
   304  
   305  		return res
   306  	}
   307  
   308  	generateTestCmd := func(name string) *Command {
   309  		return NewCommand(
   310  			Metadata{
   311  				Name: name,
   312  			},
   313  			&mockConfig{
   314  				func(fs *flag.FlagSet) {
   315  					fs.String(
   316  						name,
   317  						"",
   318  						"",
   319  					)
   320  				},
   321  			},
   322  			HelpExec,
   323  		)
   324  	}
   325  
   326  	var postorderCommands func(root *testCmd) []*testCmd
   327  
   328  	postorderCommands = func(root *testCmd) []*testCmd {
   329  		if root == nil {
   330  			return nil
   331  		}
   332  
   333  		res := make([]*testCmd, 0)
   334  
   335  		for _, child := range root.subCmds {
   336  			res = append(res, postorderCommands(child)...)
   337  		}
   338  
   339  		return append(res, root)
   340  	}
   341  
   342  	// Cases //
   343  
   344  	testTable := []struct {
   345  		name   string
   346  		topCmd *testCmd
   347  	}{
   348  		{
   349  			name: "no subcommands",
   350  			topCmd: &testCmd{
   351  				cmd:     generateTestCmd("level0"),
   352  				subCmds: nil,
   353  			},
   354  		},
   355  		{
   356  			name: "single subcommand level",
   357  			topCmd: &testCmd{
   358  				cmd: generateTestCmd("level0"),
   359  				subCmds: []*testCmd{
   360  					{
   361  						cmd:     generateTestCmd("level1"),
   362  						subCmds: nil,
   363  					},
   364  				},
   365  			},
   366  		},
   367  		{
   368  			name: "multiple subcommand levels",
   369  			topCmd: &testCmd{
   370  				cmd: generateTestCmd("level0"),
   371  				subCmds: []*testCmd{
   372  					{
   373  						cmd: generateTestCmd("level1"),
   374  						subCmds: []*testCmd{
   375  							{
   376  								cmd:     generateTestCmd("level2"),
   377  								subCmds: nil,
   378  							},
   379  						},
   380  					},
   381  				},
   382  			},
   383  		},
   384  	}
   385  
   386  	for _, testCase := range testTable {
   387  		testCase := testCase
   388  
   389  		t.Run(testCase.name, func(t *testing.T) {
   390  			t.Parallel()
   391  
   392  			var validateSubcommandTree func(flag string, root *Command)
   393  
   394  			validateSubcommandTree = func(flag string, root *Command) {
   395  				assert.NotNil(t, root.flagSet.Lookup(flag))
   396  
   397  				for _, subcommand := range root.subcommands {
   398  					validateSubcommandTree(flag, subcommand)
   399  				}
   400  			}
   401  
   402  			// Register the subcommands in LIFO order (postorder), starting from the
   403  			// leaf of the command tree (mimics how the commands package is used)
   404  			commandOrder := postorderCommands(testCase.topCmd)
   405  
   406  			for _, currCmd := range commandOrder {
   407  				// For the current command, register its subcommand tree
   408  				currCmd.cmd.AddSubCommands(getSubcommands(currCmd)...)
   409  
   410  				// Validate that the entire subcommand tree has root command flags
   411  				for _, subCmd := range currCmd.cmd.subcommands {
   412  					// For each root command flag, validate
   413  					currCmd.cmd.flagSet.VisitAll(func(f *flag.Flag) {
   414  						validateSubcommandTree(f.Name, subCmd)
   415  					})
   416  				}
   417  			}
   418  		})
   419  	}
   420  }
   421  
   422  // Forked from peterbourgon/ff/ffcli
   423  func TestHelpUsage(t *testing.T) {
   424  	t.Parallel()
   425  
   426  	tests := []struct {
   427  		name           string
   428  		command        *Command
   429  		expectedOutput string
   430  	}{
   431  		{
   432  			name: "normal case",
   433  			command: &Command{
   434  				name:       "TestHelpUsage",
   435  				shortUsage: "TestHelpUsage [flags] <args>",
   436  				shortHelp:  "some short help",
   437  				longHelp:   "Some long help.",
   438  			},
   439  			expectedOutput: strings.TrimSpace(`
   440  USAGE
   441    TestHelpUsage [flags] <args>
   442  
   443  Some long help.
   444  
   445  FLAGS
   446    -b=false  bool
   447    -d 0s     time.Duration
   448    -f 0      float64
   449    -i 0      int
   450    -s ...    string
   451    -x ...    collection of strings (repeatable)
   452  `) + "\n\n",
   453  		},
   454  		{
   455  			name: "no long help",
   456  			command: &Command{
   457  				name:       "TestHelpUsage",
   458  				shortUsage: "TestHelpUsage [flags] <args>",
   459  				shortHelp:  "some short help",
   460  			},
   461  			expectedOutput: strings.TrimSpace(`
   462  USAGE
   463    TestHelpUsage [flags] <args>
   464  
   465  some short help.
   466  
   467  FLAGS
   468    -b=false  bool
   469    -d 0s     time.Duration
   470    -f 0      float64
   471    -i 0      int
   472    -s ...    string
   473    -x ...    collection of strings (repeatable)
   474  `) + "\n\n",
   475  		},
   476  		{
   477  			name: "no short and no long help",
   478  			command: &Command{
   479  				name:       "TestHelpUsage",
   480  				shortUsage: "TestHelpUsage [flags] <args>",
   481  			},
   482  			expectedOutput: strings.TrimSpace(`
   483  USAGE
   484    TestHelpUsage [flags] <args>
   485  
   486  FLAGS
   487    -b=false  bool
   488    -d 0s     time.Duration
   489    -f 0      float64
   490    -i 0      int
   491    -s ...    string
   492    -x ...    collection of strings (repeatable)
   493  `) + "\n\n",
   494  		},
   495  	}
   496  	for _, tt := range tests {
   497  		tt := tt
   498  		t.Run(tt.name, func(t *testing.T) {
   499  			t.Parallel()
   500  
   501  			fs, _ := fftest.Pair()
   502  			var buf bytes.Buffer
   503  			fs.SetOutput(&buf)
   504  
   505  			tt.command.flagSet = fs
   506  
   507  			err := tt.command.ParseAndRun(context.Background(), []string{"-h"})
   508  
   509  			assert.ErrorIs(t, err, flag.ErrHelp)
   510  			assert.Equal(t, tt.expectedOutput, buf.String())
   511  		})
   512  	}
   513  }
   514  
   515  // Forked from peterbourgon/ff/ffcli
   516  func TestNestedOutput(t *testing.T) {
   517  	t.Parallel()
   518  
   519  	var (
   520  		rootHelpOutput = "USAGE\n  \n\nSUBCOMMANDS\n  foo\n\n"
   521  		fooHelpOutput  = "USAGE\n  foo\n\nSUBCOMMANDS\n  bar\n\n"
   522  		barHelpOutout  = "USAGE\n  bar\n\n"
   523  	)
   524  	for _, testcase := range []struct {
   525  		name       string
   526  		args       []string
   527  		wantErr    error
   528  		wantOutput string
   529  	}{
   530  		{
   531  			name:       "root without args",
   532  			args:       []string{},
   533  			wantErr:    flag.ErrHelp,
   534  			wantOutput: rootHelpOutput,
   535  		},
   536  		{
   537  			name:       "root with args",
   538  			args:       []string{"abc", "def ghi"},
   539  			wantErr:    flag.ErrHelp,
   540  			wantOutput: rootHelpOutput,
   541  		},
   542  		{
   543  			name:       "root help",
   544  			args:       []string{"-h"},
   545  			wantErr:    flag.ErrHelp,
   546  			wantOutput: rootHelpOutput,
   547  		},
   548  		{
   549  			name:       "foo without args",
   550  			args:       []string{"foo"},
   551  			wantOutput: "foo: ''\n",
   552  		},
   553  		{
   554  			name:       "foo with args",
   555  			args:       []string{"foo", "alpha", "beta"},
   556  			wantOutput: "foo: 'alpha beta'\n",
   557  		},
   558  		{
   559  			name:       "foo help",
   560  			args:       []string{"foo", "-h"},
   561  			wantErr:    flag.ErrHelp,
   562  			wantOutput: fooHelpOutput, // only one instance of usage string
   563  		},
   564  		{
   565  			name:       "foo bar without args",
   566  			args:       []string{"foo", "bar"},
   567  			wantErr:    flag.ErrHelp,
   568  			wantOutput: barHelpOutout,
   569  		},
   570  		{
   571  			name:       "foo bar with args",
   572  			args:       []string{"foo", "bar", "--", "baz quux"},
   573  			wantErr:    flag.ErrHelp,
   574  			wantOutput: barHelpOutout,
   575  		},
   576  		{
   577  			name:       "foo bar help",
   578  			args:       []string{"foo", "bar", "--help"},
   579  			wantErr:    flag.ErrHelp,
   580  			wantOutput: barHelpOutout,
   581  		},
   582  	} {
   583  		t.Run(testcase.name, func(t *testing.T) {
   584  			t.Parallel()
   585  
   586  			var (
   587  				rootfs = flag.NewFlagSet("root", flag.ContinueOnError)
   588  				foofs  = flag.NewFlagSet("foo", flag.ContinueOnError)
   589  				barfs  = flag.NewFlagSet("bar", flag.ContinueOnError)
   590  				buf    bytes.Buffer
   591  			)
   592  			rootfs.SetOutput(&buf)
   593  			foofs.SetOutput(&buf)
   594  			barfs.SetOutput(&buf)
   595  
   596  			barExec := func(_ context.Context, args []string) error {
   597  				return flag.ErrHelp
   598  			}
   599  
   600  			bar := &Command{
   601  				name:    "bar",
   602  				flagSet: barfs,
   603  				exec:    barExec,
   604  			}
   605  
   606  			fooExec := func(_ context.Context, args []string) error {
   607  				fmt.Fprintf(&buf, "foo: '%s'\n", strings.Join(args, " "))
   608  				return nil
   609  			}
   610  
   611  			foo := &Command{
   612  				name:        "foo",
   613  				flagSet:     foofs,
   614  				subcommands: []*Command{bar},
   615  				exec:        fooExec,
   616  			}
   617  
   618  			rootExec := func(_ context.Context, args []string) error {
   619  				return flag.ErrHelp
   620  			}
   621  
   622  			root := &Command{
   623  				flagSet:     rootfs,
   624  				subcommands: []*Command{foo},
   625  				exec:        rootExec,
   626  			}
   627  
   628  			err := root.ParseAndRun(context.Background(), testcase.args)
   629  
   630  			assert.ErrorIs(t, err, testcase.wantErr)
   631  			assert.Equal(t, testcase.wantOutput, buf.String())
   632  		})
   633  	}
   634  }