Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1/2] Implement the --local-execution mode for k6 cloud run #3904

Merged
merged 11 commits into from
Sep 10, 2024
2 changes: 1 addition & 1 deletion cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
154 changes: 140 additions & 14 deletions cmd/cloud_run.go
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand All @@ -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
oleiade marked this conversation as resolved.
Show resolved Hide resolved

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
}
50 changes: 49 additions & 1 deletion cmd/tests/cmd_cloud_run_test.go
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
})
}
}