diff --git a/cmd/convert.go b/cmd/convert.go index 15682f910a3..5797109f8da 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -21,16 +21,22 @@ package cmd import ( + "encoding/json" "io" + "io/ioutil" "path/filepath" "github.com/loadimpact/k6/converter/har" + "github.com/loadimpact/k6/lib" "github.com/spf13/cobra" + null "gopkg.in/guregu/null.v3" ) -var output = "" - var ( + output string + optionsFilePath string + minSleep uint + maxSleep uint enableChecks bool returnOnFailedCheck bool correlate bool @@ -75,7 +81,23 @@ var convertCmd = &cobra.Command{ return err } - script, err := har.Convert(h, enableChecks, returnOnFailedCheck, threshold, nobatch, correlate, only, skip) + // recordings include redirections as separate requests, and we dont want to trigger them twice + options := lib.Options{MaxRedirects: null.IntFrom(0)} + + if optionsFilePath != "" { + optionsFileContents, err := ioutil.ReadFile(optionsFilePath) + if err != nil { + return err + } + var injectedOptions lib.Options + if err := json.Unmarshal(optionsFileContents, &injectedOptions); err != nil { + return err + } + options = options.Apply(injectedOptions) + } + + //TODO: refactor... + script, err := har.Convert(h, options, minSleep, maxSleep, enableChecks, returnOnFailedCheck, threshold, nobatch, correlate, only, skip) if err != nil { return err } @@ -108,6 +130,7 @@ func init() { RootCmd.AddCommand(convertCmd) convertCmd.Flags().SortFlags = false convertCmd.Flags().StringVarP(&output, "output", "O", output, "k6 script output filename (stdout by default)") + convertCmd.Flags().StringVarP(&optionsFilePath, "options", "", output, "path to a JSON file with options that would be injected in the output script") convertCmd.Flags().StringSliceVarP(&only, "only", "", []string{}, "include only requests from the given domains") convertCmd.Flags().StringSliceVarP(&skip, "skip", "", []string{}, "skip requests from the given domains") convertCmd.Flags().UintVarP(&threshold, "batch-threshold", "", 500, "batch request idle time threshold (see example)") @@ -115,4 +138,6 @@ func init() { convertCmd.Flags().BoolVarP(&enableChecks, "enable-status-code-checks", "", false, "add a status code check for each HTTP response") convertCmd.Flags().BoolVarP(&returnOnFailedCheck, "return-on-failed-check", "", false, "return from iteration if we get an unexpected response status code") convertCmd.Flags().BoolVarP(&correlate, "correlate", "", false, "detect values in responses being used in subsequent requests and try adapt the script accordingly (only redirects and JSON values for now)") + convertCmd.Flags().UintVarP(&minSleep, "min-sleep", "", 20, "the minimum amount of seconds to sleep after each iteration") + convertCmd.Flags().UintVarP(&maxSleep, "max-sleep", "", 40, "the maximum amount of seconds to sleep after each iteration") } diff --git a/cmd/convert_test.go b/cmd/convert_test.go index 46046e08ac9..09f92700d46 100644 --- a/cmd/convert_test.go +++ b/cmd/convert_test.go @@ -94,7 +94,9 @@ import http from 'k6/http'; // Version: 1.2 // Creator: WebInspector -export let options = { maxRedirects: 0 }; +export let options = { + "maxRedirects": 0 +}; export default function() { @@ -185,4 +187,6 @@ func TestIntegrationConvertCmd(t *testing.T) { assert.NoError(t, err) assert.Equal(t, testHARConvertResult, string(output)) }) + // TODO: test options injection; right now that's difficult because when there are multiple + // options, they can be emitted in different order in the JSON } diff --git a/cmd/testdata/example.js b/cmd/testdata/example.js index e8c132fbf99..3064adf74a1 100644 --- a/cmd/testdata/example.js +++ b/cmd/testdata/example.js @@ -4,7 +4,9 @@ import http from 'k6/http'; // Version: 1.2 // Creator: BrowserMob Proxy -export let options = { maxRedirects: 0 }; +export let options = { + "maxRedirects": 0 +}; export default function() { diff --git a/converter/har/converter.go b/converter/har/converter.go index e0bb0054b41..aeb6284eca9 100644 --- a/converter/har/converter.go +++ b/converter/har/converter.go @@ -30,6 +30,7 @@ import ( "sort" "strings" + "github.com/loadimpact/k6/lib" "github.com/pkg/errors" "github.com/tidwall/pretty" ) @@ -56,10 +57,16 @@ func fprintf(w io.Writer, format string, a ...interface{}) int { return n } -func Convert(h HAR, enableChecks bool, returnOnFailedCheck bool, batchTime uint, nobatch bool, correlate bool, only, skip []string) (string, error) { +// TODO: refactor this to have fewer parameters... or just refactor in general... +func Convert(h HAR, options lib.Options, minSleep, maxSleep uint, enableChecks bool, returnOnFailedCheck bool, batchTime uint, nobatch bool, correlate bool, only, skip []string) (string, error) { var b bytes.Buffer w := bufio.NewWriter(&b) + scriptOptionsSrc, err := options.GetPrettyJSON("", " ") + if err != nil { + return "", err + } + if returnOnFailedCheck && !enableChecks { return "", errors.Errorf("return on failed check requires --enable-status-code-checks") } @@ -84,8 +91,7 @@ func Convert(h HAR, enableChecks bool, returnOnFailedCheck bool, batchTime uint, fprintf(w, "// %v\n", h.Log.Comment) } - // recordings include redirections as separate requests, and we dont want to trigger them twice - fprint(w, "\nexport let options = { maxRedirects: 0 };\n\n") + fprintf(w, "\nexport let options = %s;\n\n", scriptOptionsSrc) fprint(w, "export default function() {\n\n") @@ -274,8 +280,8 @@ func Convert(h HAR, enableChecks bool, returnOnFailedCheck bool, batchTime uint, if i == len(pages)-1 { // Last page; add random sleep time at the group completion - fprint(w, "\t\t// Random sleep between 20s and 40s\n") - fprint(w, "\t\tsleep(Math.floor(Math.random()*20+20));\n") + fprintf(w, "\t\t// Random sleep between %ds and %ds\n", minSleep, maxSleep) + fprintf(w, "\t\tsleep(Math.floor(Math.random()*%d+%d));\n", maxSleep-minSleep, minSleep) } else { // Add sleep time at the end of the group nextPage := pages[i+1] diff --git a/lib/options.go b/lib/options.go index af534298c33..cf69cc8d6a3 100644 --- a/lib/options.go +++ b/lib/options.go @@ -21,6 +21,7 @@ package lib import ( + "bytes" "crypto/tls" "encoding/json" "net" @@ -365,3 +366,30 @@ func (o Options) Apply(opts Options) Options { } return o } + +// GetPrettyJSON is a massive hack that works around the fact that some +// of the null-able types used in Options are marshalled to `null` when +// their `valid` flag is false. +// TODO: figure out something better or at least use reflect to do it, that +// way field order could be preserved, we could optionally emit JS objects +// (without mandatory quoted keys)` and we won't needlessly marshal and +// unmarshal things... +func (o Options) GetPrettyJSON(prefix, indent string) ([]byte, error) { + nullyResult, err := json.MarshalIndent(o, prefix, indent) + if err != nil { + return nil, err + } + + var tmpMap map[string]json.RawMessage + if err := json.Unmarshal(nullyResult, &tmpMap); err != nil { + return nil, err + } + + null := []byte("null") + for k, v := range tmpMap { + if bytes.Equal(v, null) { + delete(tmpMap, k) + } + } + return json.MarshalIndent(tmpMap, prefix, indent) +} diff --git a/release notes/upcoming.md b/release notes/upcoming.md index df80f639861..d593a2490ac 100644 --- a/release notes/upcoming.md +++ b/release notes/upcoming.md @@ -12,6 +12,8 @@ console.log(rnd) ``` * A new option `--no-vu-connection-reuse` lets users close HTTP `keep-alive` connections between iterations of a VU. (#676) +* You can now set the minimum and maximum sleep time at the end of an iteration with the new `--min-sleep` and `--max-sleep` HAR coverter CLI flags. (#694) +* Another new feature in the HAR converter enabled users to specify a JSON file with [script options](https://docs.k6.io/docs/options) that would be added to the options of the generated scripts with the new `--options` flag. (#694) ## UX