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  }