mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
feat: data-driven.
This commit is contained in:
32
convert.go
32
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},
|
||||
|
||||
@@ -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
126
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
|
||||
}
|
||||
|
||||
26
runner.go
26
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 {
|
||||
|
||||
Reference in New Issue
Block a user