github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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-ssh-keys", 455 "list-spaces", 456 "list-storage", 457 "list-storage-pools", 458 "list-subnets", 459 "list-users", 460 "login", 461 "logout", 462 "machines", 463 "metrics", 464 "model-config", 465 "model-defaults", 466 "models", 467 "plans", 468 "register", 469 "relate", //alias for add-relation 470 "remove-application", 471 "remove-backup", 472 "remove-cached-images", 473 "remove-cloud", 474 "remove-credential", 475 "remove-machine", 476 "remove-relation", 477 "remove-ssh-key", 478 "remove-unit", 479 "resolved", 480 "restore-backup", 481 "retry-provisioning", 482 "revoke", 483 "run", 484 "run-action", 485 "scp", 486 "set-budget", 487 "set-constraints", 488 "set-default-credential", 489 "set-default-region", 490 "set-meter-status", 491 "set-model-constraints", 492 "set-plan", 493 "show-action-output", 494 "show-action-status", 495 "show-backup", 496 "show-budget", 497 "show-cloud", 498 "show-controller", 499 "show-machine", 500 "show-model", 501 "show-status", 502 "show-status-log", 503 "show-storage", 504 "show-user", 505 "spaces", 506 "ssh", 507 "ssh-keys", 508 "status", 509 "storage", 510 "storage-pools", 511 "subnets", 512 "switch", 513 "sync-tools", 514 "unexpose", 515 "update-allocation", 516 "upload-backup", 517 "unregister", 518 "update-clouds", 519 "upgrade-charm", 520 "upgrade-gui", 521 "upgrade-juju", 522 "users", 523 "version", 524 "whoami", 525 } 526 527 // devFeatures are feature flags that impact registration of commands. 528 var devFeatures = []string{feature.Migration} 529 530 // These are the commands that are behind the `devFeatures`. 531 var commandNamesBehindFlags = set.NewStrings( 532 "migrate", 533 ) 534 535 func (s *MainSuite) TestHelpCommands(c *gc.C) { 536 // Check that we have correctly registered all the commands 537 // by checking the help output. 538 // First check default commands, and then check commands that are 539 // activated by feature flags. 540 541 // remove features behind dev_flag for the first test 542 // since they are not enabled. 543 cmdSet := set.NewStrings(commandNames...) 544 545 // 1. Default Commands. Disable all features. 546 setFeatureFlags("") 547 // Use sorted values here so we can better see what is wrong. 548 registered := getHelpCommandNames(c) 549 unknown := registered.Difference(cmdSet) 550 c.Assert(unknown, jc.DeepEquals, set.NewStrings()) 551 missing := cmdSet.Difference(registered) 552 c.Assert(missing, jc.DeepEquals, set.NewStrings()) 553 554 // 2. Enable development features, and test again. 555 cmdSet = cmdSet.Union(commandNamesBehindFlags) 556 setFeatureFlags(strings.Join(devFeatures, ",")) 557 registered = getHelpCommandNames(c) 558 unknown = registered.Difference(cmdSet) 559 c.Assert(unknown, jc.DeepEquals, set.NewStrings()) 560 missing = cmdSet.Difference(registered) 561 c.Assert(missing, jc.DeepEquals, set.NewStrings()) 562 } 563 564 func getHelpCommandNames(c *gc.C) set.Strings { 565 out := badrun(c, 0, "help", "commands") 566 lines := strings.Split(out, "\n") 567 names := set.NewStrings() 568 for _, line := range lines { 569 f := strings.Fields(line) 570 if len(f) == 0 { 571 continue 572 } 573 names.Add(f[0]) 574 } 575 return names 576 } 577 578 func setFeatureFlags(flags string) { 579 if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil { 580 panic(err) 581 } 582 featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) 583 } 584 585 var globalFlags = []string{ 586 "--debug .*", 587 "--description .*", 588 "-h, --help .*", 589 "--log-file .*", 590 "--logging-config .*", 591 "-q, --quiet .*", 592 "--show-log .*", 593 "-v, --verbose .*", 594 } 595 596 func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) { 597 // Check that we have correctly registered all the topics 598 // by checking the help output. 599 out := badrun(c, 0, "help", "global-options") 600 c.Assert(out, gc.Matches, `Global Options 601 602 These options may be used with any command, and may appear in front of any 603 command\.(.|\n)*`) 604 lines := strings.Split(out, "\n") 605 var flags []string 606 for _, line := range lines { 607 f := strings.Fields(line) 608 if len(f) == 0 || line[0] != '-' { 609 continue 610 } 611 flags = append(flags, line) 612 } 613 c.Assert(len(flags), gc.Equals, len(globalFlags)) 614 for i, line := range flags { 615 c.Assert(line, gc.Matches, globalFlags[i]) 616 } 617 } 618 619 func (s *MainSuite) TestRegisterCommands(c *gc.C) { 620 stub := &gitjujutesting.Stub{} 621 extraNames := []string{"cmd-a", "cmd-b"} 622 for i := range extraNames { 623 name := extraNames[i] 624 RegisterCommand(func() cmd.Command { 625 return &stubCommand{ 626 stub: stub, 627 info: &cmd.Info{ 628 Name: name, 629 }, 630 } 631 }) 632 } 633 634 registry := &stubRegistry{stub: stub} 635 registry.names = append(registry.names, "help", "version") // implicit 636 registerCommands(registry, testing.Context(c)) 637 sort.Strings(registry.names) 638 639 expected := make([]string, len(commandNames)) 640 copy(expected, commandNames) 641 expected = append(expected, extraNames...) 642 sort.Strings(expected) 643 c.Check(registry.names, jc.DeepEquals, expected) 644 } 645 646 type commands []cmd.Command 647 648 func (r *commands) Register(c cmd.Command) { 649 *r = append(*r, c) 650 } 651 652 func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) { 653 if !check.Obsolete() { 654 *r = append(*r, c) 655 } 656 } 657 658 func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { 659 // Do nothing. 660 } 661 662 func (s *MainSuite) TestModelCommands(c *gc.C) { 663 var commands commands 664 registerCommands(&commands, testing.Context(c)) 665 // There should not be any ModelCommands registered. 666 // ModelCommands must be wrapped using modelcmd.Wrap. 667 for _, cmd := range commands { 668 c.Logf("%v", cmd.Info().Name) 669 c.Check(cmd, gc.Not(gc.FitsTypeOf), modelcmd.ModelCommand(&bootstrapCommand{})) 670 } 671 } 672 673 func (s *MainSuite) TestAllCommandsPurposeDocCapitalization(c *gc.C) { 674 // Verify each command that: 675 // - the Purpose field is not empty 676 // - if set, the Doc field either begins with the name of the 677 // command or and uppercase letter. 678 // 679 // The first makes Purpose a required documentation. Also, makes 680 // both "help commands"'s output and "help <cmd>"'s header more 681 // uniform. The second makes the Doc content either start like a 682 // sentence, or start godoc-like by using the command's name in 683 // lowercase. 684 var commands commands 685 registerCommands(&commands, testing.Context(c)) 686 for _, cmd := range commands { 687 info := cmd.Info() 688 c.Logf("%v", info.Name) 689 purpose := strings.TrimSpace(info.Purpose) 690 doc := strings.TrimSpace(info.Doc) 691 comment := func(message string) interface{} { 692 return gc.Commentf("command %q %s", info.Name, message) 693 } 694 695 c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose")) 696 if purpose != "" { 697 prefix := string(purpose[0]) 698 c.Check(prefix, gc.Equals, strings.ToUpper(prefix), 699 comment("expected uppercase first-letter Purpose"), 700 ) 701 } 702 if doc != "" && !strings.HasPrefix(doc, info.Name) { 703 prefix := string(doc[0]) 704 c.Check(prefix, gc.Equals, strings.ToUpper(prefix), 705 comment("expected uppercase first-letter Doc"), 706 ) 707 } 708 } 709 }