diff --git a/cmd/cloud.go b/cmd/cloud.go index f2a29430d37..9c189606c04 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -391,7 +391,7 @@ service. Be sure to run the "k6 cloud login" command prior to authenticate with } // Register `k6 cloud` subcommands - cloudCmd.AddCommand(getCmdCloudRun(gs)) + cloudCmd.AddCommand(getCmdCloudRun(c)) cloudCmd.AddCommand(getCmdCloudLogin(gs)) cloudCmd.AddCommand(getCmdCloudUpload(c)) diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go index 2dd81033e27..e787fd3b7fe 100644 --- a/cmd/cloud_run.go +++ b/cmd/cloud_run.go @@ -1,21 +1,66 @@ package cmd import ( + "fmt" + + "go.k6.io/k6/errext/exitcodes" + + "go.k6.io/k6/errext" + "github.com/spf13/cobra" - "go.k6.io/k6/cmd/state" + "github.com/spf13/pflag" + "go.k6.io/k6/execution" + "go.k6.io/k6/execution/local" ) const cloudRunCommandName string = "run" -func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { - deprecatedCloudCmd := &cmdCloud{ - gs: gs, - showCloudLogs: true, - exitOnRunning: false, - uploadOnly: false, +type cmdCloudRun struct { + // localExecution stores the state of the --local-execution flag. + localExecution bool + + // linger stores the state of the --linger flag. + linger bool + + // noUsageReport stores the state of the --no-usage-report flag. + noUsageReport bool + + // runCmd holds an instance of the k6 run command that we store + // in order to be able to call its run method to support + // the --local-execution flag mode. + runCmd *cmdRun + + // deprecatedCloudCmd holds an instance of the k6 cloud command that we store + // in order to be able to call its run method to support the cloud execution + // feature, and to have access to its flagSet if necessary. + deprecatedCloudCmd *cmdCloud +} + +func getCmdCloudRun(cloudCmd *cmdCloud) *cobra.Command { + // We instantiate the run command here to be able to call its run method + // when the --local-execution flag is set. + runCmd := &cmdRun{ + gs: cloudCmd.gs, + + // We override the loadConfiguredTest func to use the local execution + // configuration which enforces the use of the cloud output among other + // side effects. + loadConfiguredTest: func(cmd *cobra.Command, args []string) ( + *loadedAndConfiguredTest, + execution.Controller, + error, + ) { + test, err := loadAndConfigureLocalTest(cloudCmd.gs, cmd, args, getCloudRunLocalExecutionConfig) + return test, local.NewController(), err + }, + } + + cloudRunCmd := &cmdCloudRun{ + deprecatedCloudCmd: cloudCmd, + runCmd: runCmd, } - exampleText := getExampleText(gs, ` + exampleText := getExampleText(cloudCmd.gs, ` # Run a test script in Grafana Cloud k6 $ {{.}} cloud run script.js @@ -25,7 +70,7 @@ func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { # Read a test script or archive from stdin and run it in Grafana Cloud k6 $ {{.}} cloud run - < script.js`[1:]) - cloudRunCmd := &cobra.Command{ + thisCmd := &cobra.Command{ Use: cloudRunCommandName, Short: "Run a test in Grafana Cloud k6", Long: `Run a test in Grafana Cloud k6. @@ -38,12 +83,93 @@ Use the "k6 cloud login" command to authenticate.`, "the k6 cloud run command expects a single argument consisting in either a path to a script or "+ "archive file, or the \"-\" symbol indicating the script or archive should be read from stdin", ), - PreRunE: deprecatedCloudCmd.preRun, - RunE: deprecatedCloudCmd.run, + PreRunE: cloudRunCmd.preRun, + RunE: cloudRunCmd.run, + } + + thisCmd.Flags().SortFlags = false + thisCmd.Flags().AddFlagSet(cloudRunCmd.flagSet()) + thisCmd.Flags().AddFlagSet(cloudCmd.flagSet()) + + return thisCmd +} + +func (c *cmdCloudRun) preRun(cmd *cobra.Command, args []string) error { + if c.localExecution { + if cmd.Flags().Changed("exit-on-running") { + return errext.WithExitCodeIfNone( + fmt.Errorf("the --local-execution flag is not compatible with the --exit-on-running flag"), + exitcodes.InvalidConfig, + ) + } + + if cmd.Flags().Changed("show-logs") { + return errext.WithExitCodeIfNone( + fmt.Errorf("the --local-execution flag is not compatible with the --show-logs flag"), + exitcodes.InvalidConfig, + ) + } + + return nil + } + + if c.linger { + return errext.WithExitCodeIfNone( + fmt.Errorf("the --linger flag can only be used in conjunction with the --local-execution flag"), + exitcodes.InvalidConfig, + ) + } + + return c.deprecatedCloudCmd.preRun(cmd, args) +} + +func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error { + if c.localExecution { + return c.runCmd.run(cmd, args) + } + + // When running the `k6 cloud run` command explicitly disable the usage report. + c.noUsageReport = true + + return c.deprecatedCloudCmd.run(cmd, args) +} + +func (c *cmdCloudRun) flagSet() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.SortFlags = false + + flags.BoolVar(&c.localExecution, "local-execution", c.localExecution, + "executes the test locally instead of in the cloud") + flags.BoolVar( + &c.linger, + "linger", + c.linger, + "only when using the local-execution mode, keeps the API server alive past the test end", + ) + flags.BoolVar( + &c.noUsageReport, + "no-usage-report", + c.noUsageReport, + "only when using the local-execution mode, don't send anonymous usage "+ + "stats (https://grafana.com/docs/k6/latest/set-up/usage-collection/)", + ) + + return flags +} + +func getCloudRunLocalExecutionConfig(flags *pflag.FlagSet) (Config, error) { + opts, err := getOptions(flags) + if err != nil { + return Config{}, err } - cloudRunCmd.Flags().SortFlags = false - cloudRunCmd.Flags().AddFlagSet(deprecatedCloudCmd.flagSet()) + // When running locally, we force the output to be cloud. + out := []string{"cloud"} - return cloudRunCmd + return Config{ + Options: opts, + Out: out, + Linger: getNullBool(flags, "linger"), + NoUsageReport: getNullBool(flags, "no-usage-report"), + }, nil } diff --git a/cmd/config.go b/cmd/config.go index f11d239486e..42b3b75143b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -30,7 +30,11 @@ func configFlagSet() *pflag.FlagSet { flags.SortFlags = false flags.StringArrayP("out", "o", []string{}, "`uri` for an external metrics database") flags.BoolP("linger", "l", false, "keep the API server alive past test end") - flags.Bool("no-usage-report", false, "don't send anonymous stats to the developers") + flags.Bool( + "no-usage-report", + false, + "don't send anonymous usage"+"stats (https://grafana.com/docs/k6/latest/set-up/usage-collection/)", + ) return flags } diff --git a/cmd/tests/cmd_cloud_run_test.go b/cmd/tests/cmd_cloud_run_test.go index b16ec5b1a1c..aad3f7eb4c2 100644 --- a/cmd/tests/cmd_cloud_run_test.go +++ b/cmd/tests/cmd_cloud_run_test.go @@ -1,6 +1,13 @@ package tests -import "testing" +import ( + "testing" + + "go.k6.io/k6/errext/exitcodes" + + "github.com/stretchr/testify/assert" + "go.k6.io/k6/cmd" +) func TestK6CloudRun(t *testing.T) { t.Parallel() @@ -10,3 +17,44 @@ func TestK6CloudRun(t *testing.T) { func setupK6CloudRunCmd(cliFlags []string) []string { return append([]string{"k6", "cloud", "run"}, append(cliFlags, "test.js")...) } + +func TestCloudRunCommandIncompatibleFlags(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cliArgs []string + wantStderrContains string + }{ + { + name: "using --linger should be incompatible with k6 cloud run", + cliArgs: []string{"--linger"}, + wantStderrContains: "the --linger flag can only be used in conjunction with the --local-execution flag", + }, + { + name: "using --exit-on-running should be incompatible with k6 cloud run --local-execution", + cliArgs: []string{"--local-execution", "--exit-on-running"}, + wantStderrContains: "the --local-execution flag is not compatible with the --exit-on-running flag", + }, + { + name: "using --show-logs should be incompatible with k6 cloud run --local-execution", + cliArgs: []string{"--local-execution", "--show-logs"}, + wantStderrContains: "the --local-execution flag is not compatible with the --show-logs flag", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ts := getSimpleCloudTestState(t, nil, setupK6CloudRunCmd, tc.cliArgs, nil, nil) + ts.ExpectedExitCode = int(exitcodes.InvalidConfig) + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stderr := ts.Stderr.String() + assert.Contains(t, stderr, tc.wantStderrContains) + }) + } +}