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  }