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