diff --git a/convert.go b/convert.go index 7f1bc830..f7579334 100644 --- a/convert.go +++ b/convert.go @@ -2,10 +2,12 @@ package hrp import ( "bytes" + "encoding/csv" "encoding/json" "fmt" "io/ioutil" "path/filepath" + "strings" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" @@ -94,6 +96,36 @@ func loadFromYAML(path string) (*TCase, error) { return tc, err } +func loadFromCSV(path string) []map[string]string { + path, err := filepath.Abs(path) + if err != nil { + log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") + return nil + } + log.Info().Str("path", path).Msg("load csv file") + + file, err := ioutil.ReadFile(path) + if err != nil { + log.Error().Err(err).Msg("load csv file failed") + return nil + } + r := csv.NewReader(strings.NewReader(string(file))) + content, err := r.ReadAll() + if err != nil { + log.Error().Err(err).Msg("parse csv file failed") + return nil + } + var result []map[string]string + for i := 1; i < len(content); i++ { + row := make(map[string]string) + for j := 0; j < len(content[i]); j++ { + row[content[0][j]] = content[i][j] + } + result = append(result, row) + } + return result +} + func (tc *TCase) ToTestCase() (*TestCase, error) { testCase := &TestCase{ Config: &Config{cfg: tc.Config}, diff --git a/examples/demo.yaml b/examples/demo.yaml index a0cee432..e0f70360 100644 --- a/examples/demo.yaml +++ b/examples/demo.yaml @@ -7,6 +7,11 @@ config: "n": 5 varFoo1: ${gen_random_string($n)} varFoo2: ${max($a, $b)} + parameters: + username-password: ${parameterize(examples/account.csv)} + user_agent: ["iOS/10.1", "iOS/10.2"] + parameters_setting: + strategy: random teststeps: - name: transaction 1 start transaction: @@ -19,6 +24,8 @@ teststeps: params: foo1: $varFoo1 foo2: $varFoo2 + foo3: $username + foo4: $password headers: User-Agent: HttpRunnerPlus variables: diff --git a/parser.go b/parser.go index e6ecc85e..10ec4a59 100644 --- a/parser.go +++ b/parser.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/maja42/goval" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/hrp/internal/builtin" @@ -247,15 +248,35 @@ 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 { +func contains(s []string, e string) bool { + for _, a := range s { + if strings.EqualFold(a, e) { + return true + } + } + return false +} + +func getMappingFunction(funcName string) (interface{}, error) { + if function, ok := builtin.Functions[funcName]; ok { + // function is builtin + return function, nil + } else if contains([]string{"parameterize", "P"}, funcName) { + // parameterize function + return loadFromCSV, nil + } else { // function not found return nil, fmt.Errorf("function %s is not found", funcName) } +} +// callFunc call function with arguments +// only support return at most one result value +func callFunc(funcName string, arguments ...interface{}) (interface{}, error) { + function, err := getMappingFunction(funcName) + if err != nil { + return nil, err + } funcValue := reflect.ValueOf(function) if funcValue.Kind() != reflect.Func { // function not valid @@ -494,20 +515,81 @@ func findallVariables(raw string) variableSet { return varSet } -//func parseParameters(parameters map[string]interface{}) []map[string]interface{} { -// for k, v := range parameters { -// parameter_name_list := strings.Split(k, "-") -// rawValue := reflect.ValueOf(v) -// switch rawValue.Kind() { -// case reflect.String: -// var varList []map[string]interface{} -// -// case reflect.Slice: -// for i := 0; i < rawValue.Len(); i++ { -// rawValue.Index(i).Interface() -// } -// default: -// panic(fmt.Sprintf("parameter content should be List or Text(variables or functions call), got %v", v)) -// } -// } -//} +func genCartesianProduct(params [][]map[string]interface{}) []map[string]interface{} { + var cartesianProduct []map[string]interface{} + for i := 0; i < len(params)-1; i++ { + for _, param1 := range params[i] { + for _, param2 := range params[i+1] { + cartesianProduct = append(cartesianProduct, mergeVariables(param1, param2)) + } + } + } + return cartesianProduct +} + +func parseParameters(parameters map[string]interface{}, variablesMapping map[string]interface{}) ([]map[string]interface{}, error) { + var parsedParametersList [][]map[string]interface{} + for k, v := range parameters { + parameterNameList := strings.Split(k, "-") + var parameterList []map[string]interface{} + rawValue := reflect.ValueOf(v) + switch rawValue.Kind() { + case reflect.String: + parsedParameterContent, err := parseData(rawValue.Interface(), variablesMapping) + if err != nil { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parse parameter error") + return nil, err + } + parsedParameterRawValue := reflect.ValueOf(parsedParameterContent) + if parsedParameterRawValue.Kind() != reflect.Slice { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") + return nil, errors.New("parameter content should be List or Text(variables or functions call)") + } + for i := 0; i < parsedParameterRawValue.Len(); i++ { + parameterMap := make(map[string]interface{}) + if parsedParameterRawValue.Index(i).Kind() == reflect.Map { + for _, key := range parameterNameList { + parameterMap[key] = parsedParameterRawValue.Index(i).MapIndex(reflect.ValueOf(key)).Interface() + } + } else if parsedParameterRawValue.Index(i).Kind() == reflect.Slice { + if len(parameterNameList) != parsedParameterRawValue.Index(i).Len() { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name list and parameter content list should have the same length") + return nil, errors.New("parameter name list and parameter content list should have the same length") + } + for i := 0; i < parsedParameterRawValue.Index(i).Len(); i++ { + parameterMap[parameterNameList[i]] = parsedParameterRawValue.Index(i).Index(i).Interface() + } + } else if len(parameterNameList) == 1 { + parameterMap[parameterNameList[0]] = parsedParameterRawValue.Index(i).Interface() + } else { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") + return nil, errors.New("parameter content should be List or Text(variables or functions call)") + } + parameterList = append(parameterList, parameterMap) + } + case reflect.Slice: + for i := 0; i < rawValue.Len(); i++ { + parameterMap := make(map[string]interface{}) + if rawValue.Index(i).Kind() == reflect.Interface || rawValue.Index(i).Kind() == reflect.String { + parameterMap[parameterNameList[0]] = rawValue.Index(i).Interface() + } else if rawValue.Index(i).Kind() == reflect.Slice { + if len(parameterNameList) != rawValue.Index(i).Len() { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter name list and parameter content list should have the same length") + return nil, errors.New("parameter name list and parameter content list should have the same length") + } + for i := 0; i < rawValue.Index(i).Len(); i++ { + parameterMap[parameterNameList[i]] = rawValue.Index(i).Index(i).Interface() + } + } else { + log.Error().Interface("parameter", parameters).Msg("[parseParameters] parameter content should be List or Text(variables or functions call), got %v") + return nil, errors.New("parameter content should be List or Text(variables or functions call)") + } + parameterList = append(parameterList, parameterMap) + } + default: + panic(fmt.Sprintf("parameter content should be List or Text(variables or functions call), got %v", v)) + } + parsedParametersList = append(parsedParametersList, parameterList) + } + return genCartesianProduct(parsedParametersList), nil +} diff --git a/runner.go b/runner.go index 9f89495a..b54dc9ba 100644 --- a/runner.go +++ b/runner.go @@ -130,15 +130,24 @@ func (r *hrpRunner) runCase(testcase *TestCase) error { } log.Info().Str("testcase", config.Name()).Msg("run testcase start") - r.startTime = time.Now() - for _, step := range testcase.TestSteps { - _, err := r.runStep(step, config) - if err != nil { - if r.failfast { - log.Error().Err(err).Msg("abort running due to failfast setting") - return err + // parse config parameters + parsedParams, err := parseParameters(config.ToStruct().Parameters, config.ToStruct().Variables) + if err != nil { + log.Error().Interface("params", config.ToStruct().Parameters).Err(err).Msg("parse config parameters failed") + return err + } + for _, parameter := range parsedParams { + config.ToStruct().Variables = mergeVariables(parameter, config.ToStruct().Variables) + r.startTime = time.Now() + for _, step := range testcase.TestSteps { + _, err := r.runStep(step, config) + if err != nil { + if r.failfast { + log.Error().Err(err).Msg("abort running due to failfast setting") + return err + } + log.Warn().Err(err).Msg("run step failed, continue next step") } - log.Warn().Err(err).Msg("run step failed, continue next step") } } @@ -465,7 +474,6 @@ func (r *hrpRunner) parseConfig(config IConfig) error { return err } cfg.Variables = parsedVariables - // parse config name parsedName, err := parseString(cfg.Name, cfg.Variables) if err != nil {