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