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