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 }