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

feat(tests): add range supports #453

Merged
merged 6 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ It can also generate xUnit result files.
* [Advanced usage](#advanced-usage)
* [Debug your testsuites](#debug-your-testsuites)
* [Skip testcase](#skip-testcase)
* [Iterating over data](#iterating-over-data)
* [Use venom in CI](#use-venom-in-ci)
* [Hacking](#hacking)
* [License](#license)
Expand Down Expand Up @@ -508,6 +509,42 @@ testcases:

```

## Iterating over data

It is possible to iterate over data using `range` attribute.

The following data types are supported, each exposing contexted variables `.index`, `.key` and `.value`:

- An array where each value will be iterated over (`[]interface{}`)
- `.index`/`.key`: current iteration index
- `.value`: current iteration item value
- A map where each key will be iterated over (`map[string]interface{}`)
- `.index`: current iteration index
- `.key`: current iteration item key
- `.value`: current iteration item value
- An integer to perform target step `n` times (`int`)
- `.index`/`.key`/`.value`: current iteration index
- A templated string which results in one of the above typing (`string`)
- It can be either inherited from vars file, or interpolated from a previous step result

For instance, the following example will iterate over an array of two items containing maps:
```yaml
- name: range with harcoded array
steps:
- type: exec
range:
- actual: hello
expected: hello
- actual: world
expected: world
script: echo "{{.value.actual}}"
assertions:
- result.code ShouldEqual 0
- result.systemout ShouldEqual "{{.value.expected}}"
```

More examples are available in [`tests/ranged.yml`](/tests/ranged.yml).

# FAQ

## Common errors with quotes
Expand Down
256 changes: 186 additions & 70 deletions process_testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"

"github.com/ghodss/yaml"
Expand Down Expand Up @@ -164,114 +165,229 @@ func (v *Venom) runTestSteps(ctx context.Context, tc *TestCase) {
stepVars.AddAllWithPrefix(tc.Name, tc.computedVars)
stepVars.Add("venom.teststep.number", stepNumber)

vars, err := DumpStringPreserveCase(stepVars)
ranged, err := parseRanged(ctx, rawStep, stepVars)
if err != nil {
Error(ctx, "unable to dump testcase vars: %v", err)
Error(ctx, "unable to parse \"range\" attribute: %v", err)
tc.AppendError(err)
return
}

for k, v := range vars {
content, err := interpolate.Do(v, vars)
for rangedIndex, rangedData := range ranged.Items {
if ranged.Enabled {
Debug(ctx, "processing step %d", rangedIndex)
stepVars.Add("index", rangedIndex)
stepVars.Add("key", rangedData.Key)
stepVars.Add("value", rangedData.Value)
}

vars, err := DumpStringPreserveCase(stepVars)
if err != nil {
Error(ctx, "unable to dump testcase vars: %v", err)
tc.AppendError(err)
Error(ctx, "unable to interpolate variable %q: %v", v, err)
return
}
vars[k] = content
}

// the value of each var can contains a double-quote -> "
// if the value is not escaped, it will be used as is, and the json sent to unmarshall will be incorrect.
// This also avoids injections into the json structure of a step
for i := range vars {
vars[i] = strings.ReplaceAll(vars[i], "\"", "\\\"")
}
for k, v := range vars {
content, err := interpolate.Do(v, vars)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to interpolate variable %q: %v", v, err)
return
}
vars[k] = content
}

var content string
for i := 0; i < 10; i++ {
content, err = interpolate.Do(string(rawStep), vars)
if err != nil {
// the value of each var can contains a double-quote -> "
// if the value is not escaped, it will be used as is, and the json sent to unmarshall will be incorrect.
// This also avoids injections into the json structure of a step
for i := range vars {
vars[i] = strings.ReplaceAll(vars[i], "\"", "\\\"")
}

var content string
for i := 0; i < 10; i++ {
content, err = interpolate.Do(string(rawStep), vars)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to interpolate step: %v", err)
return
}
if !strings.Contains(content, "{{") {
break
}
}

Info(ctx, "Step #%d content is: %q", stepNumber, content)
var step TestStep
if err := yaml.Unmarshal([]byte(content), &step); err != nil {
tc.AppendError(err)
Error(ctx, "unable to interpolate step: %v", err)
Error(ctx, "unable to unmarshal step: %v", err)
return
}
if !strings.Contains(content, "{{") {

tc.testSteps = append(tc.testSteps, step)
var e ExecutorRunner
ctx, e, err = v.GetExecutorRunner(ctx, step, tc.Vars)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to get executor: %v", err)
break
}
}

Info(ctx, "Step #%d content is: %q", stepNumber, content)
var step TestStep
if err := yaml.Unmarshal([]byte(content), &step); err != nil {
tc.AppendError(err)
Error(ctx, "unable to unmarshal step: %v", err)
return
}
_, known := knowExecutors[e.Name()]
if !known {
knowExecutors[e.Name()] = struct{}{}
ctx, err = e.Setup(ctx, tc.Vars)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to setup executor: %v", err)
break
}
defer func(ctx context.Context) {
if err := e.TearDown(ctx); err != nil {
tc.AppendError(err)
Error(ctx, "unable to teardown executor: %v", err)
}
}(ctx)
}

tc.testSteps = append(tc.testSteps, step)
var e ExecutorRunner
ctx, e, err = v.GetExecutorRunner(ctx, step, tc.Vars)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to get executor: %v", err)
break
}
v.RunTestStep(ctx, e, tc, stepNumber, step)

tc.testSteps = append(tc.testSteps, step)

var hasFailed bool
if len(tc.Failures) > 0 {
for _, f := range tc.Failures {
Warning(ctx, "%v", f)
}
hasFailed = true
}

_, known := knowExecutors[e.Name()]
if !known {
knowExecutors[e.Name()] = struct{}{}
ctx, err = e.Setup(ctx, tc.Vars)
if len(tc.Errors) > 0 {
Error(ctx, "Errors: ")
for _, e := range tc.Errors {
Error(ctx, "%v", e)
}
hasFailed = true
}

if hasFailed {
break
}

allVars := tc.Vars.Clone()
allVars.AddAll(tc.computedVars.Clone())

assign, _, err := processVariableAssigments(ctx, tc.Name, allVars, rawStep)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to setup executor: %v", err)
Error(ctx, "unable to process variable assignments: %v", err)
break
}
defer func(ctx context.Context) {
if err := e.TearDown(ctx); err != nil {
tc.AppendError(err)
Error(ctx, "unable to teardown executor: %v", err)
}
}(ctx)

tc.computedVars.AddAll(assign)
tc.Vars.AddAll(tc.computedVars)
}
}
}

v.RunTestStep(ctx, e, tc, stepNumber, step)
//Parse and format range data to allow iterations over user data
func parseRanged(ctx context.Context, rawStep []byte, stepVars H) (Range, error) {

tc.testSteps = append(tc.testSteps, step)
//Load "range" attribute and perform actions depending on its typing
var ranged Range
if err := json.Unmarshal(rawStep, &ranged); err != nil {
return ranged, fmt.Errorf("unable to parse range expression: %v", err)
}

var hasFailed bool
if len(tc.Failures) > 0 {
for _, f := range tc.Failures {
Warning(ctx, "%v", f)
}
hasFailed = true
switch ranged.RawContent.(type) {

//Nil means this is not a ranged data, append an empty item to force at least one iteration and exit
case nil:
ranged.Items = append(ranged.Items, RangeData{})
return ranged, nil

//String needs to be parsed and possibly templated
case string:
Debug(ctx, "attempting to parse range expression")
rawString := ranged.RawContent.(string)
if len(rawString) == 0 {
return ranged, fmt.Errorf("range expression has been specified without any data")
}

if len(tc.Errors) > 0 {
Error(ctx, "Errors: ")
for _, e := range tc.Errors {
Error(ctx, "%v", e)
// Try parsing already templated data
err := json.Unmarshal([]byte("{\"range\":"+rawString+"}"), &ranged)
// ... or fallback
if err != nil {
//Try templating and escaping data
Debug(ctx, "attempting to template range expression and parse it again")
vars, err := DumpStringPreserveCase(stepVars)
if err != nil {
Warn(ctx, "failed to parse range expression when loading step variables: %v", err)
break
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
}
for i := range vars {
vars[i] = strings.ReplaceAll(vars[i], "\"", "\\\"")
}
content, err := interpolate.Do(string(rawStep), vars)
if err != nil {
Warn(ctx, "failed to parse range expression when templating variables: %v", err)
break
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
}

//Try parsing data
err = json.Unmarshal([]byte(content), &ranged)
if err != nil {
Warn(ctx, "failed to parse range expression when parsing data into raw string: %v", err)
break
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
}
switch ranged.RawContent.(type) {
case string:
rawString = ranged.RawContent.(string)
err := json.Unmarshal([]byte("{\"range\":"+rawString+"}"), &ranged)
if err != nil {
Warn(ctx, "failed to parse range expression when parsing raw string into data: %v", err)
return ranged, fmt.Errorf("unable to parse range expression: unable to transform string data into a supported range expression type")
}
}
hasFailed = true
}
}

//Format data
switch t := ranged.RawContent.(type) {

if hasFailed {
break
//Array-like data
case []interface{}:
Debug(ctx, "\"range\" data is array-like")
for index, value := range ranged.RawContent.([]interface{}) {
key := strconv.Itoa(index)
ranged.Items = append(ranged.Items, RangeData{key, value})
}

allVars := tc.Vars.Clone()
allVars.AddAll(tc.computedVars.Clone())
//Number data
case float64:
Debug(ctx, "\"range\" data is number-like")
upperBound := int(ranged.RawContent.(float64))
for i := 0; i < upperBound; i++ {
key := strconv.Itoa(i)
ranged.Items = append(ranged.Items, RangeData{key, i})
}

assign, _, err := processVariableAssigments(ctx, tc.Name, allVars, rawStep)
if err != nil {
tc.AppendError(err)
Error(ctx, "unable to process variable assignments: %v", err)
break
//Map-like data
case map[string]interface{}:
lowlighter marked this conversation as resolved.
Show resolved Hide resolved
Debug(ctx, "\"range\" data is map-like")
for key, value := range ranged.RawContent.(map[string]interface{}) {
ranged.Items = append(ranged.Items, RangeData{key, value})
}

tc.computedVars.AddAll(assign)
tc.Vars.AddAll(tc.computedVars)
//Unsupported data format
default:
return ranged, fmt.Errorf("\"range\" was provided an unsupported type %T", t)
}

ranged.Enabled = true
ranged.RawContent = nil
return ranged, nil
}

func processVariableAssigments(ctx context.Context, tcName string, tcVars H, rawStep json.RawMessage) (H, bool, error) {
Expand Down
Loading