github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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 "io/ioutil" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strings" 12 13 "github.com/juju/cmd" 14 jc "github.com/juju/testing/checkers" 15 "github.com/juju/utils/featureflag" 16 "github.com/juju/utils/set" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/cmd/envcmd" 20 "github.com/juju/juju/cmd/juju/block" 21 "github.com/juju/juju/cmd/juju/helptopics" 22 "github.com/juju/juju/cmd/juju/service" 23 cmdtesting "github.com/juju/juju/cmd/testing" 24 "github.com/juju/juju/juju/osenv" 25 _ "github.com/juju/juju/provider/dummy" 26 "github.com/juju/juju/testing" 27 "github.com/juju/juju/version" 28 ) 29 30 type MainSuite struct { 31 testing.FakeJujuHomeSuite 32 } 33 34 var _ = gc.Suite(&MainSuite{}) 35 36 func deployHelpText() string { 37 return cmdtesting.HelpText(envcmd.Wrap(&DeployCommand{}), "juju deploy") 38 } 39 40 func setHelpText() string { 41 return cmdtesting.HelpText(envcmd.Wrap(&service.SetCommand{}), "juju service set") 42 } 43 44 func syncToolsHelpText() string { 45 return cmdtesting.HelpText(envcmd.Wrap(&SyncToolsCommand{}), "juju sync-tools") 46 } 47 48 func blockHelpText() string { 49 return cmdtesting.HelpText(block.NewSuperBlockCommand(), "juju block") 50 } 51 52 func (s *MainSuite) TestRunMain(c *gc.C) { 53 // The test array structure needs to be inline here as some of the 54 // expected values below use deployHelpText(). This constructs the deploy 55 // command and runs gets the help for it. When the deploy command is 56 // setting the flags (which is needed for the help text) it is accessing 57 // osenv.JujuHome(), which panics if SetJujuHome has not been called. 58 // The FakeHome from testing does this. 59 for i, t := range []struct { 60 summary string 61 args []string 62 code int 63 out string 64 }{{ 65 summary: "no params shows help", 66 args: []string{}, 67 code: 0, 68 out: strings.TrimLeft(helptopics.Basics, "\n"), 69 }, { 70 summary: "juju help is the same as juju", 71 args: []string{"help"}, 72 code: 0, 73 out: strings.TrimLeft(helptopics.Basics, "\n"), 74 }, { 75 summary: "juju --help works too", 76 args: []string{"--help"}, 77 code: 0, 78 out: strings.TrimLeft(helptopics.Basics, "\n"), 79 }, { 80 summary: "juju help basics is the same as juju", 81 args: []string{"help", "basics"}, 82 code: 0, 83 out: strings.TrimLeft(helptopics.Basics, "\n"), 84 }, { 85 summary: "juju help foo doesn't exist", 86 args: []string{"help", "foo"}, 87 code: 1, 88 out: "ERROR unknown command or topic for foo\n", 89 }, { 90 summary: "juju help deploy shows the default help without global options", 91 args: []string{"help", "deploy"}, 92 code: 0, 93 out: deployHelpText(), 94 }, { 95 summary: "juju --help deploy shows the same help as 'help deploy'", 96 args: []string{"--help", "deploy"}, 97 code: 0, 98 out: deployHelpText(), 99 }, { 100 summary: "juju deploy --help shows the same help as 'help deploy'", 101 args: []string{"deploy", "--help"}, 102 code: 0, 103 out: deployHelpText(), 104 }, { 105 summary: "juju help set shows the default help without global options", 106 args: []string{"help", "set"}, 107 code: 0, 108 out: setHelpText(), 109 }, { 110 summary: "juju --help set shows the same help as 'help set'", 111 args: []string{"--help", "set"}, 112 code: 0, 113 out: setHelpText(), 114 }, { 115 summary: "juju set --help shows the same help as 'help set'", 116 args: []string{"set", "--help"}, 117 code: 0, 118 out: setHelpText(), 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: flag provided but not defined: --cheese\n", 129 }, { 130 summary: "unknown option after command", 131 args: []string{"bootstrap", "--cheese"}, 132 code: 2, 133 out: "error: flag provided but not defined: --cheese\n", 134 }, { 135 summary: "known option, but specified before command", 136 args: []string{"--environment", "blah", "bootstrap"}, 137 code: 2, 138 out: "error: flag provided but not defined: --environment\n", 139 }, { 140 summary: "juju sync-tools registered properly", 141 args: []string{"sync-tools", "--help"}, 142 code: 0, 143 out: syncToolsHelpText(), 144 }, { 145 summary: "check version command registered properly", 146 args: []string{"version"}, 147 code: 0, 148 out: version.Current.String() + "\n", 149 }, { 150 summary: "check block command registered properly", 151 args: []string{"block", "-h"}, 152 code: 0, 153 out: blockHelpText(), 154 }, { 155 summary: "check unblock command registered properly", 156 args: []string{"unblock"}, 157 code: 0, 158 out: "error: must specify one of [destroy-environment | remove-object | all-changes] to unblock\n", 159 }, 160 } { 161 c.Logf("test %d: %s", i, t.summary) 162 out := badrun(c, t.code, t.args...) 163 c.Assert(out, gc.Equals, t.out) 164 } 165 } 166 167 func (s *MainSuite) TestActualRunJujuArgOrder(c *gc.C) { 168 //TODO(bogdanteleaga): cannot read the env file because of some suite 169 //problems. The juju home, when calling something from the command line is 170 //not the same as in the test suite. 171 if runtime.GOOS == "windows" { 172 c.Skip("bug 1403084: cannot read env file on windows because of suite problems") 173 } 174 logpath := filepath.Join(c.MkDir(), "log") 175 tests := [][]string{ 176 {"--log-file", logpath, "--debug", "env"}, // global flags before 177 {"env", "--log-file", logpath, "--debug"}, // after 178 {"--log-file", logpath, "env", "--debug"}, // mixed 179 } 180 for i, test := range tests { 181 c.Logf("test %d: %v", i, test) 182 badrun(c, 0, test...) 183 content, err := ioutil.ReadFile(logpath) 184 c.Assert(err, jc.ErrorIsNil) 185 c.Assert(string(content), gc.Matches, "(.|\n)*running juju(.|\n)*command finished(.|\n)*") 186 err = os.Remove(logpath) 187 c.Assert(err, jc.ErrorIsNil) 188 } 189 } 190 191 var commandNames = []string{ 192 "action", 193 "add-machine", 194 "add-relation", 195 "add-unit", 196 "api-endpoints", 197 "api-info", 198 "authorised-keys", // alias for authorized-keys 199 "authorized-keys", 200 "backups", 201 "block", 202 "bootstrap", 203 "cached-images", 204 "debug-hooks", 205 "debug-log", 206 "deploy", 207 "destroy-environment", 208 "destroy-machine", 209 "destroy-relation", 210 "destroy-service", 211 "destroy-unit", 212 "ensure-availability", 213 "env", // alias for switch 214 "environment", 215 "expose", 216 "generate-config", // alias for init 217 "get", 218 "get-constraints", 219 "get-env", // alias for get-environment 220 "get-environment", 221 "help", 222 "help-tool", 223 "init", 224 "machine", 225 "publish", 226 "remove-machine", // alias for destroy-machine 227 "remove-relation", // alias for destroy-relation 228 "remove-service", // alias for destroy-service 229 "remove-unit", // alias for destroy-unit 230 "resolved", 231 "retry-provisioning", 232 "run", 233 "scp", 234 "service", 235 "set", 236 "set-constraints", 237 "set-env", // alias for set-environment 238 "set-environment", 239 "space", 240 "ssh", 241 "stat", // alias for status 242 "status", 243 "status-history", 244 "storage", 245 "subnet", 246 "switch", 247 "sync-tools", 248 "terminate-machine", // alias for destroy-machine 249 "unblock", 250 "unexpose", 251 "unset", 252 "unset-env", // alias for unset-environment 253 "unset-environment", 254 "upgrade-charm", 255 "upgrade-juju", 256 "user", 257 "version", 258 } 259 260 func (s *MainSuite) TestHelpCommands(c *gc.C) { 261 defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) 262 263 // Check that we have correctly registered all the commands 264 // by checking the help output. 265 // First check default commands, and then check commands that are 266 // activated by feature flags. 267 268 // Here we can add feature flags for any commands we want to hide by default. 269 devFeatures := []string{} 270 271 // remove features behind dev_flag for the first test 272 // since they are not enabled. 273 cmdSet := set.NewStrings(commandNames...) 274 for _, feature := range devFeatures { 275 cmdSet.Remove(feature) 276 } 277 278 // 1. Default Commands. Disable all features. 279 setFeatureFlags("") 280 c.Assert(getHelpCommandNames(c), jc.SameContents, cmdSet.Values()) 281 282 // 2. Enable development features, and test again. 283 setFeatureFlags(strings.Join(devFeatures, ",")) 284 c.Assert(getHelpCommandNames(c), jc.SameContents, commandNames) 285 } 286 287 func getHelpCommandNames(c *gc.C) []string { 288 out := badrun(c, 0, "help", "commands") 289 lines := strings.Split(out, "\n") 290 var names []string 291 for _, line := range lines { 292 f := strings.Fields(line) 293 if len(f) == 0 { 294 continue 295 } 296 names = append(names, f[0]) 297 } 298 return names 299 } 300 301 func setFeatureFlags(flags string) { 302 if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil { 303 panic(err) 304 } 305 featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) 306 } 307 308 var topicNames = []string{ 309 "azure-provider", 310 "basics", 311 "commands", 312 "constraints", 313 "ec2-provider", 314 "global-options", 315 "glossary", 316 "hpcloud-provider", 317 "juju", 318 "juju-systems", 319 "local-provider", 320 "logging", 321 "maas-provider", 322 "openstack-provider", 323 "placement", 324 "plugins", 325 "topics", 326 "users", 327 } 328 329 func (s *MainSuite) TestHelpTopics(c *gc.C) { 330 // Check that we have correctly registered all the topics 331 // by checking the help output. 332 defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) 333 out := badrun(c, 0, "help", "topics") 334 lines := strings.Split(out, "\n") 335 var names []string 336 for _, line := range lines { 337 f := strings.Fields(line) 338 if len(f) == 0 { 339 continue 340 } 341 names = append(names, f[0]) 342 } 343 // The names should be output in alphabetical order, so don't sort. 344 c.Assert(names, gc.DeepEquals, topicNames) 345 } 346 347 var globalFlags = []string{ 348 "--debug .*", 349 "--description .*", 350 "-h, --help .*", 351 "--log-file .*", 352 "--logging-config .*", 353 "-q, --quiet .*", 354 "--show-log .*", 355 "-v, --verbose .*", 356 } 357 358 func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) { 359 // Check that we have correctly registered all the topics 360 // by checking the help output. 361 defer osenv.SetJujuHome(osenv.SetJujuHome(c.MkDir())) 362 out := badrun(c, 0, "help", "global-options") 363 c.Assert(out, gc.Matches, `Global Options 364 365 These options may be used with any command, and may appear in front of any 366 command\.(.|\n)*`) 367 lines := strings.Split(out, "\n") 368 var flags []string 369 for _, line := range lines { 370 f := strings.Fields(line) 371 if len(f) == 0 || line[0] != '-' { 372 continue 373 } 374 flags = append(flags, line) 375 } 376 c.Assert(len(flags), gc.Equals, len(globalFlags)) 377 for i, line := range flags { 378 c.Assert(line, gc.Matches, globalFlags[i]) 379 } 380 } 381 382 type commands []cmd.Command 383 384 func (r *commands) Register(c cmd.Command) { 385 *r = append(*r, c) 386 } 387 388 func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) { 389 if !check.Obsolete() { 390 *r = append(*r, c) 391 } 392 } 393 394 func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) { 395 // Do nothing. 396 } 397 398 func (s *MainSuite) TestEnvironCommands(c *gc.C) { 399 var commands commands 400 registerCommands(&commands, testing.Context(c)) 401 // There should not be any EnvironCommands registered. 402 // EnvironCommands must be wrapped using envcmd.Wrap. 403 for _, cmd := range commands { 404 c.Logf("%v", cmd.Info().Name) 405 c.Check(cmd, gc.Not(gc.FitsTypeOf), envcmd.EnvironCommand(&BootstrapCommand{})) 406 } 407 } 408 409 func (s *MainSuite) TestAllCommandsPurposeDocCapitalization(c *gc.C) { 410 // Verify each command that: 411 // - the Purpose field is not empty and begins with a lowercase 412 // letter, and, 413 // - if set, the Doc field either begins with the name of the 414 // command or and uppercase letter. 415 // 416 // The first makes Purpose a required documentation. Also, makes 417 // both "help commands"'s output and "help <cmd>"'s header more 418 // uniform. The second makes the Doc content either start like a 419 // sentence, or start godoc-like by using the command's name in 420 // lowercase. 421 var commands commands 422 registerCommands(&commands, testing.Context(c)) 423 for _, cmd := range commands { 424 info := cmd.Info() 425 c.Logf("%v", info.Name) 426 purpose := strings.TrimSpace(info.Purpose) 427 doc := strings.TrimSpace(info.Doc) 428 comment := func(message string) interface{} { 429 return gc.Commentf("command %q %s", info.Name, message) 430 } 431 432 c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose")) 433 if purpose != "" { 434 prefix := string(purpose[0]) 435 c.Check(prefix, gc.Equals, strings.ToLower(prefix), 436 comment("expected lowercase first-letter Purpose"), 437 ) 438 } 439 if doc != "" && !strings.HasPrefix(doc, info.Name) { 440 prefix := string(doc[0]) 441 c.Check(prefix, gc.Equals, strings.ToUpper(prefix), 442 comment("expected uppercase first-letter Doc"), 443 ) 444 } 445 } 446 } 447 448 func (s *MainSuite) TestTwoDotOhDeprecation(c *gc.C) { 449 check := twoDotOhDeprecation("the replacement") 450 451 // first check pre-2.0 452 s.PatchValue(&version.Current.Number, version.MustParse("1.26.4")) 453 deprecated, replacement := check.Deprecated() 454 c.Check(deprecated, jc.IsFalse) 455 c.Check(replacement, gc.Equals, "") 456 c.Check(check.Obsolete(), jc.IsFalse) 457 458 s.PatchValue(&version.Current.Number, version.MustParse("2.0-alpha1")) 459 deprecated, replacement = check.Deprecated() 460 c.Check(deprecated, jc.IsTrue) 461 c.Check(replacement, gc.Equals, "the replacement") 462 c.Check(check.Obsolete(), jc.IsFalse) 463 464 s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) 465 deprecated, replacement = check.Deprecated() 466 c.Check(deprecated, jc.IsTrue) 467 c.Check(replacement, gc.Equals, "the replacement") 468 c.Check(check.Obsolete(), jc.IsTrue) 469 } 470 471 // obsoleteCommandNames is the list of commands that are deprecated in 472 // 2.0, and obsolete in 3.0 473 var obsoleteCommandNames = []string{ 474 "add-machine", 475 "destroy-machine", 476 "get-constraints", 477 "get-env", 478 "get-environment", 479 "remove-machine", 480 "retry-provisioning", 481 "set-constraints", 482 "set-env", 483 "set-environment", 484 "terminate-machine", 485 "unset-env", 486 "unset-environment", 487 } 488 489 func (s *MainSuite) TestObsoleteRegistration(c *gc.C) { 490 var commands commands 491 s.PatchValue(&version.Current.Number, version.MustParse("3.0-alpha1")) 492 registerCommands(&commands, testing.Context(c)) 493 494 cmdSet := set.NewStrings(obsoleteCommandNames...) 495 registeredCmdSet := set.NewStrings() 496 for _, cmd := range commands { 497 registeredCmdSet.Add(cmd.Info().Name) 498 } 499 500 intersection := registeredCmdSet.Intersection(cmdSet) 501 c.Logf("Registered obsolete commands: %s", intersection.Values()) 502 c.Assert(intersection.IsEmpty(), gc.Equals, true) 503 }