github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/appcli/completion_test.go (about)

     1  package appcli
     2  
     3  import (
     4  	"bytes"
     5  	"os"
     6  	"testing"
     7  
     8  	"github.com/clusterize-io/tusk/marshal"
     9  	"github.com/clusterize-io/tusk/runner"
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/urfave/cli"
    12  )
    13  
    14  func TestDefaultComplete(t *testing.T) {
    15  	tests := []struct {
    16  		name     string
    17  		narg     int
    18  		trailing string
    19  		flagsSet []string
    20  		want     string
    21  	}{
    22  		{
    23  			name:     "invalid input",
    24  			narg:     1,
    25  			want:     ``,
    26  			trailing: "foo",
    27  		},
    28  		{
    29  			name: "default completion",
    30  			want: `normal
    31  foo:a foo command
    32  --bool:a boolean value
    33  --string:a string value
    34  `,
    35  			trailing: "tusk",
    36  		},
    37  		{
    38  			name: "ignores set values",
    39  			want: `normal
    40  foo:a foo command
    41  --string:a string value
    42  `,
    43  			trailing: "--bool",
    44  			flagsSet: []string{"bool"},
    45  		},
    46  		{
    47  			name: "flag completion",
    48  			want: `file
    49  `,
    50  			trailing: "--string",
    51  		},
    52  	}
    53  
    54  	for _, tt := range tests {
    55  		t.Run(tt.name, func(t *testing.T) {
    56  			defer func(args []string) {
    57  				os.Args = args
    58  			}(os.Args)
    59  			// We only care about the "trailing" arg, second from last
    60  			os.Args = []string{tt.trailing, "--"}
    61  
    62  			var buf bytes.Buffer
    63  			app := cli.NewApp()
    64  			app.Commands = []cli.Command{
    65  				{
    66  					Name:  "foo",
    67  					Usage: "a foo command",
    68  					Flags: []cli.Flag{
    69  						cli.BoolFlag{
    70  							Name:  "foo-flag",
    71  							Usage: "a flag for foo",
    72  						},
    73  					},
    74  				},
    75  			}
    76  			app.Flags = []cli.Flag{
    77  				cli.BoolFlag{
    78  					Name:  "bool",
    79  					Usage: "a boolean value",
    80  				},
    81  				cli.StringFlag{
    82  					Name:  "string",
    83  					Usage: "a string value",
    84  				},
    85  			}
    86  
    87  			c := mockContext{
    88  				narg:  tt.narg,
    89  				flags: tt.flagsSet,
    90  			}
    91  			defaultComplete(&buf, c, app)
    92  
    93  			if diff := cmp.Diff(tt.want, buf.String()); diff != "" {
    94  				t.Errorf("completion output differs:\n%s", diff)
    95  			}
    96  		})
    97  	}
    98  }
    99  
   100  func TestCommandComplete(t *testing.T) {
   101  	tests := []struct {
   102  		name     string
   103  		command  *cli.Command
   104  		narg     int
   105  		taskArgs runner.Args
   106  		flagsSet []string
   107  		trailing string
   108  		want     string
   109  	}{
   110  		{
   111  			name: "default",
   112  			want: `task-no-args
   113  --bool:a boolean flag
   114  --string:a string flag
   115  --values:a flag with limited allowed values
   116  `,
   117  			trailing: "my-cmd",
   118  		},
   119  		{
   120  			name: "first arg",
   121  			want: `task-args
   122  foo
   123  bar
   124  --bool:a boolean flag
   125  --string:a string flag
   126  --values:a flag with limited allowed values
   127  `,
   128  			taskArgs: runner.Args{
   129  				{
   130  					Name: "first",
   131  					ValueWithList: runner.ValueWithList{
   132  						ValuesAllowed: []string{"foo", "bar"},
   133  					},
   134  				},
   135  				{
   136  					Name: "second",
   137  					ValueWithList: runner.ValueWithList{
   138  						ValuesAllowed: []string{"baz"},
   139  					},
   140  				},
   141  			},
   142  			trailing: "my-cmd",
   143  		},
   144  		{
   145  			name: "second arg",
   146  			want: `task-args
   147  baz
   148  --bool:a boolean flag
   149  --string:a string flag
   150  --values:a flag with limited allowed values
   151  `,
   152  			taskArgs: runner.Args{
   153  				{
   154  					Name: "first",
   155  					ValueWithList: runner.ValueWithList{
   156  						ValuesAllowed: []string{"foo", "bar"},
   157  					},
   158  				},
   159  				{
   160  					Name: "second",
   161  					ValueWithList: runner.ValueWithList{
   162  						ValuesAllowed: []string{"baz"},
   163  					},
   164  				},
   165  			},
   166  			narg:     1,
   167  			trailing: "my-cmd",
   168  		},
   169  		{
   170  			name: "args with a flag set",
   171  			want: `task-args
   172  foo
   173  bar
   174  baz
   175  --bool:a boolean flag
   176  --values:a flag with limited allowed values
   177  `,
   178  			taskArgs: runner.Args{
   179  				{
   180  					Name: "foo",
   181  					ValueWithList: runner.ValueWithList{
   182  						ValuesAllowed: []string{"foo", "bar", "baz"},
   183  					},
   184  				},
   185  			},
   186  			flagsSet: []string{"string"},
   187  			trailing: "my-cmd",
   188  		},
   189  		{
   190  			name:     "string option",
   191  			want:     "file\n",
   192  			trailing: "--string",
   193  		},
   194  		{
   195  			name: "string option with values",
   196  			want: `value
   197  foo
   198  bar
   199  baz
   200  `,
   201  			trailing: "--values",
   202  		},
   203  		{
   204  			name: "boolean no values",
   205  			want: `task-no-args
   206  --string:a string flag
   207  --values:a flag with limited allowed values
   208  `,
   209  			flagsSet: []string{"bool"},
   210  			trailing: "--bool",
   211  		},
   212  	}
   213  
   214  	for _, tt := range tests {
   215  		t.Run(tt.name, func(t *testing.T) {
   216  			defer func(args []string) {
   217  				os.Args = args
   218  			}(os.Args)
   219  			// We only care about the "trailing" arg, second from last
   220  			os.Args = []string{tt.trailing, "--"}
   221  
   222  			var buf bytes.Buffer
   223  
   224  			cmd := &cli.Command{
   225  				Name:  "my-cmd",
   226  				Usage: "a command",
   227  				Flags: []cli.Flag{
   228  					cli.BoolFlag{
   229  						Name:  "bool",
   230  						Usage: "a boolean flag",
   231  					},
   232  					cli.StringFlag{
   233  						Name:  "string",
   234  						Usage: "a string flag",
   235  					},
   236  					cli.StringFlag{
   237  						Name:  "values",
   238  						Usage: "a flag with limited allowed values",
   239  					},
   240  				},
   241  			}
   242  
   243  			cfg := &runner.Config{
   244  				Tasks: map[string]*runner.Task{
   245  					cmd.Name: {
   246  						Args: tt.taskArgs,
   247  						Options: runner.Options{
   248  							{Name: "bool", Type: "bool"},
   249  							{Name: "string"},
   250  							{
   251  								Name: "values",
   252  								ValueWithList: runner.ValueWithList{
   253  									ValuesAllowed: marshal.StringList{"foo", "bar", "baz"},
   254  								},
   255  							},
   256  						},
   257  					},
   258  				},
   259  			}
   260  
   261  			c := mockContext{
   262  				narg:  tt.narg,
   263  				flags: tt.flagsSet,
   264  			}
   265  
   266  			commandComplete(&buf, c, cmd, cfg)
   267  
   268  			if diff := cmp.Diff(tt.want, buf.String()); diff != "" {
   269  				t.Errorf("completion output differs:\n%v", diff)
   270  			}
   271  		})
   272  	}
   273  }
   274  
   275  func TestPrintCommand(t *testing.T) {
   276  	tests := []struct {
   277  		name    string
   278  		command *cli.Command
   279  		want    string
   280  	}{
   281  		{
   282  			name: "arg without usage",
   283  			command: &cli.Command{
   284  				Name: "my-cmd",
   285  			},
   286  			want: "my-cmd\n",
   287  		},
   288  		{
   289  			name: "arg with usage",
   290  			command: &cli.Command{
   291  				Name:  "my-cmd",
   292  				Usage: "My description",
   293  			},
   294  			want: "my-cmd:My description\n",
   295  		},
   296  		{
   297  			name: "arg without usage escapes colon",
   298  			command: &cli.Command{
   299  				Name: "my:cmd",
   300  			},
   301  			want: "my\\:cmd\n",
   302  		},
   303  		{
   304  			name: "arg with usage escapes colon",
   305  			command: &cli.Command{
   306  				Name:  "my:cmd",
   307  				Usage: "My description",
   308  			},
   309  			want: "my\\:cmd:My description\n",
   310  		},
   311  		{
   312  			name: "hidden",
   313  			command: &cli.Command{
   314  				Name:   "my-cmd",
   315  				Usage:  "My description",
   316  				Hidden: true,
   317  			},
   318  			want: "",
   319  		},
   320  	}
   321  
   322  	for _, tt := range tests {
   323  		t.Run(tt.name, func(t *testing.T) {
   324  			var buf bytes.Buffer
   325  
   326  			printCommand(&buf, tt.command)
   327  
   328  			if diff := cmp.Diff(tt.want, buf.String()); diff != "" {
   329  				t.Errorf("completion output differs:\n%v", diff)
   330  			}
   331  		})
   332  	}
   333  }
   334  
   335  func TestPrintFlag(t *testing.T) {
   336  	tests := []struct {
   337  		name string
   338  		flag cli.Flag
   339  		want string
   340  	}{
   341  		{
   342  			name: "flag without usage",
   343  			flag: &cli.BoolFlag{
   344  				Name: "my-flag",
   345  			},
   346  			want: "--my-flag\n",
   347  		},
   348  		{
   349  			name: "arg with usage",
   350  			flag: &cli.BoolFlag{
   351  				Name:  "my-flag",
   352  				Usage: "My description",
   353  			},
   354  			want: "--my-flag:My description\n",
   355  		},
   356  		{
   357  			name: "arg without usage escapes colon",
   358  			flag: &cli.BoolFlag{
   359  				Name: "my:flag",
   360  			},
   361  			want: "--my\\:flag\n",
   362  		},
   363  		{
   364  			name: "arg with usage escapes colon",
   365  			flag: &cli.BoolFlag{
   366  				Name:  "my:flag",
   367  				Usage: "My description",
   368  			},
   369  			want: "--my\\:flag:My description\n",
   370  		},
   371  	}
   372  
   373  	for _, tt := range tests {
   374  		t.Run(tt.name, func(t *testing.T) {
   375  			var buf bytes.Buffer
   376  			var c mockContext
   377  
   378  			printFlag(&buf, c, tt.flag)
   379  
   380  			if diff := cmp.Diff(tt.want, buf.String()); diff != "" {
   381  				t.Errorf("completion output differs:\n%v", diff)
   382  			}
   383  		})
   384  	}
   385  }
   386  
   387  func TestIsCompletingFlagArg(t *testing.T) {
   388  	tests := []struct {
   389  		flags    []cli.Flag
   390  		arg      string
   391  		expected bool
   392  	}{
   393  		{[]cli.Flag{}, "foo", false},
   394  		{[]cli.Flag{}, "-f", false},
   395  		{[]cli.Flag{}, "--foo", false},
   396  		{[]cli.Flag{cli.BoolFlag{Name: "f, foo"}}, "-f", false},
   397  		{[]cli.Flag{cli.BoolFlag{Name: "f, foo"}}, "--foo", false},
   398  		{[]cli.Flag{cli.BoolTFlag{Name: "f, foo"}}, "-f", false},
   399  		{[]cli.Flag{cli.BoolTFlag{Name: "f, foo"}}, "--foo", false},
   400  		{[]cli.Flag{cli.StringFlag{Name: "f, foo"}}, "-f", true},
   401  		{[]cli.Flag{cli.StringFlag{Name: "f, foo"}}, "--foo", true},
   402  		{[]cli.Flag{cli.StringFlag{Name: "f, foo"}}, "--f", false},
   403  		{[]cli.Flag{cli.StringFlag{Name: "b, bar"}}, "-f", false},
   404  		{[]cli.Flag{cli.StringFlag{Name: "b, bar"}}, "--foo", false},
   405  		{[]cli.Flag{cli.StringFlag{Name: "f, foo"}, cli.StringFlag{Name: "b, bar"}}, "-f", true},
   406  		{[]cli.Flag{cli.StringFlag{Name: "f, foo"}, cli.StringFlag{Name: "b, bar"}}, "--foo", true},
   407  	}
   408  
   409  	for _, tt := range tests {
   410  		actual := isCompletingFlagArg(tt.flags, tt.arg)
   411  		if tt.expected != actual {
   412  			t.Errorf(
   413  				"isCompletingFlagArg(%#v, %s) => %t, want %t",
   414  				tt.flags, tt.arg, actual, tt.expected,
   415  			)
   416  		}
   417  	}
   418  }
   419  
   420  type mockContext struct {
   421  	narg  int
   422  	flags []string
   423  }
   424  
   425  func (m mockContext) NArg() int {
   426  	return m.narg
   427  }
   428  
   429  func (m mockContext) IsSet(name string) bool {
   430  	for _, flag := range m.flags {
   431  		if flag == name {
   432  			return true
   433  		}
   434  	}
   435  
   436  	return false
   437  }