...

Source file src/github.com/peterbourgon/ff/v3/ffcli/command_test.go

Documentation: github.com/peterbourgon/ff/v3/ffcli

     1  package ffcli_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"log"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/peterbourgon/ff/v3/ffcli"
    17  	"github.com/peterbourgon/ff/v3/fftest"
    18  )
    19  
    20  func TestCommandRun(t *testing.T) {
    21  	t.Parallel()
    22  
    23  	for _, testcase := range []struct {
    24  		name     string
    25  		args     []string
    26  		rootvars fftest.Vars
    27  		rootran  bool
    28  		rootargs []string
    29  		foovars  fftest.Vars
    30  		fooran   bool
    31  		fooargs  []string
    32  		barvars  fftest.Vars
    33  		barran   bool
    34  		barargs  []string
    35  	}{
    36  		{
    37  			name:    "root",
    38  			rootran: true,
    39  		},
    40  		{
    41  			name:     "root flags",
    42  			args:     []string{"-s", "123", "-b"},
    43  			rootvars: fftest.Vars{S: "123", B: true},
    44  			rootran:  true,
    45  		},
    46  		{
    47  			name:     "root args",
    48  			args:     []string{"hello"},
    49  			rootran:  true,
    50  			rootargs: []string{"hello"},
    51  		},
    52  		{
    53  			name:     "root flags args",
    54  			args:     []string{"-i=123", "hello world"},
    55  			rootvars: fftest.Vars{I: 123},
    56  			rootran:  true,
    57  			rootargs: []string{"hello world"},
    58  		},
    59  		{
    60  			name:     "root flags -- args",
    61  			args:     []string{"-f", "1.23", "--", "hello", "world"},
    62  			rootvars: fftest.Vars{F: 1.23},
    63  			rootran:  true,
    64  			rootargs: []string{"hello", "world"},
    65  		},
    66  		{
    67  			name:   "root foo",
    68  			args:   []string{"foo"},
    69  			fooran: true,
    70  		},
    71  		{
    72  			name:     "root flags foo",
    73  			args:     []string{"-s", "OK", "-d", "10m", "foo"},
    74  			rootvars: fftest.Vars{S: "OK", D: 10 * time.Minute},
    75  			fooran:   true,
    76  		},
    77  		{
    78  			name:     "root flags foo flags",
    79  			args:     []string{"-s", "OK", "-d", "10m", "foo", "-s", "Yup"},
    80  			rootvars: fftest.Vars{S: "OK", D: 10 * time.Minute},
    81  			foovars:  fftest.Vars{S: "Yup"},
    82  			fooran:   true,
    83  		},
    84  		{
    85  			name:     "root flags foo flags args",
    86  			args:     []string{"-f=0.99", "foo", "-f", "1.01", "verb", "noun", "adjective adjective"},
    87  			rootvars: fftest.Vars{F: 0.99},
    88  			foovars:  fftest.Vars{F: 1.01},
    89  			fooran:   true,
    90  			fooargs:  []string{"verb", "noun", "adjective adjective"},
    91  		},
    92  		{
    93  			name:     "root flags foo args",
    94  			args:     []string{"-f=0.99", "foo", "abc", "def", "ghi"},
    95  			rootvars: fftest.Vars{F: 0.99},
    96  			fooran:   true,
    97  			fooargs:  []string{"abc", "def", "ghi"},
    98  		},
    99  		{
   100  			name:    "root bar -- args",
   101  			args:    []string{"bar", "--", "argument", "list"},
   102  			barran:  true,
   103  			barargs: []string{"argument", "list"},
   104  		},
   105  	} {
   106  		t.Run(testcase.name, func(t *testing.T) {
   107  			foofs, foovars := fftest.Pair()
   108  			var fooargs []string
   109  			var fooran bool
   110  			foo := &ffcli.Command{
   111  				Name:    "foo",
   112  				FlagSet: foofs,
   113  				Exec:    func(_ context.Context, args []string) error { fooran, fooargs = true, args; return nil },
   114  			}
   115  
   116  			barfs, barvars := fftest.Pair()
   117  			var barargs []string
   118  			var barran bool
   119  			bar := &ffcli.Command{
   120  				Name:    "bar",
   121  				FlagSet: barfs,
   122  				Exec:    func(_ context.Context, args []string) error { barran, barargs = true, args; return nil },
   123  			}
   124  
   125  			rootfs, rootvars := fftest.Pair()
   126  			var rootargs []string
   127  			var rootran bool
   128  			root := &ffcli.Command{
   129  				FlagSet:     rootfs,
   130  				Subcommands: []*ffcli.Command{foo, bar},
   131  				Exec:        func(_ context.Context, args []string) error { rootran, rootargs = true, args; return nil },
   132  			}
   133  
   134  			err := root.ParseAndRun(context.Background(), testcase.args)
   135  			assertNoError(t, err)
   136  			fftest.Compare(t, &testcase.rootvars, rootvars)
   137  			assertBool(t, testcase.rootran, rootran)
   138  			assertStringSlice(t, testcase.rootargs, rootargs)
   139  			fftest.Compare(t, &testcase.foovars, foovars)
   140  			assertBool(t, testcase.fooran, fooran)
   141  			assertStringSlice(t, testcase.fooargs, fooargs)
   142  			fftest.Compare(t, &testcase.barvars, barvars)
   143  			assertBool(t, testcase.barran, barran)
   144  			assertStringSlice(t, testcase.barargs, barargs)
   145  		})
   146  	}
   147  }
   148  
   149  func TestHelpUsage(t *testing.T) {
   150  	t.Parallel()
   151  
   152  	for _, testcase := range []struct {
   153  		name      string
   154  		usageFunc func(*ffcli.Command) string
   155  		exec      func(context.Context, []string) error
   156  		args      []string
   157  		output    string
   158  	}{
   159  		{
   160  			name:   "nil",
   161  			args:   []string{"-h"},
   162  			output: defaultUsageFuncOutput,
   163  		},
   164  		{
   165  			name:      "DefaultUsageFunc",
   166  			usageFunc: ffcli.DefaultUsageFunc,
   167  			args:      []string{"-h"},
   168  			output:    defaultUsageFuncOutput,
   169  		},
   170  		{
   171  			name:      "custom usage",
   172  			usageFunc: func(*ffcli.Command) string { return "๐Ÿฐ" },
   173  			args:      []string{"-h"},
   174  			output:    "๐Ÿฐ\n",
   175  		},
   176  		{
   177  			name:      "ErrHelp",
   178  			usageFunc: func(*ffcli.Command) string { return "๐Ÿ‘น" },
   179  			exec:      func(context.Context, []string) error { return flag.ErrHelp },
   180  			output:    "๐Ÿ‘น\n",
   181  		},
   182  	} {
   183  		t.Run(testcase.name, func(t *testing.T) {
   184  			fs, _ := fftest.Pair()
   185  			var buf bytes.Buffer
   186  			fs.SetOutput(&buf)
   187  
   188  			command := &ffcli.Command{
   189  				Name:       "TestHelpUsage",
   190  				ShortUsage: "TestHelpUsage [flags] <args>",
   191  				ShortHelp:  "Some short help.",
   192  				LongHelp:   "Some long help.",
   193  				FlagSet:    fs,
   194  				UsageFunc:  testcase.usageFunc,
   195  				Exec:       testcase.exec,
   196  			}
   197  
   198  			err := command.ParseAndRun(context.Background(), testcase.args)
   199  			assertErrorIs(t, flag.ErrHelp, err)
   200  			assertMultilineString(t, testcase.output, buf.String())
   201  		})
   202  	}
   203  }
   204  
   205  func TestNestedOutput(t *testing.T) {
   206  	t.Parallel()
   207  
   208  	for _, testcase := range []struct {
   209  		name       string
   210  		args       []string
   211  		wantErr    error
   212  		wantOutput string
   213  	}{
   214  		{
   215  			name:       "root without args",
   216  			args:       []string{},
   217  			wantErr:    flag.ErrHelp,
   218  			wantOutput: "root usage func\n",
   219  		},
   220  		{
   221  			name:       "root with args",
   222  			args:       []string{"abc", "def ghi"},
   223  			wantErr:    flag.ErrHelp,
   224  			wantOutput: "root usage func\n",
   225  		},
   226  		{
   227  			name:       "root help",
   228  			args:       []string{"-h"},
   229  			wantErr:    flag.ErrHelp,
   230  			wantOutput: "root usage func\n",
   231  		},
   232  		{
   233  			name:       "foo without args",
   234  			args:       []string{"foo"},
   235  			wantOutput: "foo: ''\n",
   236  		},
   237  		{
   238  			name:       "foo with args",
   239  			args:       []string{"foo", "alpha", "beta"},
   240  			wantOutput: "foo: 'alpha beta'\n",
   241  		},
   242  		{
   243  			name:       "foo help",
   244  			args:       []string{"foo", "-h"},
   245  			wantErr:    flag.ErrHelp,
   246  			wantOutput: "foo usage func\n", // only one instance of usage string
   247  		},
   248  		{
   249  			name:       "foo bar without args",
   250  			args:       []string{"foo", "bar"},
   251  			wantErr:    flag.ErrHelp,
   252  			wantOutput: "bar usage func\n",
   253  		},
   254  		{
   255  			name:       "foo bar with args",
   256  			args:       []string{"foo", "bar", "--", "baz quux"},
   257  			wantErr:    flag.ErrHelp,
   258  			wantOutput: "bar usage func\n",
   259  		},
   260  		{
   261  			name:       "foo bar help",
   262  			args:       []string{"foo", "bar", "--help"},
   263  			wantErr:    flag.ErrHelp,
   264  			wantOutput: "bar usage func\n",
   265  		},
   266  	} {
   267  		t.Run(testcase.name, func(t *testing.T) {
   268  			var (
   269  				rootfs = flag.NewFlagSet("root", flag.ContinueOnError)
   270  				foofs  = flag.NewFlagSet("foo", flag.ContinueOnError)
   271  				barfs  = flag.NewFlagSet("bar", flag.ContinueOnError)
   272  				buf    bytes.Buffer
   273  			)
   274  			rootfs.SetOutput(&buf)
   275  			foofs.SetOutput(&buf)
   276  			barfs.SetOutput(&buf)
   277  
   278  			barExec := func(_ context.Context, args []string) error {
   279  				return flag.ErrHelp
   280  			}
   281  
   282  			bar := &ffcli.Command{
   283  				Name:      "bar",
   284  				FlagSet:   barfs,
   285  				UsageFunc: func(*ffcli.Command) string { return "bar usage func" },
   286  				Exec:      barExec,
   287  			}
   288  
   289  			fooExec := func(_ context.Context, args []string) error {
   290  				fmt.Fprintf(&buf, "foo: '%s'\n", strings.Join(args, " "))
   291  				return nil
   292  			}
   293  
   294  			foo := &ffcli.Command{
   295  				Name:        "foo",
   296  				FlagSet:     foofs,
   297  				UsageFunc:   func(*ffcli.Command) string { return "foo usage func" },
   298  				Subcommands: []*ffcli.Command{bar},
   299  				Exec:        fooExec,
   300  			}
   301  
   302  			rootExec := func(_ context.Context, args []string) error {
   303  				return flag.ErrHelp
   304  			}
   305  
   306  			root := &ffcli.Command{
   307  				FlagSet:     rootfs,
   308  				UsageFunc:   func(*ffcli.Command) string { return "root usage func" },
   309  				Subcommands: []*ffcli.Command{foo},
   310  				Exec:        rootExec,
   311  			}
   312  
   313  			err := root.ParseAndRun(context.Background(), testcase.args)
   314  			if want, have := testcase.wantErr, err; !errors.Is(have, want) {
   315  				t.Errorf("error: want %v, have %v", want, have)
   316  			}
   317  			if want, have := testcase.wantOutput, buf.String(); want != have {
   318  				t.Errorf("output: want %q, have %q", want, have)
   319  			}
   320  		})
   321  	}
   322  }
   323  
   324  func TestIssue57(t *testing.T) {
   325  	t.Parallel()
   326  
   327  	for _, testcase := range []struct {
   328  		args        []string
   329  		parseErrAs  error
   330  		parseErrIs  error
   331  		parseErrStr string
   332  		runErrAs    error
   333  		runErrIs    error
   334  		runErrStr   string
   335  	}{
   336  		{
   337  			args:       []string{},
   338  			parseErrAs: &ffcli.NoExecError{},
   339  			runErrAs:   &ffcli.NoExecError{},
   340  		},
   341  		{
   342  			args:       []string{"-h"},
   343  			parseErrIs: flag.ErrHelp,
   344  			runErrIs:   ffcli.ErrUnparsed,
   345  		},
   346  		{
   347  			args:       []string{"bar"},
   348  			parseErrAs: &ffcli.NoExecError{},
   349  			runErrAs:   &ffcli.NoExecError{},
   350  		},
   351  		{
   352  			args:       []string{"bar", "-h"},
   353  			parseErrAs: flag.ErrHelp,
   354  			runErrAs:   ffcli.ErrUnparsed,
   355  		},
   356  		{
   357  			args:        []string{"bar", "-undefined"},
   358  			parseErrStr: "error parsing commandline args: flag provided but not defined: -undefined",
   359  			runErrIs:    ffcli.ErrUnparsed,
   360  		},
   361  		{
   362  			args: []string{"bar", "baz"},
   363  		},
   364  		{
   365  			args:       []string{"bar", "baz", "-h"},
   366  			parseErrIs: flag.ErrHelp,
   367  			runErrIs:   ffcli.ErrUnparsed,
   368  		},
   369  		{
   370  			args:        []string{"bar", "baz", "-also.undefined"},
   371  			parseErrStr: "error parsing commandline args: flag provided but not defined: -also.undefined",
   372  			runErrIs:    ffcli.ErrUnparsed,
   373  		},
   374  	} {
   375  		t.Run(strings.Join(append([]string{"foo"}, testcase.args...), " "), func(t *testing.T) {
   376  			fs := flag.NewFlagSet("ยท", flag.ContinueOnError)
   377  			fs.SetOutput(ioutil.Discard)
   378  
   379  			var (
   380  				baz = &ffcli.Command{Name: "baz", FlagSet: fs, Exec: func(_ context.Context, args []string) error { return nil }}
   381  				bar = &ffcli.Command{Name: "bar", FlagSet: fs, Subcommands: []*ffcli.Command{baz}}
   382  				foo = &ffcli.Command{Name: "foo", FlagSet: fs, Subcommands: []*ffcli.Command{bar}}
   383  			)
   384  
   385  			var (
   386  				parseErr = foo.Parse(testcase.args)
   387  				runErr   = foo.Run(context.Background())
   388  			)
   389  
   390  			if testcase.parseErrAs != nil {
   391  				if want, have := &testcase.parseErrAs, parseErr; !errors.As(have, want) {
   392  					t.Errorf("Parse: want %v, have %v", want, have)
   393  				}
   394  			}
   395  
   396  			if testcase.parseErrIs != nil {
   397  				if want, have := testcase.parseErrIs, parseErr; !errors.Is(have, want) {
   398  					t.Errorf("Parse: want %v, have %v", want, have)
   399  				}
   400  			}
   401  
   402  			if testcase.parseErrStr != "" {
   403  				if want, have := testcase.parseErrStr, parseErr.Error(); want != have {
   404  					t.Errorf("Parse: want %q, have %q", want, have)
   405  				}
   406  			}
   407  
   408  			if testcase.runErrAs != nil {
   409  				if want, have := &testcase.runErrAs, runErr; !errors.As(have, want) {
   410  					t.Errorf("Run: want %v, have %v", want, have)
   411  				}
   412  			}
   413  
   414  			if testcase.runErrIs != nil {
   415  				if want, have := testcase.runErrIs, runErr; !errors.Is(have, want) {
   416  					t.Errorf("Run: want %v, have %v", want, have)
   417  				}
   418  			}
   419  
   420  			if testcase.runErrStr != "" {
   421  				if want, have := testcase.runErrStr, runErr.Error(); want != have {
   422  					t.Errorf("Run: want %q, have %q", want, have)
   423  				}
   424  			}
   425  
   426  			var (
   427  				noParseErr = testcase.parseErrAs == nil && testcase.parseErrIs == nil && testcase.parseErrStr == ""
   428  				noRunErr   = testcase.runErrAs == nil && testcase.runErrIs == nil && testcase.runErrStr == ""
   429  			)
   430  			if noParseErr && noRunErr {
   431  				if parseErr != nil {
   432  					t.Errorf("Parse: unexpected error: %v", parseErr)
   433  				}
   434  				if runErr != nil {
   435  					t.Errorf("Run: unexpected error: %v", runErr)
   436  				}
   437  			}
   438  		})
   439  	}
   440  }
   441  
   442  func ExampleCommand_Parse_then_Run() {
   443  	// Assume our CLI will use some client that requires a token.
   444  	type FooClient struct {
   445  		token string
   446  	}
   447  
   448  	// That client would have a constructor.
   449  	NewFooClient := func(token string) (*FooClient, error) {
   450  		if token == "" {
   451  			return nil, fmt.Errorf("token required")
   452  		}
   453  		return &FooClient{token: token}, nil
   454  	}
   455  
   456  	// We define the token in the root command's FlagSet.
   457  	var (
   458  		rootFlagSet = flag.NewFlagSet("mycommand", flag.ExitOnError)
   459  		token       = rootFlagSet.String("token", "", "API token")
   460  	)
   461  
   462  	// Create a placeholder client, initially nil.
   463  	var client *FooClient
   464  
   465  	// Commands can reference and use it, because by the time their Exec
   466  	// function is invoked, the client will be constructed.
   467  	foo := &ffcli.Command{
   468  		Name: "foo",
   469  		Exec: func(context.Context, []string) error {
   470  			fmt.Printf("subcommand foo can use the client: %v", client)
   471  			return nil
   472  		},
   473  	}
   474  
   475  	root := &ffcli.Command{
   476  		FlagSet:     rootFlagSet,
   477  		Subcommands: []*ffcli.Command{foo},
   478  	}
   479  
   480  	// Call Parse first, to populate flags and select a terminal command.
   481  	if err := root.Parse([]string{"-token", "SECRETKEY", "foo"}); err != nil {
   482  		log.Fatalf("Parse failure: %v", err)
   483  	}
   484  
   485  	// After a successful Parse, we can construct a FooClient with the token.
   486  	var err error
   487  	client, err = NewFooClient(*token)
   488  	if err != nil {
   489  		log.Fatalf("error constructing FooClient: %v", err)
   490  	}
   491  
   492  	// Then call Run, which will select the foo subcommand and invoke it.
   493  	if err := root.Run(context.Background()); err != nil {
   494  		log.Fatalf("Run failure: %v", err)
   495  	}
   496  
   497  	// Output:
   498  	// subcommand foo can use the client: &{SECRETKEY}
   499  }
   500  
   501  func assertNoError(t *testing.T, err error) {
   502  	t.Helper()
   503  	if err != nil {
   504  		t.Fatal(err)
   505  	}
   506  }
   507  
   508  func assertErrorIs(t *testing.T, want, have error) {
   509  	t.Helper()
   510  	if !errors.Is(have, want) {
   511  		t.Fatalf("want %v, have %v", want, have)
   512  	}
   513  }
   514  
   515  func assertMultilineString(t *testing.T, want, have string) {
   516  	t.Helper()
   517  	if want != have {
   518  		t.Fatalf("\nwant:\n%s\n\nhave:\n%s\n", want, have)
   519  	}
   520  }
   521  
   522  func assertBool(t *testing.T, want, have bool) {
   523  	t.Helper()
   524  	if want != have {
   525  		t.Fatalf("want %v, have %v", want, have)
   526  	}
   527  }
   528  
   529  func assertStringSlice(t *testing.T, want, have []string) {
   530  	t.Helper()
   531  	if len(want) == 0 && len(have) == 0 {
   532  		return // consider []string{} and []string(nil) equivalent
   533  	}
   534  	if !reflect.DeepEqual(want, have) {
   535  		t.Fatalf("want %#+v, have %#+v", want, have)
   536  	}
   537  }
   538  
   539  var defaultUsageFuncOutput = strings.TrimSpace(`
   540  USAGE
   541    TestHelpUsage [flags] <args>
   542  
   543  Some long help.
   544  
   545  FLAGS
   546    -b=false  bool
   547    -d 0s     time.Duration
   548    -f 0      float64
   549    -i 0      int
   550    -s ...    string
   551    -x ...    collection of strings (repeatable)
   552  `) + "\n\n"
   553  

View as plain text