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  }