github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/juju/cmd"
    17  	"github.com/juju/cmd/cmdtesting"
    18  	"github.com/juju/collections/set"
    19  	"github.com/juju/gnuflag"
    20  	"github.com/juju/os/series"
    21  	gitjujutesting "github.com/juju/testing"
    22  	jc "github.com/juju/testing/checkers"
    23  	"github.com/juju/utils/arch"
    24  	"github.com/juju/utils/featureflag"
    25  	"github.com/juju/version"
    26  	gc "gopkg.in/check.v1"
    27  
    28  	jujucmd "github.com/juju/juju/cmd"
    29  	"github.com/juju/juju/cmd/juju/application"
    30  	"github.com/juju/juju/cmd/juju/cloud"
    31  	"github.com/juju/juju/cmd/modelcmd"
    32  	"github.com/juju/juju/juju/osenv"
    33  	"github.com/juju/juju/jujuclient/jujuclienttesting"
    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  	gitjujutesting.PatchExecHelper
    42  }
    43  
    44  var _ = gc.Suite(&MainSuite{})
    45  
    46  func helpText(command cmd.Command, name string) string {
    47  	buff := &bytes.Buffer{}
    48  	info := command.Info()
    49  	info.Name = name
    50  	f := gnuflag.NewFlagSetWithFlagKnownAs(info.Name, gnuflag.ContinueOnError, cmd.FlagAlias(command, "option"))
    51  	command.SetFlags(f)
    52  
    53  	superJuju := jujucmd.NewSuperCommand(cmd.SuperCommandParams{
    54  		Name:        "juju",
    55  		FlagKnownAs: "option",
    56  	})
    57  	superF := gnuflag.NewFlagSetWithFlagKnownAs("juju", gnuflag.ContinueOnError, "option")
    58  	superJuju.SetFlags(superF)
    59  
    60  	buff.Write(info.HelpWithSuperFlags(superF, f))
    61  	return buff.String()
    62  }
    63  
    64  func deployHelpText() string {
    65  	return helpText(application.NewDeployCommand(), "juju deploy")
    66  }
    67  func configHelpText() string {
    68  	return helpText(application.NewConfigCommand(), "juju config")
    69  }
    70  
    71  func syncToolsHelpText() string {
    72  	return helpText(newSyncToolsCommand(), "juju sync-agent-binaries")
    73  }
    74  
    75  func (s *MainSuite) TestRunMain(c *gc.C) {
    76  	jujuclienttesting.SetupMinimalFileStore(c)
    77  
    78  	// The test array structure needs to be inline here as some of the
    79  	// expected values below use deployHelpText().  This constructs the deploy
    80  	// command and runs gets the help for it.  When the deploy command is
    81  	// setting the flags (which is needed for the help text) it is accessing
    82  	// osenv.JujuXDGDataHome(), which panics if SetJujuXDGDataHome has not been called.
    83  	// The FakeHome from testing does this.
    84  	for i, t := range []struct {
    85  		summary string
    86  		args    []string
    87  		code    int
    88  		out     string
    89  	}{{
    90  		summary: "juju help foo doesn't exist",
    91  		args:    []string{"help", "foo"},
    92  		code:    1,
    93  		out:     "ERROR unknown command or topic for foo\n",
    94  	}, {
    95  		summary: "juju help deploy shows the default help without global options",
    96  		args:    []string{"help", "deploy"},
    97  		code:    0,
    98  		out:     deployHelpText(),
    99  	}, {
   100  		summary: "juju --help deploy shows the same help as 'help deploy'",
   101  		args:    []string{"--help", "deploy"},
   102  		code:    0,
   103  		out:     deployHelpText(),
   104  	}, {
   105  		summary: "juju deploy --help shows the same help as 'help deploy'",
   106  		args:    []string{"deploy", "--help"},
   107  		code:    0,
   108  		out:     deployHelpText(),
   109  	}, {
   110  		summary: "juju --help config shows the same help as 'help config'",
   111  		args:    []string{"--help", "config"},
   112  		code:    0,
   113  		out:     configHelpText(),
   114  	}, {
   115  		summary: "juju config --help shows the same help as 'help config'",
   116  		args:    []string{"config", "--help"},
   117  		code:    0,
   118  		out:     configHelpText(),
   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 option provided but not defined: --cheese\n",
   129  	}, {
   130  		summary: "unknown option after command",
   131  		args:    []string{"bootstrap", "--cheese"},
   132  		code:    2,
   133  		out:     "ERROR option provided but not defined: --cheese\n",
   134  	}, {
   135  		summary: "known option, but specified before command",
   136  		args:    []string{"--model", "blah", "bootstrap"},
   137  		code:    2,
   138  		out:     "ERROR option provided but not defined: --model\n",
   139  	}, {
   140  		summary: "juju sync-agent-binaries registered properly",
   141  		args:    []string{"sync-agent-binaries", "--help"},
   142  		code:    0,
   143  		out:     syncToolsHelpText(),
   144  	}, {
   145  		summary: "check version command returns a fully qualified version string",
   146  		args:    []string{"version"},
   147  		code:    0,
   148  		out: version.Binary{
   149  			Number: jujuversion.Current,
   150  			Arch:   arch.HostArch(),
   151  			Series: series.MustHostSeries(),
   152  		}.String() + "\n",
   153  	}} {
   154  		c.Logf("test %d: %s", i, t.summary)
   155  		out := badrun(c, t.code, t.args...)
   156  		c.Assert(out, gc.Equals, t.out)
   157  	}
   158  }
   159  
   160  func (s *MainSuite) TestActualRunJujuArgOrder(c *gc.C) {
   161  	//TODO(bogdanteleaga): cannot read the env file because of some suite
   162  	//problems. The juju home, when calling something from the command line is
   163  	//not the same as in the test suite.
   164  	if runtime.GOOS == "windows" {
   165  		c.Skip("bug 1403084: cannot read env file on windows because of suite problems")
   166  	}
   167  	s.PatchEnvironment(osenv.JujuControllerEnvKey, "current-controller")
   168  	s.PatchEnvironment(osenv.JujuModelEnvKey, "current")
   169  	logpath := filepath.Join(c.MkDir(), "log")
   170  	tests := [][]string{
   171  		{"--log-file", logpath, "--debug", "help"}, // global flags before
   172  		{"help", "--log-file", logpath, "--debug"}, // after
   173  		{"--log-file", logpath, "help", "--debug"}, // mixed
   174  	}
   175  	for i, test := range tests {
   176  		c.Logf("test %d: %v", i, test)
   177  		badrun(c, 0, test...)
   178  		content, err := ioutil.ReadFile(logpath)
   179  		c.Assert(err, jc.ErrorIsNil)
   180  		c.Assert(string(content), gc.Matches, "(.|\n)*running juju(.|\n)*command finished(.|\n)*")
   181  		err = os.Remove(logpath)
   182  		c.Assert(err, jc.ErrorIsNil)
   183  	}
   184  }
   185  
   186  func (s *MainSuite) TestFirstRun2xFrom1xOnUbuntu(c *gc.C) {
   187  	if runtime.GOOS == "windows" {
   188  		// This test can't work on Windows and shouldn't need to
   189  		c.Skip("test doesn't work on Windows because Juju's 1.x and 2.x config directory are the same")
   190  	}
   191  
   192  	// Code should only run on ubuntu series, so patch out the series for
   193  	// when non-ubuntu OSes run this test.
   194  	s.PatchValue(&series.MustHostSeries, func() string { return "trusty" })
   195  
   196  	argChan := make(chan []string, 1)
   197  
   198  	execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{
   199  		Stdout: "1.25.0-trusty-amd64",
   200  		Args:   argChan,
   201  	})
   202  	stub := &gitjujutesting.Stub{}
   203  	s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command {
   204  		return &stubCommand{stub: stub}
   205  	})
   206  
   207  	// remove the new juju-home and create a fake old juju home.
   208  	err := os.RemoveAll(osenv.JujuXDGDataHomeDir())
   209  	c.Assert(err, jc.ErrorIsNil)
   210  	makeValidOldHome(c)
   211  
   212  	var code int
   213  	f := func() {
   214  		code = main{
   215  			execCommand: execCommand,
   216  		}.Run([]string{"juju", "version"})
   217  	}
   218  
   219  	stdout, stderr := gitjujutesting.CaptureOutput(c, f)
   220  
   221  	select {
   222  	case args := <-argChan:
   223  		c.Assert(args, gc.DeepEquals, []string{"juju-1", "version"})
   224  	default:
   225  		c.Fatalf("Exec function not called.")
   226  	}
   227  
   228  	c.Check(code, gc.Equals, 0)
   229  	c.Check(string(stderr), gc.Equals, fmt.Sprintf(`
   230  Welcome to Juju %s. 
   231      See https://jujucharms.com/docs/stable/introducing-2 for more details.
   232  
   233  If you want to use Juju 1.25.0, run 'juju' commands as 'juju-1'. For example, 'juju-1 bootstrap'.
   234     See https://jujucharms.com/docs/stable/juju-coexist for installation details. 
   235  
   236  Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n", jujuversion.Current))
   237  	checkVersionOutput(c, string(stdout))
   238  }
   239  
   240  func (s *MainSuite) TestFirstRun2xFrom1xNotUbuntu(c *gc.C) {
   241  	// Code should only run on ubuntu series, so pretend to be something else.
   242  	s.PatchValue(&series.MustHostSeries, func() string { return "win8" })
   243  
   244  	argChan := make(chan []string, 1)
   245  
   246  	// we shouldn't actually be running anything, but if we do, this will
   247  	// provide some consistent results.
   248  	execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{
   249  		Stdout: "1.25.0-trusty-amd64",
   250  		Args:   argChan,
   251  	})
   252  	stub := &gitjujutesting.Stub{}
   253  	s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command {
   254  		return &stubCommand{stub: stub}
   255  	})
   256  
   257  	// remove the new juju-home and create a fake old juju home.
   258  	err := os.RemoveAll(osenv.JujuXDGDataHomeDir())
   259  	c.Assert(err, jc.ErrorIsNil)
   260  
   261  	makeValidOldHome(c)
   262  
   263  	var code int
   264  	stdout, stderr := gitjujutesting.CaptureOutput(c, func() {
   265  		code = main{
   266  			execCommand: execCommand,
   267  		}.Run([]string{"juju", "version"})
   268  	})
   269  
   270  	c.Assert(code, gc.Equals, 0)
   271  
   272  	assertNoArgs(c, argChan)
   273  
   274  	c.Check(string(stderr), gc.Equals, `
   275  Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n")
   276  	checkVersionOutput(c, string(stdout))
   277  }
   278  
   279  func (s *MainSuite) TestNoWarn1xWith2xData(c *gc.C) {
   280  	// Code should only rnu on ubuntu series, so patch out the series for
   281  	// when non-ubuntu OSes run this test.
   282  	s.PatchValue(&series.MustHostSeries, func() string { return "trusty" })
   283  
   284  	argChan := make(chan []string, 1)
   285  
   286  	// we shouldn't actually be running anything, but if we do, this will
   287  	// provide some consistent results.
   288  	execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{
   289  		Stdout: "1.25.0-trusty-amd64",
   290  		Args:   argChan,
   291  	})
   292  
   293  	// there should be a 2x home directory already created by the test setup.
   294  
   295  	// create a fake old juju home.
   296  	makeValidOldHome(c)
   297  
   298  	var code int
   299  	stdout, stderr := gitjujutesting.CaptureOutput(c, func() {
   300  		code = main{
   301  			execCommand: execCommand,
   302  		}.Run([]string{"juju", "version"})
   303  	})
   304  
   305  	c.Assert(code, gc.Equals, 0)
   306  
   307  	assertNoArgs(c, argChan)
   308  	c.Assert(string(stderr), gc.Equals, "")
   309  	checkVersionOutput(c, string(stdout))
   310  }
   311  
   312  func (s *MainSuite) TestNoWarnWithNo1xOr2xData(c *gc.C) {
   313  	// Code should only rnu on ubuntu series, so patch out the series for
   314  	// when non-ubuntu OSes run this test.
   315  	s.PatchValue(&series.MustHostSeries, func() string { return "trusty" })
   316  
   317  	argChan := make(chan []string, 1)
   318  	// we shouldn't actually be running anything, but if we do, this will
   319  	// provide some consistent results.
   320  	execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{
   321  		Stdout: "1.25.0-trusty-amd64",
   322  		Args:   argChan,
   323  	})
   324  	stub := &gitjujutesting.Stub{}
   325  	s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command {
   326  		return &stubCommand{stub: stub}
   327  	})
   328  
   329  	// remove the new juju-home.
   330  	err := os.RemoveAll(osenv.JujuXDGDataHomeDir())
   331  	c.Assert(err, jc.ErrorIsNil)
   332  
   333  	// create fake (empty) old juju home.
   334  	path := c.MkDir()
   335  	s.PatchEnvironment("JUJU_HOME", path)
   336  
   337  	var code int
   338  	stdout, stderr := gitjujutesting.CaptureOutput(c, func() {
   339  		code = main{
   340  			execCommand: execCommand,
   341  		}.Run([]string{"juju", "version"})
   342  	})
   343  
   344  	c.Assert(code, gc.Equals, 0)
   345  
   346  	assertNoArgs(c, argChan)
   347  	c.Check(string(stderr), gc.Equals, `
   348  Since Juju 2 is being run for the first time, downloading latest cloud information.`[1:]+"\n")
   349  	checkVersionOutput(c, string(stdout))
   350  }
   351  
   352  func (s *MainSuite) assertRunCommandUpdateCloud(c *gc.C, expectedCall string) {
   353  	argChan := make(chan []string, 1)
   354  	execCommand := s.GetExecCommand(gitjujutesting.PatchExecConfig{
   355  		Stdout: "1.25.0-trusty-amd64",
   356  		Args:   argChan,
   357  	})
   358  
   359  	stub := &gitjujutesting.Stub{}
   360  	s.PatchValue(&cloud.NewUpdateCloudsCommand, func() cmd.Command {
   361  		return &stubCommand{stub: stub}
   362  
   363  	})
   364  	var code int
   365  	gitjujutesting.CaptureOutput(c, func() {
   366  		code = main{
   367  			execCommand: execCommand,
   368  		}.Run([]string{"juju", "version"})
   369  	})
   370  	c.Assert(code, gc.Equals, 0)
   371  	c.Assert(stub.Calls()[0].FuncName, gc.Equals, expectedCall)
   372  }
   373  
   374  func (s *MainSuite) TestFirstRunUpdateCloud(c *gc.C) {
   375  	// remove the juju-home.
   376  	err := os.RemoveAll(osenv.JujuXDGDataHomeDir())
   377  	c.Assert(err, jc.ErrorIsNil)
   378  	s.assertRunCommandUpdateCloud(c, "Run")
   379  }
   380  
   381  func (s *MainSuite) TestRunNoUpdateCloud(c *gc.C) {
   382  	s.assertRunCommandUpdateCloud(c, "Info")
   383  }
   384  
   385  func makeValidOldHome(c *gc.C) {
   386  	oldhome := osenv.OldJujuHomeDir()
   387  	err := os.MkdirAll(oldhome, 0700)
   388  	c.Assert(err, jc.ErrorIsNil)
   389  	err = ioutil.WriteFile(filepath.Join(oldhome, "environments.yaml"), []byte("boo!"), 0600)
   390  	c.Assert(err, jc.ErrorIsNil)
   391  }
   392  
   393  func checkVersionOutput(c *gc.C, output string) {
   394  	ver := version.Binary{
   395  		Number: jujuversion.Current,
   396  		Arch:   arch.HostArch(),
   397  		Series: series.MustHostSeries(),
   398  	}
   399  
   400  	c.Check(output, gc.Equals, ver.String()+"\n")
   401  }
   402  
   403  func assertNoArgs(c *gc.C, argChan <-chan []string) {
   404  	select {
   405  	case args := <-argChan:
   406  		c.Fatalf("Exec function called when it shouldn't have been (with args %q).", args)
   407  	default:
   408  		// this is the good path - there shouldn't be any args, which indicates
   409  		// the executable was not called.
   410  	}
   411  }
   412  
   413  var commandNames = []string{
   414  	"actions",
   415  	"add-cloud",
   416  	"add-credential",
   417  	"add-k8s",
   418  	"add-machine",
   419  	"add-model",
   420  	"add-relation",
   421  	"add-space",
   422  	"add-ssh-key",
   423  	"add-storage",
   424  	"add-subnet",
   425  	"add-unit",
   426  	"add-user",
   427  	"agree",
   428  	"agreements",
   429  	"attach",
   430  	"attach-resource",
   431  	"attach-storage",
   432  	"autoload-credentials",
   433  	"backups",
   434  	"bootstrap",
   435  	"budget",
   436  	"cached-images",
   437  	"cancel-action",
   438  	"change-user-password",
   439  	"charm",
   440  	"charm-resources",
   441  	"clouds",
   442  	"collect-metrics",
   443  	"config",
   444  	"consume",
   445  	"controller-config",
   446  	"controllers",
   447  	"create-backup",
   448  	"create-storage-pool",
   449  	"create-wallet",
   450  	"credentials",
   451  	"debug-hooks",
   452  	"debug-log",
   453  	"deploy",
   454  	"destroy-controller",
   455  	"destroy-model",
   456  	"detach-storage",
   457  	"diff-bundle",
   458  	"disable-command",
   459  	"disable-user",
   460  	"disabled-commands",
   461  	"download-backup",
   462  	"enable-command",
   463  	"enable-destroy-controller",
   464  	"enable-ha",
   465  	"enable-user",
   466  	"export-bundle",
   467  	"expose",
   468  	"find-offers",
   469  	"firewall-rules",
   470  	"get-constraints",
   471  	"get-model-constraints",
   472  	"grant",
   473  	"grant-cloud",
   474  	"gui",
   475  	"help",
   476  	"help-tool",
   477  	"hook-tool",
   478  	"hook-tools",
   479  	"import-filesystem",
   480  	"import-ssh-key",
   481  	"kill-controller",
   482  	"list-actions",
   483  	"list-agreements",
   484  	"list-backups",
   485  	"list-cached-images",
   486  	"list-charm-resources",
   487  	"list-clouds",
   488  	"list-controllers",
   489  	"list-credentials",
   490  	"list-disabled-commands",
   491  	"list-firewall-rules",
   492  	"list-machines",
   493  	"list-models",
   494  	"list-offers",
   495  	"list-payloads",
   496  	"list-plans",
   497  	"list-regions",
   498  	"list-resources",
   499  	"list-spaces",
   500  	"list-ssh-keys",
   501  	"list-storage",
   502  	"list-storage-pools",
   503  	"list-subnets",
   504  	"list-users",
   505  	"list-wallets",
   506  	"login",
   507  	"logout",
   508  	"machines",
   509  	"metrics",
   510  	"migrate",
   511  	"model-config",
   512  	"model-default",
   513  	"model-defaults",
   514  	"models",
   515  	"offer",
   516  	"offers",
   517  	"payloads",
   518  	"plans",
   519  	"regions",
   520  	"register",
   521  	"relate", //alias for add-relation
   522  	"reload-spaces",
   523  	"remove-application",
   524  	"remove-backup",
   525  	"remove-cached-images",
   526  	"remove-cloud",
   527  	"remove-consumed-application",
   528  	"remove-credential",
   529  	"remove-k8s",
   530  	"remove-machine",
   531  	"remove-offer",
   532  	"remove-relation",
   533  	"remove-saas",
   534  	"remove-ssh-key",
   535  	"remove-storage",
   536  	"remove-storage-pool",
   537  	"remove-unit",
   538  	"remove-user",
   539  	"resolved",
   540  	"resolve",
   541  	"resources",
   542  	"restore-backup",
   543  	"resume-relation",
   544  	"retry-provisioning",
   545  	"revoke",
   546  	"revoke-cloud",
   547  	"run",
   548  	"run-action",
   549  	"scale-application",
   550  	"scp",
   551  	"set-credential",
   552  	"set-constraints",
   553  	"set-default-credential",
   554  	"set-default-region",
   555  	"set-firewall-rule",
   556  	"set-meter-status",
   557  	"set-model-constraints",
   558  	"set-plan",
   559  	"set-series",
   560  	"set-wallet",
   561  	"show-action-output",
   562  	"show-action-status",
   563  	"show-application",
   564  	"show-backup",
   565  	"show-cloud",
   566  	"show-controller",
   567  	"show-credential",
   568  	"show-credentials",
   569  	"show-machine",
   570  	"show-model",
   571  	"show-offer",
   572  	"show-status",
   573  	"show-status-log",
   574  	"show-storage",
   575  	"show-user",
   576  	"show-wallet",
   577  	"sla",
   578  	"spaces",
   579  	"ssh",
   580  	"ssh-keys",
   581  	"status",
   582  	"storage",
   583  	"storage-pools",
   584  	"subnets",
   585  	"suspend-relation",
   586  	"switch",
   587  	"sync-agent-binaries",
   588  	"sync-tools",
   589  	"trust",
   590  	"unexpose",
   591  	"unregister",
   592  	"update-clouds",
   593  	"update-credential",
   594  	"update-storage-pool",
   595  	"upgrade-charm",
   596  	"upgrade-gui",
   597  	"upgrade-juju",
   598  	"upgrade-model",
   599  	"upgrade-series",
   600  	"upload-backup",
   601  	"users",
   602  	"version",
   603  	"wallets",
   604  	"whoami",
   605  }
   606  
   607  // devFeatures are feature flags that impact registration of commands.
   608  var devFeatures = []string{
   609  	// Currently no feature flags.
   610  }
   611  
   612  // These are the commands that are behind the `devFeatures`.
   613  var commandNamesBehindFlags = set.NewStrings(
   614  // Currently no commands behind feature flags.
   615  )
   616  
   617  func (s *MainSuite) TestHelpCommands(c *gc.C) {
   618  	// Check that we have correctly registered all the commands
   619  	// by checking the help output.
   620  	// First check default commands, and then check commands that are
   621  	// activated by feature flags.
   622  
   623  	// remove features behind dev_flag for the first test
   624  	// since they are not enabled.
   625  	cmdSet := set.NewStrings(commandNames...)
   626  
   627  	// 1. Default Commands. Disable all features.
   628  	setFeatureFlags("")
   629  	// Use sorted values here so we can better see what is wrong.
   630  	registered := getHelpCommandNames(c)
   631  	unknown := registered.Difference(cmdSet)
   632  	c.Assert(unknown, jc.DeepEquals, set.NewStrings())
   633  	missing := cmdSet.Difference(registered)
   634  	c.Assert(missing, jc.DeepEquals, set.NewStrings())
   635  
   636  	// 2. Enable development features, and test again.
   637  	cmdSet = cmdSet.Union(commandNamesBehindFlags)
   638  	setFeatureFlags(strings.Join(devFeatures, ","))
   639  	registered = getHelpCommandNames(c)
   640  	unknown = registered.Difference(cmdSet)
   641  	c.Assert(unknown, jc.DeepEquals, set.NewStrings())
   642  	missing = cmdSet.Difference(registered)
   643  	c.Assert(missing, jc.DeepEquals, set.NewStrings())
   644  }
   645  
   646  func getHelpCommandNames(c *gc.C) set.Strings {
   647  	out := badrun(c, 0, "help", "commands")
   648  	lines := strings.Split(out, "\n")
   649  	names := set.NewStrings()
   650  	for _, line := range lines {
   651  		f := strings.Fields(line)
   652  		if len(f) == 0 {
   653  			continue
   654  		}
   655  		names.Add(f[0])
   656  	}
   657  	return names
   658  }
   659  
   660  func setFeatureFlags(flags string) {
   661  	if err := os.Setenv(osenv.JujuFeatureFlagEnvKey, flags); err != nil {
   662  		panic(err)
   663  	}
   664  	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
   665  }
   666  
   667  var globalFlags = []string{
   668  	"--debug .*",
   669  	"--description .*",
   670  	"-h, --help .*",
   671  	"--log-file .*",
   672  	"--logging-config .*",
   673  	"-q, --quiet .*",
   674  	"--show-log .*",
   675  	"-v, --verbose .*",
   676  }
   677  
   678  func (s *MainSuite) TestHelpGlobalOptions(c *gc.C) {
   679  	// Check that we have correctly registered all the topics
   680  	// by checking the help output.
   681  	out := badrun(c, 0, "help", "global-options")
   682  	c.Assert(out, gc.Matches, `Global Options
   683  
   684  These options may be used with any command, and may appear in front of any
   685  command\.(.|\n)*`)
   686  	lines := strings.Split(out, "\n")
   687  	var flags []string
   688  	for _, line := range lines {
   689  		f := strings.Fields(line)
   690  		if len(f) == 0 || line[0] != '-' {
   691  			continue
   692  		}
   693  		flags = append(flags, line)
   694  	}
   695  	c.Assert(len(flags), gc.Equals, len(globalFlags))
   696  	for i, line := range flags {
   697  		c.Assert(line, gc.Matches, globalFlags[i])
   698  	}
   699  }
   700  
   701  func (s *MainSuite) TestRegisterCommands(c *gc.C) {
   702  	stub := &gitjujutesting.Stub{}
   703  	extraNames := []string{"cmd-a", "cmd-b"}
   704  	for i := range extraNames {
   705  		name := extraNames[i]
   706  		RegisterCommand(func() cmd.Command {
   707  			return &stubCommand{
   708  				stub: stub,
   709  				info: &cmd.Info{
   710  					Name: name,
   711  				},
   712  			}
   713  		})
   714  	}
   715  
   716  	registry := &stubRegistry{stub: stub}
   717  	registry.names = append(registry.names, "help", "version") // implicit
   718  	registerCommands(registry, cmdtesting.Context(c))
   719  	sort.Strings(registry.names)
   720  
   721  	expected := make([]string, len(commandNames))
   722  	copy(expected, commandNames)
   723  	expected = append(expected, extraNames...)
   724  	sort.Strings(expected)
   725  	c.Check(registry.names, jc.DeepEquals, expected)
   726  }
   727  
   728  type commands []cmd.Command
   729  
   730  func (r *commands) Register(c cmd.Command) {
   731  	*r = append(*r, c)
   732  }
   733  
   734  func (r *commands) RegisterDeprecated(c cmd.Command, check cmd.DeprecationCheck) {
   735  	if !check.Obsolete() {
   736  		*r = append(*r, c)
   737  	}
   738  }
   739  
   740  func (r *commands) RegisterSuperAlias(name, super, forName string, check cmd.DeprecationCheck) {
   741  	// Do nothing.
   742  }
   743  
   744  func (s *MainSuite) TestModelCommands(c *gc.C) {
   745  	var commands commands
   746  	registerCommands(&commands, cmdtesting.Context(c))
   747  	// There should not be any ModelCommands registered.
   748  	// ModelCommands must be wrapped using modelcmd.Wrap.
   749  	for _, cmd := range commands {
   750  		c.Logf("%v", cmd.Info().Name)
   751  		c.Check(cmd, gc.Not(gc.FitsTypeOf), modelcmd.ModelCommand(&bootstrapCommand{}))
   752  	}
   753  }
   754  
   755  func (s *MainSuite) TestAllCommandsPurpose(c *gc.C) {
   756  	// Verify each command that:
   757  	// - the Purpose field is not empty.
   758  	// - the Purpose ends with a full stop.
   759  	// - if set, the Doc field either begins with the name of the
   760  	// command or and uppercase letter.
   761  	//
   762  	// This:
   763  	// - makes Purpose a required documentation.
   764  	// - Standardises Purpose formatting across all commands.
   765  	// - Brings "help commands"'s output in line with "help <cmd>"'s header.
   766  	// - Makes the Doc content either start like a sentence, or start
   767  	//   godoc-like by using the command's name in lowercase.
   768  	var commands commands
   769  	registerCommands(&commands, cmdtesting.Context(c))
   770  	for _, cmd := range commands {
   771  		info := cmd.Info()
   772  		purpose := strings.TrimSpace(info.Purpose)
   773  		doc := strings.TrimSpace(info.Doc)
   774  		comment := func(message string) interface{} {
   775  			return gc.Commentf("command %q %s", info.Name, message)
   776  		}
   777  
   778  		c.Check(purpose, gc.Not(gc.Equals), "", comment("has empty Purpose"))
   779  		if purpose != "" {
   780  			prefix := string(purpose[0])
   781  			c.Check(prefix, gc.Equals, strings.ToUpper(prefix),
   782  				comment("expected uppercase first-letter Purpose"))
   783  			c.Check(strings.HasSuffix(purpose, "."), jc.IsTrue,
   784  				comment("is missing full stop in Purpose"))
   785  		}
   786  		if doc != "" && !strings.HasPrefix(doc, info.Name) {
   787  			prefix := string(doc[0])
   788  			c.Check(prefix, gc.Equals, strings.ToUpper(prefix),
   789  				comment("expected uppercase first-letter Doc"),
   790  			)
   791  		}
   792  	}
   793  }