diff --git a/convert.go b/convert.go index 7f1bc830..2bfd6d08 100644 --- a/convert.go +++ b/convert.go @@ -146,6 +146,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) { if err != nil { return nil, err } + tc.Config.Path = path.Path testcase, err := tc.ToTestCase() if err != nil { return nil, err diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6858eaa7..7023b21f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,9 +1,13 @@ # Release History +## v0.5.0 (2022-01-06) + +- feat: support creating and calling custom functions with [go plugin](https://pkg.go.dev/plugin) + ## v0.4.0 (2022-01-05) - feat: implement `parameterize` mechanism for data driven -- feat: add multiple builtin assertion methods +- feat: add multiple builtin assertion methods and builtin functions ## v0.3.1 (2021-12-30) diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 8b751a2d..af644700 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -32,4 +32,4 @@ Copyright 2021 debugtalk * [hrp har2case](hrp_har2case.md) - Convert HAR to json/yaml testcase files * [hrp run](hrp_run.md) - run API test -###### Auto generated by spf13/cobra on 30-Dec-2021 +###### Auto generated by spf13/cobra on 6-Jan-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 494ae8d6..5690fc09 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -38,4 +38,4 @@ hrp boom [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 30-Dec-2021 +###### Auto generated by spf13/cobra on 6-Jan-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index d9664517..144caa8c 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -23,4 +23,4 @@ hrp har2case harPath... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 30-Dec-2021 +###### Auto generated by spf13/cobra on 6-Jan-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 9cb28dbf..a0f5d0db 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -31,4 +31,4 @@ hrp run path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 30-Dec-2021 +###### Auto generated by spf13/cobra on 6-Jan-2022 diff --git a/examples/plugin/debugtalk.go b/examples/plugin/debugtalk.go new file mode 100644 index 00000000..ce98e20c --- /dev/null +++ b/examples/plugin/debugtalk.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "log" +) + +func init() { + log.Println("plugin init function called") +} + +func Concatenate(a int, b string, c float64) string { + return fmt.Sprintf("%v_%v_%v", a, b, c) +} diff --git a/internal/version/init.go b/internal/version/init.go index 462025a5..8b4b8520 100644 --- a/internal/version/init.go +++ b/internal/version/init.go @@ -1,3 +1,3 @@ package version -const VERSION = "v0.4.0" +const VERSION = "v0.5.0" diff --git a/models.go b/models.go index b7c7b915..ebd254b1 100644 --- a/models.go +++ b/models.go @@ -27,6 +27,7 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path } type TParamsConfig struct { diff --git a/parser.go b/parser.go index ef10fab9..49062ae2 100644 --- a/parser.go +++ b/parser.go @@ -4,6 +4,9 @@ import ( "encoding/json" "fmt" "net/url" + "os" + "path/filepath" + "plugin" "reflect" "regexp" "strings" @@ -11,10 +14,55 @@ import ( "github.com/maja42/goval" "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/hrp/internal/builtin" ) +func newParser() *parser { + return &parser{} +} + +type parser struct { + // pluginLoader stores loaded go plugins. + pluginLoader *plugin.Plugin +} + +// locatePlugin searches debugtalk.so upward recursively until current +// working directory or system root dir. +func locatePlugin(startPath string) (string, error) { + stat, err := os.Stat(startPath) + if os.IsNotExist(err) { + return "", err + } + + var startDir string + if stat.IsDir() { + startDir = startPath + } else { + startDir = filepath.Dir(startPath) + } + startDir, _ = filepath.Abs(startDir) + + // convention over configuration + // target plugin file name is always debugtalk.so + pluginPath := filepath.Join(startDir, "debugtalk.so") + if _, err := os.Stat(pluginPath); err == nil { + return pluginPath, nil + } + + // current working directory + cwd, _ := os.Getwd() + if startDir == cwd { + return "", fmt.Errorf("searched to CWD, plugin file not found") + } + + // system root dir + parentDir, _ := filepath.Abs(filepath.Dir(startDir)) + if parentDir == startDir { + return "", fmt.Errorf("searched to system root dir, plugin file not found") + } + + return locatePlugin(parentDir) +} + func buildURL(baseURL, stepURL string) string { uConfig, err := url.Parse(baseURL) if err != nil { @@ -32,9 +80,9 @@ func buildURL(baseURL, stepURL string) string { return uStep.String() } -func parseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) { +func (p *parser) parseHeaders(rawHeaders map[string]string, variablesMapping map[string]interface{}) (map[string]string, error) { parsedHeaders := make(map[string]string) - headers, err := parseData(rawHeaders, variablesMapping) + headers, err := p.parseData(rawHeaders, variablesMapping) if err != nil { return rawHeaders, err } @@ -54,7 +102,7 @@ func convertString(raw interface{}) string { } } -func parseData(raw interface{}, variablesMapping map[string]interface{}) (interface{}, error) { +func (p *parser) parseData(raw interface{}, variablesMapping map[string]interface{}) (interface{}, error) { rawValue := reflect.ValueOf(raw) switch rawValue.Kind() { case reflect.String: @@ -65,11 +113,11 @@ func parseData(raw interface{}, variablesMapping map[string]interface{}) (interf // other string value := rawValue.String() value = strings.TrimSpace(value) - return parseString(value, variablesMapping) + return p.parseString(value, variablesMapping) case reflect.Slice: parsedSlice := make([]interface{}, rawValue.Len()) for i := 0; i < rawValue.Len(); i++ { - parsedValue, err := parseData(rawValue.Index(i).Interface(), variablesMapping) + parsedValue, err := p.parseData(rawValue.Index(i).Interface(), variablesMapping) if err != nil { return raw, err } @@ -79,12 +127,12 @@ func parseData(raw interface{}, variablesMapping map[string]interface{}) (interf case reflect.Map: // convert any map to map[string]interface{} parsedMap := make(map[string]interface{}) for _, k := range rawValue.MapKeys() { - parsedKey, err := parseString(k.String(), variablesMapping) + parsedKey, err := p.parseString(k.String(), variablesMapping) if err != nil { return raw, err } v := rawValue.MapIndex(k) - parsedValue, err := parseData(v.Interface(), variablesMapping) + parsedValue, err := p.parseData(v.Interface(), variablesMapping) if err != nil { return raw, err } @@ -122,7 +170,7 @@ var ( ) // parseString parse string with variables -func parseString(raw string, variablesMapping map[string]interface{}) (interface{}, error) { +func (p *parser) parseString(raw string, variablesMapping map[string]interface{}) (interface{}, error) { matchStartPosition := 0 parsedString := "" remainedString := raw @@ -161,15 +209,19 @@ func parseString(raw string, variablesMapping map[string]interface{}) (interface if err != nil { return raw, err } - parsedArgs, err := parseData(arguments, variablesMapping) + parsedArgs, err := p.parseData(arguments, variablesMapping) if err != nil { return raw, err } - result, err := callFunc(funcName, parsedArgs.([]interface{})...) + result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) if err != nil { + log.Error().Str("funcName", funcName).Interface("arguments", arguments). + Err(err).Msg("call function failed") return raw, err } + log.Info().Str("funcName", funcName).Interface("arguments", arguments). + Interface("output", result).Msg("call function success") if funcMatched[0] == raw { // raw_string is a function, e.g. "${add_one(3)}", return its eval value directly @@ -248,73 +300,6 @@ func mergeVariables(variables, overriddenVariables map[string]interface{}) map[s return mergedVariables } -// callFunc call function with arguments -// only support return at most one result value -func callFunc(funcName string, arguments ...interface{}) (interface{}, error) { - function, ok := builtin.Functions[funcName] - if !ok { - // function not found - return nil, fmt.Errorf("function %s is not found", funcName) - } - funcValue := reflect.ValueOf(function) - if funcValue.Kind() != reflect.Func { - // function not valid - return nil, fmt.Errorf("function %s is invalid", funcName) - } - - if funcValue.Type().NumIn() != len(arguments) { - // function arguments not match - return nil, fmt.Errorf("function %s arguments number not match", funcName) - } - - argumentsValue := make([]reflect.Value, len(arguments)) - for index, argument := range arguments { - argumentValue := reflect.ValueOf(argument) - expectArgumentType := funcValue.Type().In(index) - actualArgumentType := reflect.TypeOf(argument) - - // type match - if expectArgumentType == actualArgumentType { - argumentsValue[index] = argumentValue - continue - } - - // type not match, check if convertible - if !actualArgumentType.ConvertibleTo(expectArgumentType) { - // function argument type not match and not convertible - err := fmt.Errorf("function %s argument %d type is neither match nor convertible, expect %v, actual %v", - funcName, index, expectArgumentType, actualArgumentType) - log.Error().Err(err).Msg("call function failed") - return nil, err - } - // convert argument to expect type - argumentsValue[index] = argumentValue.Convert(expectArgumentType) - } - - resultValues := funcValue.Call(argumentsValue) - if len(resultValues) > 1 { - // function should return at most one value - err := fmt.Errorf("function %s should return at most one value", funcName) - log.Error().Err(err).Msg("call function failed") - return nil, err - } - - // no return value - if len(resultValues) == 0 { - return nil, nil - } - - // return one value - // convert reflect.Value to interface{} - result := resultValues[0].Interface() - log.Info(). - Str("funcName", funcName). - Interface("arguments", arguments). - Interface("output", result). - Msg("call function success") - return result, nil -} - var eval = goval.NewEvaluator() // literalEval parse string to number if possible @@ -361,7 +346,7 @@ func parseFunctionArguments(argsStr string) ([]interface{}, error) { return arguments, nil } -func parseVariables(variables map[string]interface{}) (map[string]interface{}, error) { +func (p *parser) parseVariables(variables map[string]interface{}) (map[string]interface{}, error) { parsedVariables := make(map[string]interface{}) var traverseRounds int @@ -399,7 +384,7 @@ func parseVariables(variables map[string]interface{}) (map[string]interface{}, e return variables, fmt.Errorf("variable not defined: %v", undefinedVars) } - parsedValue, err := parseData(varValue, parsedVariables) + parsedValue, err := p.parseData(varValue, parsedVariables) if err != nil { continue } @@ -529,7 +514,7 @@ func parseParameters(parameters map[string]interface{}, variablesMapping map[str case reflect.String: // e.g. username-password: ${parameterize(examples/account.csv)} -> [{"username": "test1", "password": "111111"}, {"username": "test2", "password": "222222"}] var parsedParameterContent interface{} - parsedParameterContent, err = parseString(rawValue.String(), variablesMapping) + parsedParameterContent, err = newParser().parseString(rawValue.String(), variablesMapping) if err != nil { log.Error().Interface("parameterContent", rawValue).Msg("[parseParameters] parse parameter content error") return nil, err diff --git a/parser_test.go b/parser_test.go index f64ef7fc..dbf75af9 100644 --- a/parser_test.go +++ b/parser_test.go @@ -160,8 +160,9 @@ func TestParseDataStringWithVariables(t *testing.T) { {"abc$var_5", "abctrue"}, // "abcTrue" } + parser := newParser() for _, data := range testData { - parsedData, err := parseData(data.expr, variablesMapping) + parsedData, err := parser.parseData(data.expr, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -184,8 +185,9 @@ func TestParseDataStringWithUndefinedVariables(t *testing.T) { {"/api/$SECRET_KEY", "/api/$SECRET_KEY"}, // raise error } + parser := newParser() for _, data := range testData { - parsedData, err := parseData(data.expr, variablesMapping) + parsedData, err := parser.parseData(data.expr, variablesMapping) if !assert.Error(t, err) { t.Fail() } @@ -228,8 +230,9 @@ func TestParseDataStringWithVariablesAbnormal(t *testing.T) { {"ABC$var_1{}a", "ABCabc{}a"}, // {} } + parser := newParser() for _, data := range testData { - parsedData, err := parseData(data.expr, variablesMapping) + parsedData, err := parser.parseData(data.expr, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -258,8 +261,9 @@ func TestParseDataMapWithVariables(t *testing.T) { {map[string]interface{}{"$var2": "$val1"}, map[string]interface{}{"123": 200}}, } + parser := newParser() for _, data := range testData { - parsedData, err := parseData(data.expr, variablesMapping) + parsedData, err := parser.parseData(data.expr, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -291,8 +295,9 @@ func TestParseHeaders(t *testing.T) { {map[string]string{"$var2": "$val2"}, map[string]string{"123": ""}}, } + parser := newParser() for _, data := range testData { - parsedHeaders, err := parseHeaders(data.rawHeaders, variablesMapping) + parsedHeaders, err := parser.parseHeaders(data.rawHeaders, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -328,16 +333,18 @@ func TestMergeVariables(t *testing.T) { } } -func TestCallFunction(t *testing.T) { +func TestCallBuiltinFunction(t *testing.T) { + parser := newParser() + // call function without arguments - _, err := callFunc("get_timestamp") + _, err := parser.callFunc("get_timestamp") if !assert.NoError(t, err) { t.Fail() } // call function with one argument timeStart := time.Now() - _, err = callFunc("sleep", 1) + _, err = parser.callFunc("sleep", 1) if !assert.NoError(t, err) { t.Fail() } @@ -346,7 +353,7 @@ func TestCallFunction(t *testing.T) { } // call function with one argument - result, err := callFunc("gen_random_string", 10) + result, err := parser.callFunc("gen_random_string", 10) if !assert.NoError(t, err) { t.Fail() } @@ -355,7 +362,7 @@ func TestCallFunction(t *testing.T) { } // call function with two argument - result, err = callFunc("max", float64(10), 9.99) + result, err = parser.callFunc("max", float64(10), 9.99) if !assert.NoError(t, err) { t.Fail() } @@ -440,8 +447,9 @@ func TestParseDataStringWithFunctions(t *testing.T) { {"123${gen_random_string($n)}abc", 11}, } + parser := newParser() for _, data := range testData1 { - value, err := parseData(data.expr, variablesMapping) + value, err := parser.parseData(data.expr, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -460,7 +468,7 @@ func TestParseDataStringWithFunctions(t *testing.T) { } for _, data := range testData2 { - value, err := parseData(data.expr, variablesMapping) + value, err := parser.parseData(data.expr, variablesMapping) if !assert.NoError(t, err) { t.Fail() } @@ -506,8 +514,9 @@ func TestParseVariables(t *testing.T) { }, } + parser := newParser() for _, data := range testData { - value, err := parseVariables(data.rawVars) + value, err := parser.parseVariables(data.rawVars) if !assert.NoError(t, err) { t.Fail() } @@ -536,8 +545,9 @@ func TestParseVariablesAbnormal(t *testing.T) { }, } + parser := newParser() for _, data := range testData { - value, err := parseVariables(data.rawVars) + value, err := parser.parseVariables(data.rawVars) if !assert.Error(t, err) { t.Fail() } diff --git a/plugin.go b/plugin.go new file mode 100644 index 00000000..005dbda8 --- /dev/null +++ b/plugin.go @@ -0,0 +1,132 @@ +package hrp + +import ( + "fmt" + "plugin" + "reflect" + "runtime" + + "github.com/rs/zerolog/log" + + "github.com/httprunner/hrp/internal/builtin" +) + +func (p *parser) loadPlugin(path string) error { + if runtime.GOOS == "windows" { + log.Warn().Msg("go plugin does not support windows") + return nil + } + + if path == "" { + return nil + } + + // check if loaded before + if p.pluginLoader != nil { + return nil + } + + // locate plugin file + pluginPath, err := locatePlugin(path) + if err != nil { + // plugin not found + return nil + } + + // load plugin + plugins, err := plugin.Open(pluginPath) + if err != nil { + log.Error().Err(err).Str("path", path).Msg("load go plugin failed") + return err + } + p.pluginLoader = plugins + + log.Info().Str("path", path).Msg("load go plugin success") + return nil +} + +func getMappingFunction(funcName string, pluginLoader *plugin.Plugin) (reflect.Value, error) { + var fn reflect.Value + var err error + + defer func() { + // check function type + if err == nil && fn.Kind() != reflect.Func { + // function not valid + err = fmt.Errorf("function %s is invalid", funcName) + return + } + }() + + // get function from plugin loader + if pluginLoader != nil { + sym, err := pluginLoader.Lookup(funcName) + if err == nil { + fn = reflect.ValueOf(sym) + return fn, nil + } + } + + // get builtin function + if function, ok := builtin.Functions[funcName]; ok { + fn = reflect.ValueOf(function) + return fn, nil + } + + // function not found + return reflect.Value{}, fmt.Errorf("function %s is not found", funcName) +} + +// callFunc calls function with arguments +// only support return at most one result value +func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { + fn, err := getMappingFunction(funcName, p.pluginLoader) + if err != nil { + return nil, err + } + + if fn.Type().NumIn() != len(arguments) { + // function arguments not match + return nil, fmt.Errorf("function arguments number not match") + } + + argumentsValue := make([]reflect.Value, len(arguments)) + for index, argument := range arguments { + argumentValue := reflect.ValueOf(argument) + expectArgumentType := fn.Type().In(index) + actualArgumentType := reflect.TypeOf(argument) + + // type match + if expectArgumentType == actualArgumentType { + argumentsValue[index] = argumentValue + continue + } + + // type not match, check if convertible + if !actualArgumentType.ConvertibleTo(expectArgumentType) { + // function argument type not match and not convertible + err := fmt.Errorf("function argument %d's type is neither match nor convertible, expect %v, actual %v", + index, expectArgumentType, actualArgumentType) + return nil, err + } + // convert argument to expect type + argumentsValue[index] = argumentValue.Convert(expectArgumentType) + } + + resultValues := fn.Call(argumentsValue) + if len(resultValues) > 1 { + // function should return at most one value + err := fmt.Errorf("function should return at most one value") + return nil, err + } + + // no return value + if len(resultValues) == 0 { + return nil, nil + } + + // return one value + // convert reflect.Value to interface{} + result := resultValues[0].Interface() + return result, nil +} diff --git a/plugin_test.go b/plugin_test.go new file mode 100644 index 00000000..b811c6e3 --- /dev/null +++ b/plugin_test.go @@ -0,0 +1,80 @@ +// +build linux freebsd darwin +// go plugin doesn't support windows + +package hrp + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + fmt.Println("[TestMain] build go plugin") + // flag -race is necessary in order to be consistent with go test + cmd := exec.Command("go", "build", "-buildmode=plugin", `-race`, "-o=examples/debugtalk.so", "examples/plugin/debugtalk.go") + if err := cmd.Run(); err != nil { + panic(err) + } + os.Exit(m.Run()) +} + +func TestLocatePlugin(t *testing.T) { + cwd, _ := os.Getwd() + _, err := locatePlugin(cwd) + if !assert.Error(t, err) { + t.Fail() + } + + _, err = locatePlugin("") + if !assert.Error(t, err) { + t.Fail() + } + + startPath := "examples/debugtalk.so" + _, err = locatePlugin(startPath) + if !assert.Nil(t, err) { + t.Fail() + } + + startPath = "examples/demo.json" + _, err = locatePlugin(startPath) + if !assert.Nil(t, err) { + t.Fail() + } + + startPath = "examples/" + _, err = locatePlugin(startPath) + if !assert.Nil(t, err) { + t.Fail() + } + + startPath = "examples/plugin/debugtalk.go" + _, err = locatePlugin(startPath) + if !assert.Nil(t, err) { + t.Fail() + } + + startPath = "/abc" + _, err = locatePlugin(startPath) + if !assert.Error(t, err) { + t.Fail() + } +} + +func TestCallPluginFunction(t *testing.T) { + parser := newParser() + parser.loadPlugin("examples/debugtalk.so") + + // call function without arguments + result, err := parser.callFunc("Concatenate", 1, "2", 3.14) + if !assert.NoError(t, err) { + t.Fail() + } + if !assert.Equal(t, result, "1_2_3.14") { + t.Fail() + } +} diff --git a/response.go b/response.go index f81de3f1..e57608fe 100644 --- a/response.go +++ b/response.go @@ -13,7 +13,7 @@ import ( "github.com/httprunner/hrp/internal/builtin" ) -func newResponseObject(t *testing.T, resp *http.Response) (*responseObject, error) { +func newResponseObject(t *testing.T, parser *parser, resp *http.Response) (*responseObject, error) { // prepare response headers headers := make(map[string]string) for k, v := range resp.Header { @@ -61,6 +61,7 @@ func newResponseObject(t *testing.T, resp *http.Response) (*responseObject, erro return &responseObject{ t: t, + parser: parser, respObjMeta: data, }, nil } @@ -74,6 +75,7 @@ type respObjMeta struct { type responseObject struct { t *testing.T + parser *parser respObjMeta interface{} validationResults map[string]interface{} } @@ -101,7 +103,7 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s var checkValue interface{} if strings.Contains(checkItem, "$") { // reference variable - checkValue, err = parseData(checkItem, variablesMapping) + checkValue, err = v.parser.parseData(checkItem, variablesMapping) if err != nil { return err } @@ -114,7 +116,7 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s assertFunc := builtin.Assertions[assertMethod] // parse expected value - expectValue, err := parseData(validator.Expect, variablesMapping) + expectValue, err := v.parser.parseData(validator.Expect, variablesMapping) if err != nil { return err } diff --git a/runner.go b/runner.go index 5f73e133..c59e1fec 100644 --- a/runner.go +++ b/runner.go @@ -126,6 +126,7 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) *caseRunner { caseRunner := &caseRunner{ TestCase: testcase, hrpRunner: r, + parser: newParser(), } caseRunner.reset() return caseRunner @@ -136,6 +137,7 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) *caseRunner { type caseRunner struct { *TestCase hrpRunner *HRPRunner + parser *parser sessionVariables map[string]interface{} // transactions stores transaction timing info. // key is transaction name, value is map of transaction type and time, e.g. start time and end time. @@ -203,7 +205,7 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa stepVariables = mergeVariables(stepVariables, caseConfig.Variables) // parse step variables - parsedVariables, err := parseVariables(stepVariables) + parsedVariables, err := r.parser.parseVariables(stepVariables) if err != nil { log.Error().Interface("variables", caseConfig.Variables).Err(err).Msg("parse step variables failed") return nil, err @@ -320,7 +322,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro // prepare request headers if len(step.Request.Headers) > 0 { - headers, err := parseHeaders(step.Request.Headers, step.Variables) + headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables) if err != nil { return nil, errors.Wrap(err, "parse headers failed") } @@ -337,7 +339,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro // prepare request params var queryParams url.Values if len(step.Request.Params) > 0 { - params, err := parseData(step.Request.Params, step.Variables) + params, err := r.parser.parseData(step.Request.Params, step.Variables) if err != nil { return nil, errors.Wrap(err, "parse data failed") } @@ -369,7 +371,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro // prepare request body if step.Request.Body != nil { - data, err := parseData(step.Request.Body, step.Variables) + data, err := r.parser.parseData(step.Request.Body, step.Variables) if err != nil { return nil, err } @@ -443,7 +445,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro } // new response object - respObj, err := newResponseObject(r.hrpRunner.t, resp) + respObj, err := newResponseObject(r.hrpRunner.t, r.parser, resp) if err != nil { err = errors.Wrap(err, "init ResponseObject error") return @@ -475,8 +477,16 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err success: false, } testcase := step.TestCase + + // copy testcase to avoid data racing + copiedTestCase := &TestCase{} + if err = copier.Copy(copiedTestCase, testcase); err != nil { + log.Error().Err(err).Msg("copy testcase failed") + return nil, err + } + start := time.Now() - err = r.hrpRunner.newCaseRunner(testcase).run() + err = r.hrpRunner.newCaseRunner(copiedTestCase).run() stepResult.elapsed = time.Since(start).Milliseconds() if err != nil { return stepResult, err @@ -488,21 +498,28 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err func (r *caseRunner) parseConfig(config IConfig) error { cfg := config.ToStruct() // parse config variables - parsedVariables, err := parseVariables(cfg.Variables) + parsedVariables, err := r.parser.parseVariables(cfg.Variables) if err != nil { log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed") return err } cfg.Variables = parsedVariables + + // load plugin variables and functions + err = r.parser.loadPlugin(cfg.Path) + if err != nil { + return err + } + // parse config name - parsedName, err := parseString(cfg.Name, cfg.Variables) + parsedName, err := r.parser.parseString(cfg.Name, cfg.Variables) if err != nil { return err } cfg.Name = convertString(parsedName) // parse config base url - parsedBaseURL, err := parseString(cfg.BaseURL, cfg.Variables) + parsedBaseURL, err := r.parser.parseString(cfg.BaseURL, cfg.Variables) if err != nil { return err }