From d254f001857b69d940a508ec6e9e7e2af5ac66eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 27 Dec 2024 11:14:33 +0100 Subject: [PATCH 01/10] Refactor new command and templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new.go | 150 +++++++++------------------------- cmd/newtemplates/browser.js | 43 ++++++++++ cmd/newtemplates/minimal.js | 17 ++++ cmd/newtemplates/protocol.js | 36 ++++++++ cmd/newtemplates/templates.go | 53 ++++++++++++ 5 files changed, 188 insertions(+), 111 deletions(-) create mode 100644 cmd/newtemplates/browser.js create mode 100644 cmd/newtemplates/minimal.js create mode 100644 cmd/newtemplates/protocol.js create mode 100644 cmd/newtemplates/templates.go diff --git a/cmd/new.go b/cmd/new.go index 39a1720efbe..2089a32bcbf 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -2,99 +2,37 @@ package cmd import ( "fmt" - "path" - "text/template" - "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/pflag" + templates "go.k6.io/k6/cmd/newtemplates" "go.k6.io/k6/cmd/state" "go.k6.io/k6/lib/fsext" ) -const defaultNewScriptName = "script.js" - -//nolint:gochecknoglobals -var defaultNewScriptTemplate = template.Must(template.New("new").Parse(`import http from 'k6/http'; -import { sleep } from 'k6'; - -export const options = { - // A number specifying the number of VUs to run concurrently. - vus: 10, - // A string specifying the total duration of the test run. - duration: '30s', - - // The following section contains configuration options for execution of this - // test script in Grafana Cloud. - // - // See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/ - // to learn about authoring and running k6 test scripts in Grafana k6 Cloud. - // - // cloud: { - // // The ID of the project to which the test is assigned in the k6 Cloud UI. - // // By default tests are executed in default project. - // projectID: "", - // // The name of the test in the k6 Cloud UI. - // // Test runs with the same name will be grouped. - // name: "{{ .ScriptName }}" - // }, - - // Uncomment this section to enable the use of Browser API in your tests. - // - // See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more - // about using Browser API in your test scripts. - // - // scenarios: { - // // The scenario name appears in the result summary, tags, and so on. - // // You can give the scenario any name, as long as each name in the script is unique. - // ui: { - // // Executor is a mandatory parameter for browser-based tests. - // // Shared iterations in this case tells k6 to reuse VUs to execute iterations. - // // - // // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types. - // executor: 'shared-iterations', - // options: { - // browser: { - // // This is a mandatory parameter that instructs k6 to launch and - // // connect to a chromium-based browser, and use it to run UI-based - // // tests. - // type: 'chromium', - // }, - // }, - // }, - // } -}; - -// The function that defines VU logic. -// -// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more -// about authoring k6 scripts. -// -export default function() { - http.get('https://test.k6.io'); - sleep(1); -} -`)) - -type initScriptTemplateArgs struct { - ScriptName string -} +var ( + defaultNewScriptName = "script.js" +) -// newScriptCmd represents the `k6 new` command type newScriptCmd struct { gs *state.GlobalState overwriteFiles bool + templateType string + enableCloud bool + projectID string } func (c *newScriptCmd) flagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("", pflag.ContinueOnError) flags.SortFlags = false flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "Overwrite existing files") - + flags.StringVar(&c.templateType, "template", "minimal", "Template type (choices: minimal, protocol, browser)") + flags.BoolVar(&c.enableCloud, "cloud", false, "Enable cloud integration") + flags.StringVar(&c.projectID, "project-id", "", "Specify the project ID for cloud integration") return flags } -func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { //nolint:revive +func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { target := defaultNewScriptName if len(args) > 0 { target = args[0] @@ -104,64 +42,54 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { //nolint:r if err != nil { return err } - if fileExists && !c.overwriteFiles { - return fmt.Errorf("%s already exists, please use the `--force` flag if you want overwrite it", target) + return fmt.Errorf("%s already exists. Use the `--force` flag to overwrite", target) } fd, err := c.gs.FS.Create(target) if err != nil { return err } - defer func() { - _ = fd.Close() // we may think to check the error and log - }() + defer fd.Close() - if err := defaultNewScriptTemplate.Execute(fd, initScriptTemplateArgs{ - ScriptName: path.Base(target), - }); err != nil { + tmpl, err := templates.GetTemplate(c.templateType) + if err != nil { return err } - valueColor := getColor(c.gs.Flags.NoColor || !c.gs.Stdout.IsTTY, color.Bold) - printToStdout(c.gs, fmt.Sprintf( - "Initialized a new k6 test script in %s. You can now execute it by running `%s run %s`.\n", - valueColor.Sprint(target), - c.gs.BinaryName, - target, - )) + argsStruct := templates.TemplateArgs{ + ScriptName: target, + EnableCloud: c.enableCloud, + ProjectID: c.projectID, + } + + if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { + return err + } + fmt.Fprintf(c.gs.Stdout, "New script created: %s (%s template).\n", target, c.templateType) return nil } func getCmdNewScript(gs *state.GlobalState) *cobra.Command { c := &newScriptCmd{gs: gs} - exampleText := getExampleText(gs, ` - # Create a minimal k6 script in the current directory. By default, k6 creates script.js. - {{.}} new - - # Create a minimal k6 script in the current directory and store it in test.js - {{.}} new test.js - - # Overwrite existing test.js with a minimal k6 script - {{.}} new -f test.js`[1:]) - initCmd := &cobra.Command{ - Use: "new", - Short: "Create and initialize a new k6 script", - Long: `Create and initialize a new k6 script. - -This command will create a minimal k6 script in the current directory and -store it in the file specified by the first argument. If no argument is -provided, the script will be stored in script.js. - -This command will not overwrite existing files.`, - Example: exampleText, - Args: cobra.MaximumNArgs(1), - RunE: c.run, + Use: "new [file]", + Short: "Create a new k6 script", + Long: `Create a new k6 script using one of the predefined templates. + +By default, the script will be named script.js unless a different name is specified.`, + Example: ` + # Create a minimal k6 script + {{.}} new --template minimal + + # Overwrite an existing file with a protocol-based script + {{.}} new -f --template protocol test.js`, + Args: cobra.MaximumNArgs(1), + RunE: c.run, } - initCmd.Flags().AddFlagSet(c.flagSet()) + initCmd.Flags().AddFlagSet(c.flagSet()) return initCmd } diff --git a/cmd/newtemplates/browser.js b/cmd/newtemplates/browser.js new file mode 100644 index 00000000000..1a5ab479669 --- /dev/null +++ b/cmd/newtemplates/browser.js @@ -0,0 +1,43 @@ +import { browser } from "k6/browser"; +import { check } from "k6"; + +const BASE_URL = __ENV.BASE_URL || "https://quickpizza.grafana.com"; + +export const options = { + scenarios: { + ui: { + executor: "shared-iterations", + options: { + browser: { + type: "chromium", + }, + }, + }, + }, {{ if .EnableCloud }} + cloud: { {{ if .ProjectID }} + projectID: {{ .ProjectID }}, {{ else }} + // projectID: 12345, // Change this {{ end }} + name: "{{ .ScriptName }}", + }, {{ end }} +}; + +export default async function () { + let checkData; + const page = await browser.newPage(); + try { + await page.goto(BASE_URL); + checkData = await page.locator("h1").textContent(); + check(page, { + header: checkData === "Looking to break out of your pizza routine?", + }); + await page.locator('//button[. = "Pizza, Please!"]').click(); + await page.waitForTimeout(500); + await page.screenshot({ path: "screenshot.png" }); + checkData = await page.locator("div#recommendations").textContent(); + check(page, { + recommendation: checkData !== "", + }); + } finally { + await page.close(); + } +} diff --git a/cmd/newtemplates/minimal.js b/cmd/newtemplates/minimal.js new file mode 100644 index 00000000000..7e29b48b1ad --- /dev/null +++ b/cmd/newtemplates/minimal.js @@ -0,0 +1,17 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + vus: 10, + duration: '30s',{{ if .EnableCloud }} + cloud: { {{ if .ProjectID }} + projectID: {{ .ProjectID }}, {{ else }} + // projectID: 12345, // Change this {{ end }} + name: "{{ .ScriptName }}", + }, {{ end }} +}; + +export default function () { + http.get('https://quickpizza.grafana.com'); + sleep(1); +} diff --git a/cmd/newtemplates/protocol.js b/cmd/newtemplates/protocol.js new file mode 100644 index 00000000000..f2cc0dbd24a --- /dev/null +++ b/cmd/newtemplates/protocol.js @@ -0,0 +1,36 @@ +import http from "k6/http"; +import { check, sleep } from "k6"; + +const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com'; + +export const options = { + vus: 5, + duration: '10s',{{ if .EnableCloud }} + cloud: { {{ if .ProjectID }} + projectID: {{ .ProjectID }}, {{ else }} + // projectID: 12345, // Change this {{ end }} + name: "{{ .ScriptName }}", + }, {{ end }} +}; + +export default function () { + let restrictions = { + maxCaloriesPerSlice: 500, + mustBeVegetarian: false, + excludedIngredients: ["pepperoni"], + excludedTools: ["knife"], + maxNumberOfToppings: 6, + minNumberOfToppings: 2 + }; + + let res = http.post(BASE_URL + "/api/pizza", JSON.stringify(restrictions), { + headers: { + 'Content-Type': 'application/json', + 'X-User-ID': 23423, + }, + }); + + check(res, { "status is 200": (res) => res.status === 200 }); + console.log(res.json().pizza.name + " (" + res.json().pizza.ingredients.length + " ingredients)"); + sleep(1); +} diff --git a/cmd/newtemplates/templates.go b/cmd/newtemplates/templates.go new file mode 100644 index 00000000000..538480f6b4b --- /dev/null +++ b/cmd/newtemplates/templates.go @@ -0,0 +1,53 @@ +package cmd + +import ( + _ "embed" + "fmt" + "io" + "text/template" +) + +// Embed templates +// +//go:embed minimal.js +var minimalTemplateContent string + +//go:embed protocol.js +var protocolTemplateContent string + +//go:embed browser.js +var browserTemplateContent string + +// Pre-parse templates +var ( + MinimalScriptTemplate = template.Must(template.New("minimal").Parse(minimalTemplateContent)) + ProtocolScriptTemplate = template.Must(template.New("protocol").Parse(protocolTemplateContent)) + BrowserScriptTemplate = template.Must(template.New("browser").Parse(browserTemplateContent)) + DefaultNewScriptName = "script.js" +) + +// TemplateArgs represents arguments passed to templates +type TemplateArgs struct { + ScriptName string + EnableCloud bool + ProjectID string +} + +// GetTemplate selects the appropriate template based on the type +func GetTemplate(templateType string) (*template.Template, error) { + switch templateType { + case "minimal": + return MinimalScriptTemplate, nil + case "protocol": + return ProtocolScriptTemplate, nil + case "browser": + return BrowserScriptTemplate, nil + default: + return nil, fmt.Errorf("invalid template type: %s", templateType) + } +} + +// ExecuteTemplate applies the template with provided arguments and writes to the provided writer +func ExecuteTemplate(w io.Writer, tmpl *template.Template, args TemplateArgs) error { + return tmpl.Execute(w, args) +} From e7e95f15d9658cef6ab3caff72d2fa51f2e9d218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 27 Dec 2024 11:41:58 +0100 Subject: [PATCH 02/10] Move Cloud to another command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/cloud.go | 6 ++- cmd/cloud_new.go | 97 ++++++++++++++++++++++++++++++++++++ cmd/new.go | 34 ++++++------- cmd/newtemplates/browser.js | 2 +- cmd/newtemplates/minimal.js | 2 +- cmd/newtemplates/protocol.js | 2 +- 6 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 cmd/cloud_new.go diff --git a/cmd/cloud.go b/cmd/cloud.go index 9c189606c04..91bc3ece08f 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -371,7 +371,10 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { $ {{.}} cloud run script.js # Run a k6 archive in the Grafana Cloud k6 - $ {{.}} cloud run archive.tar`[1:]) + $ {{.}} cloud run archive.tar + + # Create a new cloud-ready k6 script + $ {{.}} cloud new script.js`[1:]) cloudCmd := &cobra.Command{ Use: "cloud", @@ -394,6 +397,7 @@ service. Be sure to run the "k6 cloud login" command prior to authenticate with cloudCmd.AddCommand(getCmdCloudRun(c)) cloudCmd.AddCommand(getCmdCloudLogin(gs)) cloudCmd.AddCommand(getCmdCloudUpload(c)) + cloudCmd.AddCommand(getCmdCloudNew(c)) cloudCmd.Flags().SortFlags = false cloudCmd.Flags().AddFlagSet(c.flagSet()) diff --git a/cmd/cloud_new.go b/cmd/cloud_new.go new file mode 100644 index 00000000000..0447661e493 --- /dev/null +++ b/cmd/cloud_new.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "fmt" + + templates "go.k6.io/k6/cmd/newtemplates" + "go.k6.io/k6/cmd/state" + "go.k6.io/k6/lib/fsext" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const defaultNewCloudScriptName = "script.js" + +type cmdCloudNew struct { + gs *state.GlobalState + overwriteFiles bool + templateType string + projectID string +} + +func (c *cmdCloudNew) flagSet() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.SortFlags = false + flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "overwrite existing files") + flags.StringVar(&c.templateType, "template", "minimal", "template type (choices: minimal, protocol, browser)") + flags.StringVar(&c.projectID, "project-id", "", "specify the Grafana Cloud ProjectID for the test") + return flags +} + +func (c *cmdCloudNew) run(cmd *cobra.Command, args []string) error { + target := defaultNewCloudScriptName + if len(args) > 0 { + target = args[0] + } + + fileExists, err := fsext.Exists(c.gs.FS, target) + if err != nil { + return err + } + if fileExists && !c.overwriteFiles { + return fmt.Errorf("%s already exists. Use the `--force` flag to overwrite", target) + } + + fd, err := c.gs.FS.Create(target) + if err != nil { + return err + } + defer fd.Close() + + tmpl, err := templates.GetTemplate(c.templateType) + if err != nil { + return err + } + + argsStruct := templates.TemplateArgs{ + ScriptName: target, + EnableCloud: true, + ProjectID: c.projectID, + } + + if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { + return err + } + + fmt.Fprintf(c.gs.Stdout, "New cloud script created: %s (%s template).\n", target, c.templateType) + return nil +} + +func getCmdCloudNew(cloudCmd *cmdCloud) *cobra.Command { + c := &cmdCloudNew{gs: cloudCmd.gs} + + exampleText := getExampleText(cloudCmd.gs, ` + # Create a minimal cloud-ready script + $ {{.}} cloud new --template minimal + + # Create a protocol-based cloud script with a specific ProjectID + $ {{.}} cloud new --template protocol --project-id my-project-id + + # Overwrite an existing file with a browser-based cloud script + $ {{.}} cloud new -f --template browser test.js --project-id my-project-id`[1:]) + + initCmd := &cobra.Command{ + Use: "new [file]", + Short: "Create a new k6 script", + Long: `Create a new cloud-ready k6 script using one of the predefined templates. + +By default, the script will be named script.js unless a different name is specified.`, + Example: exampleText, + Args: cobra.MaximumNArgs(1), + RunE: c.run, + } + + initCmd.Flags().AddFlagSet(c.flagSet()) + return initCmd +} diff --git a/cmd/new.go b/cmd/new.go index 2089a32bcbf..531b20d782a 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -10,25 +10,19 @@ import ( "go.k6.io/k6/lib/fsext" ) -var ( - defaultNewScriptName = "script.js" -) +const defaultNewScriptName = "script.js" type newScriptCmd struct { gs *state.GlobalState overwriteFiles bool templateType string - enableCloud bool - projectID string } func (c *newScriptCmd) flagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("", pflag.ContinueOnError) flags.SortFlags = false - flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "Overwrite existing files") - flags.StringVar(&c.templateType, "template", "minimal", "Template type (choices: minimal, protocol, browser)") - flags.BoolVar(&c.enableCloud, "cloud", false, "Enable cloud integration") - flags.StringVar(&c.projectID, "project-id", "", "Specify the project ID for cloud integration") + flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "overwrite existing files") + flags.StringVar(&c.templateType, "template", "minimal", "template type (choices: minimal, protocol, browser)") return flags } @@ -58,9 +52,7 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { } argsStruct := templates.TemplateArgs{ - ScriptName: target, - EnableCloud: c.enableCloud, - ProjectID: c.projectID, + ScriptName: target, } if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { @@ -74,20 +66,22 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { func getCmdNewScript(gs *state.GlobalState) *cobra.Command { c := &newScriptCmd{gs: gs} + exampleText := getExampleText(c.gs, ` + # Create a minimal k6 script + $ {{.}} new --template minimal + + # Overwrite an existing file with a protocol-based script + $ {{.}} new -f --template protocol test.js`[1:]) + initCmd := &cobra.Command{ Use: "new [file]", Short: "Create a new k6 script", Long: `Create a new k6 script using one of the predefined templates. By default, the script will be named script.js unless a different name is specified.`, - Example: ` - # Create a minimal k6 script - {{.}} new --template minimal - - # Overwrite an existing file with a protocol-based script - {{.}} new -f --template protocol test.js`, - Args: cobra.MaximumNArgs(1), - RunE: c.run, + Example: exampleText, + Args: cobra.MaximumNArgs(1), + RunE: c.run, } initCmd.Flags().AddFlagSet(c.flagSet()) diff --git a/cmd/newtemplates/browser.js b/cmd/newtemplates/browser.js index 1a5ab479669..b9376541071 100644 --- a/cmd/newtemplates/browser.js +++ b/cmd/newtemplates/browser.js @@ -16,7 +16,7 @@ export const options = { }, {{ if .EnableCloud }} cloud: { {{ if .ProjectID }} projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Change this {{ end }} + // projectID: 12345, // Replace this with your own projectID {{ end }} name: "{{ .ScriptName }}", }, {{ end }} }; diff --git a/cmd/newtemplates/minimal.js b/cmd/newtemplates/minimal.js index 7e29b48b1ad..58d64064afb 100644 --- a/cmd/newtemplates/minimal.js +++ b/cmd/newtemplates/minimal.js @@ -6,7 +6,7 @@ export const options = { duration: '30s',{{ if .EnableCloud }} cloud: { {{ if .ProjectID }} projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Change this {{ end }} + // projectID: 12345, // Replace this with your own projectID {{ end }} name: "{{ .ScriptName }}", }, {{ end }} }; diff --git a/cmd/newtemplates/protocol.js b/cmd/newtemplates/protocol.js index f2cc0dbd24a..0d302d94be4 100644 --- a/cmd/newtemplates/protocol.js +++ b/cmd/newtemplates/protocol.js @@ -8,7 +8,7 @@ export const options = { duration: '10s',{{ if .EnableCloud }} cloud: { {{ if .ProjectID }} projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Change this {{ end }} + // projectID: 12345, // Replace this with your own projectID {{ end }} name: "{{ .ScriptName }}", }, {{ end }} }; From c3f84ba99d88c16957effbd225e6dbaf6e26ca39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 27 Dec 2024 11:58:51 +0100 Subject: [PATCH 03/10] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/new_test.go b/cmd/new_test.go index 1b97cf38718..a3d58f68949 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -56,8 +55,7 @@ func TestNewScriptCmd(t *testing.T) { jsData := string(data) assert.Contains(t, jsData, "export const options = {") - assert.Contains(t, jsData, fmt.Sprintf(`name: "%s"`, testCase.expectedCloudName)) - assert.Contains(t, jsData, "export default function() {") + assert.Contains(t, jsData, "export default function () {") }) } } @@ -93,5 +91,5 @@ func TestNewScriptCmd_FileExists_Overwrite(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(data), "export const options = {") - assert.Contains(t, string(data), "export default function() {") + assert.Contains(t, string(data), "export default function () {") } From fea3e7ff9f0431fe82e62438af68f08f459a433c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 27 Dec 2024 12:25:26 +0100 Subject: [PATCH 04/10] Small changes to the templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new_test.go | 4 ++-- cmd/newtemplates/browser.js | 15 +++++++++++++-- cmd/newtemplates/minimal.js | 7 ++++--- cmd/newtemplates/protocol.js | 22 +++++++++++++++++++--- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/cmd/new_test.go b/cmd/new_test.go index a3d58f68949..5f955a0b423 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -55,7 +55,7 @@ func TestNewScriptCmd(t *testing.T) { jsData := string(data) assert.Contains(t, jsData, "export const options = {") - assert.Contains(t, jsData, "export default function () {") + assert.Contains(t, jsData, "export default function() {") }) } } @@ -91,5 +91,5 @@ func TestNewScriptCmd_FileExists_Overwrite(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(data), "export const options = {") - assert.Contains(t, string(data), "export default function () {") + assert.Contains(t, string(data), "export default function() {") } diff --git a/cmd/newtemplates/browser.js b/cmd/newtemplates/browser.js index b9376541071..46dbc00f15a 100644 --- a/cmd/newtemplates/browser.js +++ b/cmd/newtemplates/browser.js @@ -1,5 +1,6 @@ import { browser } from "k6/browser"; -import { check } from "k6"; +import http from "k6/http"; +import { sleep, check } from 'k6'; const BASE_URL = __ENV.BASE_URL || "https://quickpizza.grafana.com"; @@ -21,7 +22,16 @@ export const options = { }, {{ end }} }; -export default async function () { +export function setup() { + let res = http.get(BASE_URL); + if (res.status !== 200) { + throw new Error( + `Got unexpected status code ${res.status} when trying to setup. Exiting.` + ); + } +} + +export default async function() { let checkData; const page = await browser.newPage(); try { @@ -40,4 +50,5 @@ export default async function () { } finally { await page.close(); } + sleep(1); } diff --git a/cmd/newtemplates/minimal.js b/cmd/newtemplates/minimal.js index 58d64064afb..191ced04806 100644 --- a/cmd/newtemplates/minimal.js +++ b/cmd/newtemplates/minimal.js @@ -1,5 +1,5 @@ import http from 'k6/http'; -import { sleep } from 'k6'; +import { sleep, check } from 'k6'; export const options = { vus: 10, @@ -11,7 +11,8 @@ export const options = { }, {{ end }} }; -export default function () { - http.get('https://quickpizza.grafana.com'); +export default function() { + let res = http.get('https://quickpizza.grafana.com'); + check(res, { "status is 200": (res) => res.status === 200 }); sleep(1); } diff --git a/cmd/newtemplates/protocol.js b/cmd/newtemplates/protocol.js index 0d302d94be4..623be6fabe5 100644 --- a/cmd/newtemplates/protocol.js +++ b/cmd/newtemplates/protocol.js @@ -4,8 +4,15 @@ import { check, sleep } from "k6"; const BASE_URL = __ENV.BASE_URL || 'https://quickpizza.grafana.com'; export const options = { - vus: 5, - duration: '10s',{{ if .EnableCloud }} + vus: 10, + stages: [ + { duration: "10s", target: 5 }, + { duration: "20s", target: 10 }, + { duration: "1s", target: 0 }, + ], thresholds: { + http_req_failed: ["rate<0.01"], + http_req_duration: ["p(95)<500", "p(99)<1000"], + },{{ if .EnableCloud }} cloud: { {{ if .ProjectID }} projectID: {{ .ProjectID }}, {{ else }} // projectID: 12345, // Replace this with your own projectID {{ end }} @@ -13,7 +20,16 @@ export const options = { }, {{ end }} }; -export default function () { +export function setup() { + let res = http.get(BASE_URL); + if (res.status !== 200) { + throw new Error( + `Got unexpected status code ${res.status} when trying to setup. Exiting.` + ); + } +} + +export default function() { let restrictions = { maxCaloriesPerSlice: 500, mustBeVegetarian: false, From b1af40f20fc82cb228cb0dce071eebc110e842b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 13:16:42 +0100 Subject: [PATCH 05/10] Move everything to one command based on feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/cloud.go | 6 +-- cmd/cloud_new.go | 97 ------------------------------------------------ cmd/new.go | 11 +++++- cmd/new_test.go | 25 +++++++++++++ 4 files changed, 35 insertions(+), 104 deletions(-) delete mode 100644 cmd/cloud_new.go diff --git a/cmd/cloud.go b/cmd/cloud.go index 91bc3ece08f..9c189606c04 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -371,10 +371,7 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { $ {{.}} cloud run script.js # Run a k6 archive in the Grafana Cloud k6 - $ {{.}} cloud run archive.tar - - # Create a new cloud-ready k6 script - $ {{.}} cloud new script.js`[1:]) + $ {{.}} cloud run archive.tar`[1:]) cloudCmd := &cobra.Command{ Use: "cloud", @@ -397,7 +394,6 @@ service. Be sure to run the "k6 cloud login" command prior to authenticate with cloudCmd.AddCommand(getCmdCloudRun(c)) cloudCmd.AddCommand(getCmdCloudLogin(gs)) cloudCmd.AddCommand(getCmdCloudUpload(c)) - cloudCmd.AddCommand(getCmdCloudNew(c)) cloudCmd.Flags().SortFlags = false cloudCmd.Flags().AddFlagSet(c.flagSet()) diff --git a/cmd/cloud_new.go b/cmd/cloud_new.go deleted file mode 100644 index 0447661e493..00000000000 --- a/cmd/cloud_new.go +++ /dev/null @@ -1,97 +0,0 @@ -package cmd - -import ( - "fmt" - - templates "go.k6.io/k6/cmd/newtemplates" - "go.k6.io/k6/cmd/state" - "go.k6.io/k6/lib/fsext" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -const defaultNewCloudScriptName = "script.js" - -type cmdCloudNew struct { - gs *state.GlobalState - overwriteFiles bool - templateType string - projectID string -} - -func (c *cmdCloudNew) flagSet() *pflag.FlagSet { - flags := pflag.NewFlagSet("", pflag.ContinueOnError) - flags.SortFlags = false - flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "overwrite existing files") - flags.StringVar(&c.templateType, "template", "minimal", "template type (choices: minimal, protocol, browser)") - flags.StringVar(&c.projectID, "project-id", "", "specify the Grafana Cloud ProjectID for the test") - return flags -} - -func (c *cmdCloudNew) run(cmd *cobra.Command, args []string) error { - target := defaultNewCloudScriptName - if len(args) > 0 { - target = args[0] - } - - fileExists, err := fsext.Exists(c.gs.FS, target) - if err != nil { - return err - } - if fileExists && !c.overwriteFiles { - return fmt.Errorf("%s already exists. Use the `--force` flag to overwrite", target) - } - - fd, err := c.gs.FS.Create(target) - if err != nil { - return err - } - defer fd.Close() - - tmpl, err := templates.GetTemplate(c.templateType) - if err != nil { - return err - } - - argsStruct := templates.TemplateArgs{ - ScriptName: target, - EnableCloud: true, - ProjectID: c.projectID, - } - - if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { - return err - } - - fmt.Fprintf(c.gs.Stdout, "New cloud script created: %s (%s template).\n", target, c.templateType) - return nil -} - -func getCmdCloudNew(cloudCmd *cmdCloud) *cobra.Command { - c := &cmdCloudNew{gs: cloudCmd.gs} - - exampleText := getExampleText(cloudCmd.gs, ` - # Create a minimal cloud-ready script - $ {{.}} cloud new --template minimal - - # Create a protocol-based cloud script with a specific ProjectID - $ {{.}} cloud new --template protocol --project-id my-project-id - - # Overwrite an existing file with a browser-based cloud script - $ {{.}} cloud new -f --template browser test.js --project-id my-project-id`[1:]) - - initCmd := &cobra.Command{ - Use: "new [file]", - Short: "Create a new k6 script", - Long: `Create a new cloud-ready k6 script using one of the predefined templates. - -By default, the script will be named script.js unless a different name is specified.`, - Example: exampleText, - Args: cobra.MaximumNArgs(1), - RunE: c.run, - } - - initCmd.Flags().AddFlagSet(c.flagSet()) - return initCmd -} diff --git a/cmd/new.go b/cmd/new.go index 531b20d782a..fb27e3bd890 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -16,6 +16,7 @@ type newScriptCmd struct { gs *state.GlobalState overwriteFiles bool templateType string + projectID string } func (c *newScriptCmd) flagSet() *pflag.FlagSet { @@ -23,6 +24,7 @@ func (c *newScriptCmd) flagSet() *pflag.FlagSet { flags.SortFlags = false flags.BoolVarP(&c.overwriteFiles, "force", "f", false, "overwrite existing files") flags.StringVar(&c.templateType, "template", "minimal", "template type (choices: minimal, protocol, browser)") + flags.StringVar(&c.projectID, "project-id", "", "specify the Grafana Cloud project ID for the test") return flags } @@ -52,7 +54,9 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { } argsStruct := templates.TemplateArgs{ - ScriptName: target, + ScriptName: target, + EnableCloud: c.projectID != "", + ProjectID: c.projectID, } if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { @@ -71,7 +75,10 @@ func getCmdNewScript(gs *state.GlobalState) *cobra.Command { $ {{.}} new --template minimal # Overwrite an existing file with a protocol-based script - $ {{.}} new -f --template protocol test.js`[1:]) + $ {{.}} new -f --template protocol test.js + + # Create a cloud-ready script with a specific project ID + $ {{.}} new --project-id 12315`[1:]) initCmd := &cobra.Command{ Use: "new [file]", diff --git a/cmd/new_test.go b/cmd/new_test.go index 5f955a0b423..2a0e6fd05fd 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -93,3 +93,28 @@ func TestNewScriptCmd_FileExists_Overwrite(t *testing.T) { assert.Contains(t, string(data), "export const options = {") assert.Contains(t, string(data), "export default function() {") } + +func TestNewScriptCmd_InvalidTemplateType(t *testing.T) { + t.Parallel() + + ts := tests.NewGlobalTestState(t) + ts.CmdArgs = []string{"k6", "new", "--template", "invalid-template"} + + ts.ExpectedExitCode = -1 + + newRootCommand(ts.GlobalState).execute() + assert.Contains(t, ts.Stderr.String(), "invalid template type") +} +func TestNewScriptCmd_ProjectID(t *testing.T) { + t.Parallel() + + ts := tests.NewGlobalTestState(t) + ts.CmdArgs = []string{"k6", "new", "--project-id", "1422"} + + newRootCommand(ts.GlobalState).execute() + + data, err := fsext.ReadFile(ts.FS, defaultNewScriptName) + require.NoError(t, err) + + assert.Contains(t, string(data), "projectID: 1422") +} From b3e99b800375c8213101d376b0ac8a202512473f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 13:29:25 +0100 Subject: [PATCH 06/10] Fix help text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index fb27e3bd890..466325094ce 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -82,8 +82,8 @@ func getCmdNewScript(gs *state.GlobalState) *cobra.Command { initCmd := &cobra.Command{ Use: "new [file]", - Short: "Create a new k6 script", - Long: `Create a new k6 script using one of the predefined templates. + Short: "Create and initialize a new k6 script", + Long: `Create and initialize a new k6 script using one of the predefined templates. By default, the script will be named script.js unless a different name is specified.`, Example: exampleText, From 6423d0971d3684176544904684bb175287c42b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 13:38:43 +0100 Subject: [PATCH 07/10] Simplify cloud logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new.go | 5 ++--- cmd/newtemplates/browser.js | 9 ++++----- cmd/newtemplates/minimal.js | 9 ++++----- cmd/newtemplates/protocol.js | 12 ++++++------ cmd/newtemplates/templates.go | 5 ++--- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 466325094ce..9cd8c2a5025 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -54,9 +54,8 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { } argsStruct := templates.TemplateArgs{ - ScriptName: target, - EnableCloud: c.projectID != "", - ProjectID: c.projectID, + ScriptName: target, + ProjectID: c.projectID, } if err := templates.ExecuteTemplate(fd, tmpl, argsStruct); err != nil { diff --git a/cmd/newtemplates/browser.js b/cmd/newtemplates/browser.js index 46dbc00f15a..5ef80094e7b 100644 --- a/cmd/newtemplates/browser.js +++ b/cmd/newtemplates/browser.js @@ -14,12 +14,11 @@ export const options = { }, }, }, - }, {{ if .EnableCloud }} - cloud: { {{ if .ProjectID }} - projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Replace this with your own projectID {{ end }} + },{{ if .ProjectID }} + cloud: { + projectID: {{ .ProjectID }}, name: "{{ .ScriptName }}", - }, {{ end }} + },{{ end }} }; export function setup() { diff --git a/cmd/newtemplates/minimal.js b/cmd/newtemplates/minimal.js index 191ced04806..716c08802ea 100644 --- a/cmd/newtemplates/minimal.js +++ b/cmd/newtemplates/minimal.js @@ -3,12 +3,11 @@ import { sleep, check } from 'k6'; export const options = { vus: 10, - duration: '30s',{{ if .EnableCloud }} - cloud: { {{ if .ProjectID }} - projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Replace this with your own projectID {{ end }} + duration: '30s',{{ if .ProjectID }} + cloud: { + projectID: {{ .ProjectID }}, name: "{{ .ScriptName }}", - }, {{ end }} + },{{ end }} }; export default function() { diff --git a/cmd/newtemplates/protocol.js b/cmd/newtemplates/protocol.js index 623be6fabe5..f2781c2d7c1 100644 --- a/cmd/newtemplates/protocol.js +++ b/cmd/newtemplates/protocol.js @@ -9,15 +9,15 @@ export const options = { { duration: "10s", target: 5 }, { duration: "20s", target: 10 }, { duration: "1s", target: 0 }, - ], thresholds: { + ], + thresholds: { http_req_failed: ["rate<0.01"], http_req_duration: ["p(95)<500", "p(99)<1000"], - },{{ if .EnableCloud }} - cloud: { {{ if .ProjectID }} - projectID: {{ .ProjectID }}, {{ else }} - // projectID: 12345, // Replace this with your own projectID {{ end }} + },{{ if .ProjectID }} + cloud: { + projectID: {{ .ProjectID }}, name: "{{ .ScriptName }}", - }, {{ end }} + },{{ end }} }; export function setup() { diff --git a/cmd/newtemplates/templates.go b/cmd/newtemplates/templates.go index 538480f6b4b..4b14ffc8a6f 100644 --- a/cmd/newtemplates/templates.go +++ b/cmd/newtemplates/templates.go @@ -28,9 +28,8 @@ var ( // TemplateArgs represents arguments passed to templates type TemplateArgs struct { - ScriptName string - EnableCloud bool - ProjectID string + ScriptName string + ProjectID string } // GetTemplate selects the appropriate template based on the type From 1d8d321b9fdd773b4040ef0cd76656efd29653de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 14:02:36 +0100 Subject: [PATCH 08/10] Make linter happy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/new.go | 26 +++++++++++++---- cmd/new_test.go | 1 + cmd/newtemplates/templates.go | 55 ++++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/cmd/new.go b/cmd/new.go index 9cd8c2a5025..4453d23b42e 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -28,7 +28,7 @@ func (c *newScriptCmd) flagSet() *pflag.FlagSet { return flags } -func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { +func (c *newScriptCmd) run(_ *cobra.Command, args []string) error { target := defaultNewScriptName if len(args) > 0 { target = args[0] @@ -46,9 +46,20 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer fd.Close() + defer func() { + if cerr := fd.Close(); cerr != nil { + if _, err := fmt.Fprintf(c.gs.Stderr, "error closing file: %v\n", cerr); err != nil { + panic(fmt.Sprintf("error writing error message to stderr: %v", err)) + } + } + }() + + tm, err := templates.NewTemplateManager() + if err != nil { + return err + } - tmpl, err := templates.GetTemplate(c.templateType) + tmpl, err := tm.GetTemplate(c.templateType) if err != nil { return err } @@ -62,7 +73,10 @@ func (c *newScriptCmd) run(cmd *cobra.Command, args []string) error { return err } - fmt.Fprintf(c.gs.Stdout, "New script created: %s (%s template).\n", target, c.templateType) + if _, err := fmt.Fprintf(c.gs.Stdout, "New script created: %s (%s template).\n", target, c.templateType); err != nil { + return err + } + return nil } @@ -72,10 +86,10 @@ func getCmdNewScript(gs *state.GlobalState) *cobra.Command { exampleText := getExampleText(c.gs, ` # Create a minimal k6 script $ {{.}} new --template minimal - + # Overwrite an existing file with a protocol-based script $ {{.}} new -f --template protocol test.js - + # Create a cloud-ready script with a specific project ID $ {{.}} new --project-id 12315`[1:]) diff --git a/cmd/new_test.go b/cmd/new_test.go index 2a0e6fd05fd..f451889feda 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -105,6 +105,7 @@ func TestNewScriptCmd_InvalidTemplateType(t *testing.T) { newRootCommand(ts.GlobalState).execute() assert.Contains(t, ts.Stderr.String(), "invalid template type") } + func TestNewScriptCmd_ProjectID(t *testing.T) { t.Parallel() diff --git a/cmd/newtemplates/templates.go b/cmd/newtemplates/templates.go index 4b14ffc8a6f..cb421bf9e28 100644 --- a/cmd/newtemplates/templates.go +++ b/cmd/newtemplates/templates.go @@ -7,8 +7,6 @@ import ( "text/template" ) -// Embed templates -// //go:embed minimal.js var minimalTemplateContent string @@ -18,34 +16,57 @@ var protocolTemplateContent string //go:embed browser.js var browserTemplateContent string -// Pre-parse templates -var ( - MinimalScriptTemplate = template.Must(template.New("minimal").Parse(minimalTemplateContent)) - ProtocolScriptTemplate = template.Must(template.New("protocol").Parse(protocolTemplateContent)) - BrowserScriptTemplate = template.Must(template.New("browser").Parse(browserTemplateContent)) - DefaultNewScriptName = "script.js" -) +// TemplateManager manages the pre-parsed templates +type TemplateManager struct { + minimalTemplate *template.Template + protocolTemplate *template.Template + browserTemplate *template.Template +} -// TemplateArgs represents arguments passed to templates -type TemplateArgs struct { - ScriptName string - ProjectID string +// NewTemplateManager initializes a new TemplateManager with parsed templates +func NewTemplateManager() (*TemplateManager, error) { + minimalTmpl, err := template.New("minimal").Parse(minimalTemplateContent) + if err != nil { + return nil, fmt.Errorf("failed to parse minimal template: %w", err) + } + + protocolTmpl, err := template.New("protocol").Parse(protocolTemplateContent) + if err != nil { + return nil, fmt.Errorf("failed to parse protocol template: %w", err) + } + + browserTmpl, err := template.New("browser").Parse(browserTemplateContent) + if err != nil { + return nil, fmt.Errorf("failed to parse browser template: %w", err) + } + + return &TemplateManager{ + minimalTemplate: minimalTmpl, + protocolTemplate: protocolTmpl, + browserTemplate: browserTmpl, + }, nil } // GetTemplate selects the appropriate template based on the type -func GetTemplate(templateType string) (*template.Template, error) { +func (tm *TemplateManager) GetTemplate(templateType string) (*template.Template, error) { switch templateType { case "minimal": - return MinimalScriptTemplate, nil + return tm.minimalTemplate, nil case "protocol": - return ProtocolScriptTemplate, nil + return tm.protocolTemplate, nil case "browser": - return BrowserScriptTemplate, nil + return tm.browserTemplate, nil default: return nil, fmt.Errorf("invalid template type: %s", templateType) } } +// TemplateArgs represents arguments passed to templates +type TemplateArgs struct { + ScriptName string + ProjectID string +} + // ExecuteTemplate applies the template with provided arguments and writes to the provided writer func ExecuteTemplate(w io.Writer, tmpl *template.Template, args TemplateArgs) error { return tmpl.Execute(w, args) From 5cf591630fc2f1592f4dfa87770b13a1676a7ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 14:24:37 +0100 Subject: [PATCH 09/10] Fix package name and description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/newtemplates/templates.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/newtemplates/templates.go b/cmd/newtemplates/templates.go index cb421bf9e28..3f8ed58cdff 100644 --- a/cmd/newtemplates/templates.go +++ b/cmd/newtemplates/templates.go @@ -1,4 +1,5 @@ -package cmd +// Package templates provides the templates for the k6 new command +package templates import ( _ "embed" From beda4b1c7d95e85c218aa6e9c55100a1e6e27ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gonz=C3=A1lez=20Lopes?= Date: Fri, 3 Jan 2025 14:27:28 +0100 Subject: [PATCH 10/10] Use Authorization header instead of User-ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel González Lopes --- cmd/newtemplates/protocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/newtemplates/protocol.js b/cmd/newtemplates/protocol.js index f2781c2d7c1..60d20c869e2 100644 --- a/cmd/newtemplates/protocol.js +++ b/cmd/newtemplates/protocol.js @@ -42,7 +42,7 @@ export default function() { let res = http.post(BASE_URL + "/api/pizza", JSON.stringify(restrictions), { headers: { 'Content-Type': 'application/json', - 'X-User-ID': 23423, + 'Authorization': 'token abcdef0123456789', }, });