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 }