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