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

Add HAR converter options #694

Merged
merged 6 commits into from
Jul 6, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions cmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -108,11 +130,14 @@ 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)")
convertCmd.Flags().BoolVarP(&nobatch, "no-batch", "", false, "don't generate batch calls")
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")
}
6 changes: 5 additions & 1 deletion cmd/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion cmd/testdata/example.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions converter/har/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"sort"
"strings"

"github.com/loadimpact/k6/lib"
"github.com/pkg/errors"
"github.com/tidwall/pretty"
)
Expand All @@ -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")
}
Expand All @@ -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")

Expand Down Expand Up @@ -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]
Expand Down
28 changes: 28 additions & 0 deletions lib/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package lib

import (
"bytes"
"crypto/tls"
"encoding/json"
"net"
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we could perhaps compare the keys from the user supplied JSON file + any options from the converter itself (like maxRedirects) with the keys in the options struct and only marshal the ones that have been specified (via some sort of intermediary data struct/container, like you use below for removing the null values). Maybe that's what you mean by "use reflect to do it".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly, we can just enumerate struct fields with reflect (in order, compared to the unordered map traversal in Go) and check each one for a nil or a false value of its Valid flag. That way we'll also be able to output invalid JSON, but valid JS object syntax like this (notice the lack of quotes around the key name):

{
    maxRedirects: 0
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't do it now because reflect is quite finicky and I couldn't be sure that I'd get all of the edge cases correctly..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// 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)
}
1 change: 1 addition & 0 deletions release notes/upcoming.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ console.log(rnd)
```

* A new option `--no-vu-connection-reuse` lets users close HTTP `keep-alive` connections between iterations of a VU. (#676)
* New options were added to the HAR converter. You can set the minimum and maximum sleep time at the end of an iteration with the new `--min-sleep` and `--max-sleep` CLI flags of `k6 convert`. You can also 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd separate the --min-sleep/--max-sleep and --options into two separate items.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 8660bad



## UX
Expand Down