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 @@ -382,7 +382,7 @@ To run tests in the cloud, users are now invited to migrate to the "k6 cloud run
}

// Register `k6 cloud` subcommands
cloudCmd.AddCommand(getCmdCloudRun(gs))
cloudCmd.AddCommand(getCmdCloudRun(c))
cloudCmd.AddCommand(getCmdCloudLogin(gs))

cloudCmd.Flags().SortFlags = false
Expand Down
144 changes: 130 additions & 14 deletions cmd/cloud_run.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
package cmd

import (
"fmt"

"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
},
}

exampleText := getExampleText(gs, `
cloudRunCmd := &cmdCloudRun{
deprecatedCloudCmd: cloudCmd,
runCmd: runCmd,
}

exampleText := getExampleText(cloudCmd.gs, `
# Run a test script in Grafana Cloud k6
$ {{.}} cloud run script.js

Expand All @@ -25,7 +66,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 +79,87 @@ 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 fmt.Errorf("the --local-execution flag is not compatible with the --exit-on-running flag")
}

if cmd.Flags().Changed("show-logs") {
return fmt.Errorf("the --local-execution flag is not compatible with the --show-logs flag")
}

return nil
}

if c.linger {
return fmt.Errorf("the --linger flag can only be used in conjunction with the --local-execution flag")
}

return c.deprecatedCloudCmd.preRun(cmd, args)
}

func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error {
if c.localExecution {
// Note that when running the k6 cloud run command with the --local-execution
oleiade marked this conversation as resolved.
Show resolved Hide resolved
// flag, we handle the no-usage-report flag here as we would do in the k6 run
// command.
return c.runCmd.run(cmd, args)
}

// When running the k6 cloud run command and executing in the cloud however, we
// explicitly disable the usage report.
oleiade marked this conversation as resolved.
Show resolved Hide resolved
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 stats to the developers",
oleiade marked this conversation as resolved.
Show resolved Hide resolved
)

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
}
48 changes: 47 additions & 1 deletion cmd/tests/cmd_cloud_run_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package tests

import "testing"
import (
"testing"

"github.com/stretchr/testify/assert"
"go.k6.io/k6/cmd"
)

func TestK6CloudRun(t *testing.T) {
t.Parallel()
Expand All @@ -10,3 +15,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 = -1
oleiade marked this conversation as resolved.
Show resolved Hide resolved
cmd.ExecuteWithGlobalState(ts.GlobalState)

stderr := ts.Stderr.String()
assert.Contains(t, stderr, tc.wantStderrContains)
})
}
}