github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/main_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "bytes" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "runtime" 13 "sort" 14 "strings" 15 16 "github.com/juju/cmd" 17 "github.com/juju/cmd/cmdtesting" 18 "github.com/juju/collections/set" 19 "github.com/juju/gnuflag" 20 "github.com/juju/os/series" 21 gitjujutesting "github.com/juju/testing" 22 jc "github.com/juju/testing/checkers" 23 "github.com/juju/utils/arch" 24 "github.com/juju/utils/featureflag" 25 "github.com/juju/version" 26 gc "gopkg.in/check.v1" 27 28 jujucmd "github.com/juju/juju/cmd" 29 "github.com/juju/juju/cmd/juju/application" 30 "github.com/juju/juju/cmd/juju/cloud" 31 "github.com/juju/juju/cmd/modelcmd" 32 "github.com/juju/juju/juju/osenv" 33 "github.com/juju/juju/jujuclient/jujuclienttesting" 34 _ "github.com/juju/juju/provider/dummy" 35 "github.com/juju/juju/testing" 36 jujuversion "github.com/juju/juju/version" 37 ) 38 39 type MainSuite struct { 40 testing.FakeJujuXDGDataHomeSuite 41 gitjujutesting.PatchExecHelper 42 } 43 44 var _ = gc.Suite(&MainSuite{}) 45 46 func helpText(command cmd.Command, name string) string { 47 buff := &bytes.Buffer{} 48 info := command.Info() 49 info.Name = name 50 f := gnuflag.NewFlagSetWithFlagKnownAs(info.Name, gnuflag.ContinueOnError, cmd.FlagAlias(command, "option")) 51 command.SetFlags(f) 52 53 superJuju := jujucmd.NewSuperCommand(cmd.SuperCommandParams{ 54 Name: "juju", 55 FlagKnownAs: "option", 56 }) 57 superF := gnuflag.NewFlagSetWithFlagKnownAs("juju", gnuflag.ContinueOnError, "option") 58 superJuju.SetFlags(superF) 59 60 buff.Write(info.HelpWithSuperFlags(superF, f)) 61 return buff.String() 62 } 63 64 func deployHelpText() string { 65 return helpText(application.NewDeployCommand(), "juju deploy") 66 } 67 func configHelpText() string { 68 return helpText(application.NewConfigCommand(), "juju config") 69 } 70 71 func syncToolsHelpText() string { 72 return helpText(newSyncToolsCommand(), "juju sync-agent-binaries") 73 } 74 75 func (s *MainSuite) TestRunMain(c *gc.C) { 76 jujuclienttesting.SetupMinimalFileStore(c) 77 78 // The test array structure needs to be inline here as some of the 79 // expected values below use deployHelpText(). This constructs the deploy 80 // command and runs gets the help for it. When the deploy command is 81 // setting the flags (which is needed for the help text) it is accessing 82 // osenv.JujuXDGDataHome(), which panics if SetJujuXDGDataHome has not been called. 83 // The FakeHome from testing does this. 84 for i, t := range []struct { 85 summary string 86 args []string 87 code int 88 out string 89 }{{ 90 summary: "juju help foo doesn't exist", 91 args: []string{"help", "foo"}, 92 code: 1, 93 out: "ERROR unknown command or topic for foo\n", 94 }, { 95 summary: "juju help deploy shows the default help without global options", 96 args: []string{"help", "deploy"}, 97 code: 0, 98 out: deployHelpText(), 99 }, { 100 summary: "juju --help deploy shows the same help as 'help deploy'", 101 args: []string{"--help", "deploy"}, 102 code: 0, 103 out: deployHelpText(), 104 }, { 105 summary: "juju deploy --help shows the same help as 'help deploy'", 106 args: []string{"deploy", "--help"}, 107 code: 0, 108 out: deployHelpText(), 109 }, { 110 summary: "juju --help config shows the same help as 'help config'", 111 args: []string{"--help", "config"}, 112 code: 0, 113 out: configHelpText(), 114 }, { 115 summary: "juju config --help shows the same help as 'help config'", 116 args: []string{"config", "--help"}, 117 code: 0, 118 out: configHelpText(), 119 }, { 120 summary: "unknown command", 121 args: []string{"discombobulate"}, 122 code: 1, 123 out: "ERROR unrecognized command: juju discombobulate\n", 124 }, { 125 summary: "unknown option before command", 126 args: []string{"--cheese", "bootstrap"}, 127 code: 2, 128 out: "ERROR option provided but not defined: --cheese\n", 129 }, { 130 summary: "unknown option after command", 131 args: []string{"bootstrap", "--cheese"}, 132 code: 2, 133 out: "ERROR option provided but not defined: --cheese\n", 134 }, { 135 summary: "known option, but specified before command", 136 args: []string{"--model", "blah", "bootstrap"}, 137 code: 2, 138 out: "ERROR option provided but not defined: --model\n", 139 }, { 140 summary: "juju sync-agent-binaries registered properly", 141 args: []string{"sync-agent-binaries", "--help"}, 142 code: 0, 143 out: syncToolsHelpText(), 144 }, { 145 summary: "check version command returns a fully qualified version string", 146 args: []string{"version"}, 147 code: 0, 148 out: version.Binary{ 149 Number: jujuversion.Current, 150 Arch: arch.HostArch(), 151 Series: series.MustHostSeries(), 152 }.String() + "\n", 153 }} { 154 c.Logf("test %d: %s", i, t.summary) 155 out := badrun(c, t.code, t.args...) 156 c.Assert(out, gc.Equals, t.out) 157 } 158 } 159 160 func (s *MainSuite) TestActualRunJujuArgOrder(c *gc.C) { 161 //TODO(bogdanteleaga): cannot read the env file because of some suite 162 //problems. The juju home, when calling something from the command line is 163 //not the same as in the test suite. 164 if runtime.GOOS == "windows" { 165 c.Skip("bug 1403084: cannot read env file on windows because of suite problems") 166 } 167 s.PatchEnvironment(osenv.JujuControllerEnvKey, "current-controller") 168 s.PatchEnvironment(osenv.JujuModelEnvKey, "current") 169 logpath := filepath.Join(c.MkDir(), "log") 170 tests := [][]string{ 171 {"--log-file", logpath, "--debug", "help"}, // global flags before 172 {"help", "--log-file", logpath, "--debug"}, // after 173 {"--log-file", logpath, "help", "--debug"}, // mixed 174 } 175 for i, test := range tests { 176 c.Logf("test %d: %v", i, test) 177 badrun(c, 0, test...) 178 content, err := ioutil.ReadFile(logpath) 179 c.Assert(err, jc.ErrorIsNil) 180 c.Assert(string(content), gc.Matches, "(.|\n)*running juju(.|\n)*command finished(.|\n)*") 181 err = os.Remove(logpath) 182 c.Assert(err, jc.ErrorIsNil) 183 } 184 } 185 186 func (s *MainSuite) TestFirstRun2xFrom1xOnUbuntu(c *gc.C) { 187 if runtime.GOOS == "windows" { 188 // This test can't work on Windows and shouldn't need to 189 c.Skip("test doesn't work on Windows because Juju's 1.x and 2.x config directory are the same") 190 } 191 192 // Code should only run on ubuntu series, so patch out the series for 193 // when non-ubuntu OSes run this test. 194 s.PatchValue(&series.MustHostSeries, func() string { return "trusty" }) 195 196 argChan := make(chan []string, 1) 197 198 execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{ 199 Stdout: "1.25.0-trusty-amd64", 200 Args: argChan, 201 }) 202 stub := &gitjujutesting.Stub{} 203 s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command { 204 return &stubCommand{stub: stub} 205 }) 206 207 // remove the new juju-home and create a fake old juju home. 208 err := os.RemoveAll(osenv.JujuXDGDataHomeDir()) 209 c.Assert(err, jc.ErrorIsNil) 210 makeValidOldHome(c) 211 212 var code int 213 f := func() { 214 code = main{ 215 execCommand: execCommand, 216 }.Run([]string{"juju", "version"}) 217 } 218 219 stdout, stderr := gitjujutesting.CaptureOutput(c, f) 220 221 select { 222 case args := <-argChan: 223 c.Assert(args, gc.DeepEquals, []string{"juju-1", "version"}) 224 default: 225 c.Fatalf("Exec function not called.") 226 } 227 228 c.Check(code, gc.Equals, 0) 229 c.Check(string(stderr), gc.Equals, fmt.Sprintf(` 230 Welcome to Juju %s. 231 See https://jujucharms.com/docs/stable/introducing-2 for more details. 232 233 If you want to use Juju 1.25.0, run 'juju' commands as 'juju-1'. For example, 'juju-1 bootstrap'. 234 See https://jujucharms.com/docs/stable/juju-coexist for installation details. 235 236 Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n", jujuversion.Current)) 237 checkVersionOutput(c, string(stdout)) 238 } 239 240 func (s *MainSuite) TestFirstRun2xFrom1xNotUbuntu(c *gc.C) { 241 // Code should only run on ubuntu series, so pretend to be something else. 242 s.PatchValue(&series.MustHostSeries, func() string { return "win8" }) 243 244 argChan := make(chan []string, 1) 245 246 // we shouldn't actually be running anything, but if we do, this will 247 // provide some consistent results. 248 execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{ 249 Stdout: "1.25.0-trusty-amd64", 250 Args: argChan, 251 }) 252 stub := &gitjujutesting.Stub{} 253 s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command { 254 return &stubCommand{stub: stub} 255 }) 256 257 // remove the new juju-home and create a fake old juju home. 258 err := os.RemoveAll(osenv.JujuXDGDataHomeDir()) 259 c.Assert(err, jc.ErrorIsNil) 260 261 makeValidOldHome(c) 262 263 var code int 264 stdout, stderr := gitjujutesting.CaptureOutput(c, func() { 265 code = main{ 266 execCommand: execCommand, 267 }.Run([]string{"juju", "version"}) 268 }) 269 270 c.Assert(code, gc.Equals, 0) 271 272 assertNoArgs(c, argChan) 273 274 c.Check(string(stderr), gc.Equals, ` 275 Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n") 276 checkVersionOutput(c, string(stdout)) 277 } 278 279 func (s *MainSuite) TestNoWarn1xWith2xData(c *gc.C) { 280 // Code should only rnu on ubuntu series, so patch out the series for 281 // when non-ubuntu OSes run this test. 282 s.PatchValue(&series.MustHostSeries, func() string { return "trusty" }) 283 284 argChan := make(chan []string, 1) 285 286 // we shouldn't actually be running anything, but if we do, this will 287 // provide some consistent results. 288 execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{ 289 Stdout: "1.25.0-trusty-amd64", 290 Args: argChan, 291 }) 292 293 // there should be a 2x home directory already created by the test setup. 294 295 // create a fake old juju home. 296 makeValidOldHome(c) 297 298 var code int 299 stdout, stderr := gitjujutesting.CaptureOutput(c, func() { 300 code = main{ 301 execCommand: execCommand, 302 }.Run([]string{"juju", "version"}) 303 }) 304 305 c.Assert(code, gc.Equals, 0) 306 307 assertNoArgs(c, argChan) 308 c.Assert(string(stderr), gc.Equals, "") 309 checkVersionOutput(c, string(stdout)) 310 } 311 312 func (s *MainSuite) TestNoWarnWithNo1xOr2xData(c *gc.C) { 313 // Code should only rnu on ubuntu series, so patch out the series for 314 // when non-ubuntu OSes run this test. 315 s.PatchValue(&series.MustHostSeries, func() string { return "trusty" }) 316 317 argChan := make(chan []string, 1) 318 // we shouldn't actually be running anything, but if we do, this will 319 // provide some consistent results. 320 execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{ 321 Stdout: "1.25.0-trusty-amd64", 322 Args: argChan, 323 }) 324 stub := &gitjujutesting.Stub{} 325 s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command { 326 return &stubCommand{stub: stub} 327 }) 328 329 // remove the new juju-home. 330 err := os.RemoveAll(osenv.JujuXDGDataHomeDir()) 331 c.Assert(err, jc.ErrorIsNil) 332 333 // create fake (empty) old juju home. 334 path := c.MkDir() 335 s.PatchEnvironment("JUJU_HOME", path) 336 337 var code int 338 stdout, stderr := gitjujutesting.CaptureOutput(c, func() { 339 code = main{ 340 execCommand: execCommand, 341 }.Run([]string{"juju", "version"}) 342 }) 343 344 c.Assert(code, gc.Equals, 0) 345 346 assertNoArgs(c, argChan) 347 c.Check(string(stderr), gc.Equals, ` 348 Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n") 349 checkVersionOutput(c, string(stdout)) 350 } 351 352 func (s *MainSuite) assertRunCommandUpdateCloud(c *gc.C, expectedCall string) { 353 argChan := make(chan []string, 1) 354 execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{ 355 Stdout: "1.25.0-trusty-amd64", 356 Args: argChan, 357 }) 358 359 stub := &gitjujutesting.Stub{} 360 s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command { 361 return &stubCommand{stub: stub} 362 363 }) 364 var code int 365 gitjujutesting.CaptureOutput(c, func() { 366 code = main{ 367 execCommand: execCommand, 368 }.Run([]string{"juju", "version"}) 369 }) 370 c.Assert(code, gc.Equals, 0) 371 c.Assert(stub.Calls()[0].FuncName, gc.Equals, expectedCall) 372 } 373 374 func (s *MainSuite) TestFirstRunUpdateCloud(c *gc.C) { 375 // remove the juju-home. 376 err := os.RemoveAll(osenv.JujuXDGDataHomeDir()) 377 c.Assert(err, jc.ErrorIsNil) 378 s.assertRunCommandUpdateCloud(c, "Run") 379 } 380 381 func (s *MainSuite) TestRunNoUpdateCloud(c *gc.C) { 382 s.assertRunCommandUpdateCloud(c, "Info") 383 } 384 385 func makeValidOldHome(c *gc.C) { 386 oldhome := osenv.OldJujuHomeDir() 387 err := os.MkdirAll(oldhome, 0700) 388 c.Assert(err, jc.ErrorIsNil) 389 err = ioutil.WriteFile(filepath.Join(oldhome, "environments.yaml"), []byte("boo!"), 0600) 390 c.Assert(err, jc.ErrorIsNil) 391 } 392 393 func checkVersionOutput(c *gc.C, output string) { 394 ver := version.Binary{ 395 Number: jujuversion.Current, 396 Arch: arch.HostArch(), 397 Series: series.MustHostSeries(), 398 } 399 400 c.Check(output, gc.Equals, ver.String()+"\n") 401 } 402 403 func assertNoArgs(c *gc.C, argChan <-chan []string) { 404 select { 405 case args := <-argChan: 406 c.Fatalf("Exec function called when it shouldn't have been (with args %q).", args) 407 default: 408 // this is the good path - there shouldn't be any args, which indicates 409 // the executable was not called. 410 } 411 } 412 413 var commandNames = []string{ 414 "actions", 415 "add-cloud", 416 "add-credential", 417 "add-k8s", 418 "add-machine", 419 "add-model", 420 "add-relation", 421 "add-space", 422 "add-ssh-key", 423 "add-storage", 424 "add-subnet", 425 "add-unit", 426 "add-user", 427 "agree", 428 "agreements", 429 "attach", 430 "attach-resource", 431 "attach-storage", 432 "autoload-credentials", 433 "backups", 434 "bootstrap", 435 "budget", 436 "cached-images", 437 "cancel-action", 438 "change-user-password", 439 "charm", 440 "charm-resources", 441 "clouds", 442 "collect-metrics", 443 "config", 444 "consume", 445 "controller-config", 446 "controllers", 447 "create-backup", 448 "create-storage-pool", 449 "create-wallet", 450 "credentials", 451 "debug-hooks", 452 "debug-log", 453 "deploy", 454 "destroy-controller", 455 "destroy-model", 456 "detach-storage", 457 "diff-bundle", 458 "disable-command", 459 "disable-user", 460 "disabled-commands", 461 "download-backup", 462 "enable-command", 463 "enable-destroy-controller", 464 "enable-ha", 465 "enable-user", 466 "export-bundle", 467 "expose", 468 "find-offers", 469 "firewall-rules", 470 "get-constraints", 471 "get-model-constraints", 472 "grant", 473 "grant-cloud", 474 "gui", 475 "help", 476 "help-tool", 477 "hook-tool", 478 "hook-tools", 479 "import-filesystem", 480 "import-ssh-key", 481 "kill-controller", 482 "list-actions", 483 "list-agreements", 484 "list-backups", 485 "list-cached-images", 486 "list-charm-resources", 487 "list-clouds", 488 "list-controllers", 489 "list-credentials", 490 "list-disabled-commands", 491 "list-firewall-rules", 492 "list-machines", 493 "list-models", 494 "list-offers", 495 "list-payloads", 496 "list-plans", 497 "list-regions", 498 "list-resources", 499 "list-spaces", 500 "list-ssh-keys", 501 "list-storage", 502 "list-storage-pools", 503 "list-subnets", 504 "list-users", 505 "list-wallets", 506 "login", 507 "logout", 508 "machines", 509 "metrics", 510 "migrate", 511 "model-config", 512 "model-default", 513 "model-defaults", 514 "models", 515 "offer", 516 "offers", 517 "payloads", 518 "plans", 519 "regions", 520 "register", 521 "relate", //alias for add-relation 522 "reload-spaces", 523 "remove-application", 524 "remove-backup", 525 "remove-cached-images", 526 "remove-cloud", 527 "remove-consumed-application", 528 "remove-credential", 529 "remove-k8s", 530 "remove-machine", 531 "remove-offer", 532 "remove-relation", 533 "remove-saas", 534 "remove-ssh-key", 535 "remove-storage", 536 "remove-storage-pool", 537 "remove-unit", 538 "remove-user", 539 "resolved", 540 "resolve", 541 "resources", 542 "restore-backup", 543 "resume-relation", 544 "retry-provisioning", 545 "revoke", 546 "revoke-cloud", 547 "run", 548 "run-action", 549 "scale-application", 550 "scp", 551 "set-credential", 552 "set-constraints", 553 "set-default-credential", 554 "set-default-region", 555 "set-firewall-rule", 556 "set-meter-status", 557 "set-model-constraints", 558 "set-plan", 559 "set-series", 560 "set-wallet", 561 "show-action-output", 562 "show-action-status", 563 "show-application", 564 "show-backup", 565 "show-cloud", 566 "show-controller", 567 "show-credential", 568 "show-credentials", 569 "show-machine", 570 "show-model", 571 "show-offer", 572 "show-status", 573 "show-status-log", 574 "show-storage", 575 "show-user", 576 "show-wallet", 577 "sla", 578 "spaces", 579 "ssh", 580 "ssh-keys", 581 "status", 582 "storage", 583 "storage-pools", 584 "subnets", 585 "suspend-relation", 586 "switch", 587 "sync-agent-binaries", 588 "sync-tools", 589 "trust", 590 "unexpose", 591 "unregister", 592 "update-clouds", 593 "update-credential", 594 "update-storage-pool", 595 "upgrade-charm", 596 "upgrade-gui", 597 "upgrade-juju", 598 "upgrade-model", 599 "upgrade-series", 600 "upload-backup", 601 "users", 602 "version", 603 "wallets", 604 "whoami", 605 } 606 607 // devFeatures are feature flags that impact registration of commands. 608 var devFeatures = []string{ 609 // Currently no feature flags. 610 } 611 612 // These are the commands that are behind the `devFeatures`. 613 var commandNamesBehindFlags = set.NewStrings( 614 // Currently no commands behind feature flags. 615 ) 616 617 func (s *MainSuite) TestHelpCommands(c *gc.C) { 618 // Check that we have correctly registered all the commands 619 // by checking the help output. 620 // First check default commands, and then check commands that are 621 // activated by feature flags. 622 623 // remove features behind dev_flag for the first test 624 // since they are not enabled. 625 cmdSet := set.NewStrings(commandNames...) 626 627 // 1. Default Commands. Disable all features. 628 setFeatureFlags("") 629 // Use sorted values here so we can better see what is wrong. 630 registered := getHelpCommandNames(c) 631 unknown := registered.Difference(cmdSet) 632 c.Assert(unknown, jc.DeepEquals, set.NewStrings()) 633 missing := cmdSet.Difference(registered) 634 c.Assert(missing, jc.DeepEquals, set.NewStrings()) 635 636 // 2. Enable development features, and test again. 637 cmdSet = cmdSet.Union(commandNamesBehindFlags) 638 setFeatureFlags(strings.Join(devFeatures, ",")) 639 registered = getHelpCommandNames(c) 640 unknown = registered.Difference(cmdSet) 641 c.Assert(unknown, jc.DeepEquals, set.NewStrings()) 642 missing = cmdSet.Difference(registered) 643 c.Assert(missing, jc.DeepEquals, set.NewStrings()) 644 } 645 646 func getHelpCommandNames(c *gc.C) set.Strings { 647 out := badrun(c, 0, "help", "commands") 648 lines := strings.Split(out, "\n") 649 names := set.NewStrings() 650 for _, line := range lines { 651 f := strings.Fields(line) 652 if len(f) == 0 { 653 continue 654 } 655 names.Add(f[0]) 656 } 657 return names 658 } 659 660 func setFeatureFlags(flags string) { 661 if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil { 662 panic(err) 663 } 664 featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) 665 } 666 667 var globalFlags = []string{ 668 "--debug .*", 669 "--description .*", 670 "-h, --help .*", 671 "--log-file .*", 672 "--logging-config .*", 673 "-q, --quiet .*", 674 "--show-log .*", 675 "-v, --verbose .*", 676 } 677 678 func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) { 679 // Check that we have correctly registered all the topics 680 // by checking the help output. 681 out := badrun(c, 0, "help", "global-options") 682 c.Assert(out, gc.Matches, `Global Options 683 684 These options may be used with any command, and may appear in front of any 685 command\.(.|\n)*`) 686 lines := strings.Split(out, "\n") 687 var flags []string 688 for _, line := range lines { 689 f := strings.Fields(line) 690 if len(f) == 0 || line[0] != '-' { 691 continue 692 } 693 flags = append(flags, line) 694 } 695 c.Assert(len(flags), gc.Equals, len(globalFlags)) 696 for i, line := range flags { 697 c.Assert(line, gc.Matches, globalFlags[i]) 698 } 699 } 700 701 func (s *MainSuite) TestRegisterCommands(c *gc.C) { 702 stub := &gitjujutesting.Stub{} 703 extraNames := []string{"cmd-a", "cmd-b"} 704 for i := range extraNames { 705 name := extraNames[i] 706 RegisterCommand(func() cmd.Command { 707 return &stubCommand{ 708 stub: stub, 709 info: &cmd.Info{ 710 Name: name, 711 }, 712 } 713 }) 714 } 715 716 registry := &stubRegistry{stub: stub} 717 registry.names = append(registry.names, "help", "version") // implicit 718 registerCommands(registry, cmdtesting.Context(c)) 719 sort.Strings(registry.names) 720 721 expected := make([]string, len(commandNames)) 722 copy(expected, commandNames) 723 expected = append(expected, extraNames...) 724 sort.Strings(expected) 725 c.Check(registry.names, jc.DeepEquals, expected) 726 } 727 728 type commands []cmd.Command 729 730 func (r *commands) Register(c cmd.Command) { 731 *r = append(*r, c) 732 } 733 734 func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) { 735 if !check.Obsolete() { 736 *r = append(*r, c) 737 } 738 } 739 740 func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { 741 // Do nothing. 742 } 743 744 func (s *MainSuite) TestModelCommands(c *gc.C) { 745 var commands commands 746 registerCommands(&commands, cmdtesting.Context(c)) 747 // There should not be any ModelCommands registered. 748 // ModelCommands must be wrapped using modelcmd.Wrap. 749 for _, cmd := range commands { 750 c.Logf("%v", cmd.Info().Name) 751 c.Check(cmd, gc.Not(gc.FitsTypeOf), modelcmd.ModelCommand(&bootstrapCommand{})) 752 } 753 } 754 755 func (s *MainSuite) TestAllCommandsPurpose(c *gc.C) { 756 // Verify each command that: 757 // - the Purpose field is not empty. 758 // - the Purpose ends with a full stop. 759 // - if set, the Doc field either begins with the name of the 760 // command or and uppercase letter. 761 // 762 // This: 763 // - makes Purpose a required documentation. 764 // - Standardises Purpose formatting across all commands. 765 // - Brings "help commands"'s output in line with "help <cmd>"'s header. 766 // - Makes the Doc content either start like a sentence, or start 767 // godoc-like by using the command's name in lowercase. 768 var commands commands 769 registerCommands(&commands, cmdtesting.Context(c)) 770 for _, cmd := range commands { 771 info := cmd.Info() 772 purpose := strings.TrimSpace(info.Purpose) 773 doc := strings.TrimSpace(info.Doc) 774 comment := func(message string) interface{} { 775 return gc.Commentf("command %q %s", info.Name, message) 776 } 777 778 c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose")) 779 if purpose != "" { 780 prefix := string(purpose[0]) 781 c.Check(prefix, gc.Equals, strings.ToUpper(prefix), 782 comment("expected uppercase first-letter Purpose")) 783 c.Check(strings.HasSuffix(purpose, "."), jc.IsTrue, 784 comment("is missing full stop in Purpose")) 785 } 786 if doc != "" && !strings.HasPrefix(doc, info.Name) { 787 prefix := string(doc[0]) 788 c.Check(prefix, gc.Equals, strings.ToUpper(prefix), 789 comment("expected uppercase first-letter Doc"), 790 ) 791 } 792 } 793 }