feat: data-driven.

This commit is contained in:
徐聪
2021-12-27 15:31:49 +08:00
parent ddc0ec0a0d
commit f43450fa6e
4 changed files with 160 additions and 31 deletions

View File

@@ -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},

View File

@@ -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:

126
parser.go
View File

@@ -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
}

View File

@@ -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 {