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 possibility to override script values #138

Merged
merged 9 commits into from
Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions cmd/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var (
metricsGroupings []string
profTyp string
objDefFile string
scriptOverrides []string
)

// *** Custom errors ***
Expand Down
55 changes: 55 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"fmt"
"io/ioutil"
"os"
"strings"

jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
Expand Down Expand Up @@ -32,8 +34,12 @@ func AddAllSharedParameters(cmd *cobra.Command) {
// AddConfigParameter add config file parameter to command
func AddConfigParameter(cmd *cobra.Command) {
cmd.Flags().StringVarP(&cfgFile, "config", "c", "", `Scenario config file.`)

// Script overrides
cmd.Flags().StringArrayVarP(&scriptOverrides, "set", "s", nil, "Override a value in script with key.path=value.")
}

// AddLoggingParameters add logging parameters to command
func AddLoggingParameters(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&traffic, "traffic", "t", false, "Log traffic. Logging traffic is heavy and should only be done for debugging purposes.")
cmd.Flags().BoolVarP(&trafficMetrics, "trafficmetrics", "m", false, "Log traffic metrics.")
Expand All @@ -52,14 +58,50 @@ func unmarshalConfigFile() (*config.Config, error) {
return nil, errors.Wrapf(err, "Error reading config from file<%s>", cfgFile)
}

var overrides []string
cfgJSON, overrides, err = overrideScriptValues(cfgJSON)
if err != nil {
return nil, errors.WithStack(err)
}

var cfg config.Config
if err = jsonit.Unmarshal(cfgJSON, &cfg); err != nil {
return nil, errors.Wrap(err, "Failed to unmarshal config from json")
}

if cfg.Settings.LogSettings.Format != config.LogFormatNoLogs {
PrintOverrides(overrides)
}

return &cfg, nil
}

func overrideScriptValues(cfgJSON []byte) ([]byte, []string, error) {
overrides := make([]string, 0, len(scriptOverrides))
for _, kvp := range scriptOverrides {
kvSplit := strings.SplitN(kvp, "=", 2)
if len(kvSplit) != 2 {
return cfgJSON, overrides, errors.Errorf("malformed override: %s, should be in the form key.path=value", kvp)
DnlLrssn marked this conversation as resolved.
Show resolved Hide resolved
}
path := helpers.DataPath(kvSplit[0])
rawOrig, err := path.Lookup(cfgJSON)
Copy link
Contributor

Choose a reason for hiding this comment

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

Still not entirely sure about this one 🤔 Also if the override is a json object, the individual keys won't be checked. I do understand it's a good thing to throw errors whenever possible, though, so removing it outright is not the best option either.

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 an easy one. My thinking, right or not, is that mistakes will more likely be done when writing the path, if an entire object is used it's at least more likely to be copy-pasted. My vote is on keeping the check, but agree not optimal, don't have a better solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I tend to agree if the choice is between keeping it and removing it outright.

if err != nil {
return cfgJSON, overrides, errors.Wrap(err, "invalid script override")
}
cfgJSON, err = path.Set(cfgJSON, []byte(kvSplit[1]))
if err != nil {
return cfgJSON, overrides, errors.WithStack(err)
}
rawModified, err := path.Lookup(cfgJSON)
if err != nil {
return cfgJSON, overrides, errors.WithStack(err)
}
overrides = append(overrides, fmt.Sprintf("%s: %s -> %s\n", path, rawOrig, rawModified))
}

return cfgJSON, overrides, nil
}

func getLogFormatHelpString() string {
buf := helpers.NewBuffer()
buf.WriteString("Set a log format to be used. One of:\n")
Expand All @@ -82,6 +124,7 @@ func getSummaryTypeHelpString() string {
return buf.String()
}

// ConfigOverrideLogSettings override log settings with parameters
func ConfigOverrideLogSettings(cfg *config.Config) error {
if trafficMetrics {
cfg.SetTrafficMetricsLogging()
Expand Down Expand Up @@ -113,3 +156,15 @@ func ConfigOverrideLogSettings(cfg *config.Config) error {

return nil
}

// PrintOverrides to script
func PrintOverrides(overrides []string) {
if len(overrides) < 1 {
return
}
os.Stderr.WriteString("=== Script overrides ===\n")
for _, override := range overrides {
os.Stderr.WriteString(override)
}
os.Stderr.WriteString("========================\n")
}
2 changes: 1 addition & 1 deletion cmd/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cmd
import (
"context"
"fmt"
"github.com/qlik-oss/gopherciser/appstructure"
"os"

"github.com/qlik-oss/gopherciser/appstructure"
"github.com/qlik-oss/gopherciser/config"
"github.com/spf13/cobra"
)
Expand Down
49 changes: 49 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Flags:

* `-c`, `--config string`: Load the specified scenario setup file.
* `--debug`: Log debug information.
* `-d`, `--definitions`: Custom object definitions and overrides.
* `-h`, `--help`: Show the help for the `execute` command.
* `--logformat string`: Set the specified log format. The log format specified in the scenario setup file is used by default. If no log format is specified, `tsvfile` is used.
* `0` or `tsvfile`: TSV file
Expand All @@ -74,6 +75,7 @@ Flags:
* `6` or `mutex`: Mutex
* `7` or `trace`: Trace
* `8` or `mem`: Mem
* `-s`, `--set`: Override a value in script with key.path=value. See [Using script overrides](#using-script-overrides) for further explanation.
* `--summary string`: Set the type of summary to display after the test run. Defaults to `simple` for minimal performance impact.
* `0` or `undefined`: Simple, single-row summary
* `1` or `none`: No summary
Expand Down Expand Up @@ -137,6 +139,7 @@ Sub-commands:

* `-c`, `--config string`: Connect using the specified scenario config file.
* `-h`, `--help`: Show the help for the `connect` command.
* `-s`, `--set`: Override a value in script with key.path=value. See [Using script overrides](#using-script-overrides) for further explanation.

`structure` command flags:

Expand All @@ -162,18 +165,64 @@ Sub-commands:
* `4` or `full`: Currently the same as the `extended` summary, includes a list of all objects in the structure.
* `-t`, `--traffic`: Log traffic information.
* `-m`, `--trafficmetrics`: Log metrics information.
* `-s`, `--set`: Override a value in script with key.path=value. See [Using script overrides](#using-script-overrides) for further explanation.

`validate` command flags:

* `-c`, `--config string`: Load the specified scenario setup file.
* `-h`, `--help`: Show the help for the `validate` command.
* `-s`, `--set`: Override a value in script with key.path=value. See [Using script overrides](#using-script-overrides) for further explanation.

`template` command flags:

* `-c`, `--config string`: (optional) Create the specified scenario setup file. Defaults to `template.json`.
* `-f`, `--force`: Overwrite existing scenario setup file.
* `-h`, `--help`: Show the help for the `template` command.

#### Using script overrides

Script overrides overrides a value pointed to by a path to its key. If the key doesn't exist in the script there will be a not found error, even if it's a valid value according to config.
Copy link
Contributor

Choose a reason for hiding this comment

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

A script override overrides a value..
or
Script overrides override values..

Copy link
Contributor

Choose a reason for hiding this comment

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

not-found-error? "not found"-error?

Copy link
Member Author

@DnlLrssn DnlLrssn Oct 29, 2020

Choose a reason for hiding this comment

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

If the key doesn't exist in the script there will an error, even if it's a valid value according to config.

?


The syntax is path/to/key=value. A common thing to override would be the settings of the simple scheduler.

```json
"scheduler": {
"type": "simple",
"settings": {
"executiontime": -1,
"iterations": 1,
"rampupdelay": 1.0,
"concurrentusers": 1
}
}
```

`scheduler` is in the root of the JSON, so the path to the key of `concurrentusers` would be `scheduler/settings/concurrentusers`. To override concurrent users from command line:

```bash
./gopherciser x -c ./docs/examples/sheetChangerQlikCore.json -s 'scheduler/settings/concurrentusers=2'
```

Overriding a string, such as the server the servername requires it to be wrapped with double quotes. E.g. to override the server:

```bash
./gopherciser x -c ./docs/examples/sheetChangerQlikCore.json -s 'connectionSettings/server="127.0.0.1"'
```

Do note that the path is case sensitive. It needs to be `connectionSettings/server` as `connectionsettings/server` would try, and fail, to add new key called `connectionsettings`.

Overrides could also be used to with more advanced paths. If the position in `scenario` is known for `openapp` we could replace e.g. the app opened, assuming `openapp` is the first action in `scenario`:

```bash
./gopherciser x -c ./docs/examples/sheetChangerQlikCore.json -s 'scenario/[0]/settings/app="mynewapp"'
```

It could even replace an entire JSON object such as the `connectionSettings` with one replace call:

```bash
./gopherciser x -c ./docs/examples/sheetChangerQlikCore.json -s 'connectionSettings={"mode":"ws","server":"127.0.0.1","port":19076}'
```

## Analyzing the test results

A log file is recorded during each test execution. The `logs.filename` setting in the `settings` section of the load test script specifies the name of and the path to the log file (see [Setting up load scenarios](./settingup.md)). If a file with the specified filename already exists, a number is appended to the filename (for example, if the file `xxxxx.yyy` already exists, the log file is stored as `xxxxx-001.yyy`).
Expand Down
25 changes: 21 additions & 4 deletions helpers/datapath.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ func (path *DataPath) String() string {
return string(*path)
}

//steps path substrings splitted on / (slash)
// steps path substrings splitted on / (slash)
func (path *DataPath) steps() []string {
return strings.Split(strings.Trim(path.String(), "/"), "/")
}

//Lookup object in path, if data found is of type string it will be quoted with ""
// Lookup object in path, if data found is of type string it will be quoted with ""
func (path DataPath) Lookup(data json.RawMessage) (json.RawMessage, error) {
return path.lookup(data, true)
}

//Lookup object in path, data of type string will not be quoted
// LookupNoQuotes object in path, data of type string will not be quoted
func (path DataPath) LookupNoQuotes(data json.RawMessage) (json.RawMessage, error) {
return path.lookup(data, false)
}
Expand Down Expand Up @@ -78,7 +78,24 @@ func (path DataPath) lookup(data json.RawMessage, quoteString bool) (json.RawMes
}
}

//LookupMulti objects with subpaths under an array in a path
// Set look object in path and set to new object
func (path DataPath) Set(data []byte, newValue []byte) ([]byte, error) {
steps := path.steps()

if steps == nil || len(steps) < 1 {
return data, errors.WithStack(NoStepsError(string(path)))
}

var err error
data, err = jsonparser.Set([]byte(data), newValue, steps...)
if err != nil {
return nil, errors.Wrapf(err, "failed to set data<%s> at path<%s>", newValue, path)
}

return data, nil
}

// LookupMulti objects with subpaths under an array in a path
func (path DataPath) LookupMulti(data json.RawMessage, separator string) ([]json.RawMessage, error) {
// todo change to use jsonparser
if separator == "" || !strings.Contains(string(path), separator) {
Expand Down
86 changes: 86 additions & 0 deletions helpers/datapath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package helpers

import (
"encoding/json"
"fmt"
"testing"
)

Expand Down Expand Up @@ -44,6 +45,91 @@ func TestMultiLookup(t *testing.T) {
}
}

type LookupAndSetResult struct {
Node struct {
Intval int
Floatval float64
Stringval string
}
}

func TestLookupAndSet(t *testing.T) {
data := []byte(`{
"node" : {
"intval" : 2,
"floatval": 4.2,
"stringval": "string1"
}
}`)
t.Logf("data: %s", data)

tests := []struct {
Path DataPath
NewData []byte
ExpectedValue interface{}
}{
{
Path: DataPath("node/intval"),
NewData: []byte("4"),
ExpectedValue: 4,
},
{
Path: DataPath("node/floatval"),
NewData: []byte("5.2"),
ExpectedValue: 5.2,
},
{
Path: DataPath("node/stringval"),
NewData: []byte(`"yadda"`),
ExpectedValue: "yadda",
},
{
Path: DataPath("new/path/novaluehere"),
NewData: []byte("404"),
ExpectedValue: 404,
},
{
Path: DataPath("nodenode"),
NewData: []byte(`{"newkey":"newvalue"}`),
ExpectedValue: `{"newkey":"newvalue"}`,
},
}

for i, test := range tests {
t.Run(fmt.Sprintf("TestLookupAndSet%d", i), func(t *testing.T) {
// Lookup and modify
t.Logf("%s;%s;%v", test.Path, string(test.NewData), test.ExpectedValue)
modifiedData, err := test.Path.Set(data, test.NewData)
if len(modifiedData) > 0 {
t.Logf("modified data: %s", modifiedData)
}
if err != nil {
t.Fatal(err)
}

// unmarshal results
var result LookupAndSetResult
err = jsonit.Unmarshal(modifiedData, &result)
if err != nil {
t.Fatal(err)
}

// validate data
newVal, err := test.Path.LookupNoQuotes(modifiedData)
if err != nil {
t.Fatal(err)
}

// compare values as strings
newValueAsString := string(newVal)
expectedValueAsString := fmt.Sprintf("%v", test.ExpectedValue)
if newValueAsString != expectedValueAsString {
t.Errorf("unexpected result, got<%s> expected<%s>", newValueAsString, expectedValueAsString)
}
})
}
}

//TODO Both Lookup and MultiLookup needs performance optimizations

func BenchmarkMultiLookup(b *testing.B) {
Expand Down