Files
httprunner/hrp/testcase.go
2022-05-24 20:50:53 +08:00

384 lines
11 KiB
Go

package hrp
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
// ITestCase represents interface for testcases,
// includes TestCase and TestCasePath.
type ITestCase interface {
GetPath() string
ToTestCase() (*TestCase, error)
}
// TestCase is a container for one testcase, which is used for testcase runner.
// TestCase implements ITestCase interface.
type TestCase struct {
Config *TConfig
TestSteps []IStep
}
func (tc *TestCase) GetPath() string {
return tc.Config.Path
}
func (tc *TestCase) ToTestCase() (*TestCase, error) {
return tc, nil
}
func (tc *TestCase) ToTCase() *TCase {
tCase := &TCase{
Config: tc.Config,
}
for _, step := range tc.TestSteps {
tCase.TestSteps = append(tCase.TestSteps, step.Struct())
}
return tCase
}
// TestCasePath implements ITestCase interface.
type TestCasePath string
func (path *TestCasePath) GetPath() string {
return fmt.Sprintf("%v", *path)
}
// ToTestCase loads testcase path and convert to *TestCase
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
tc := &TCase{}
casePath := path.GetPath()
err := builtin.LoadFile(casePath, tc)
if err != nil {
return nil, err
}
if tc.Config == nil {
return nil, errors.New("incorrect testcase file format, expected config in file")
}
err = tc.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
tc.Config.Path = casePath
testCase := &TestCase{
Config: tc.Config,
}
// locate project root dir by plugin path
projectRootDir, err := GetProjectRootDirPath(casePath)
if err != nil {
return nil, errors.Wrap(err, "failed to get project root dir")
}
for _, step := range tc.TestSteps {
if step.API != nil {
apiPath, ok := step.API.(string)
if !ok {
return nil, fmt.Errorf("referenced api path should be string, got %v", step.API)
}
path := filepath.Join(projectRootDir, apiPath)
if !builtin.IsFilePathExists(path) {
return nil, errors.New("referenced api file not found: " + path)
}
refAPI := APIPath(path)
apiContent, err := refAPI.ToAPI()
if err != nil {
return nil, err
}
step.API = apiContent
testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{
step: step,
})
} else if step.TestCase != nil {
casePath, ok := step.TestCase.(string)
if !ok {
return nil, fmt.Errorf("referenced testcase path should be string, got %v", step.TestCase)
}
path := filepath.Join(projectRootDir, casePath)
if !builtin.IsFilePathExists(path) {
return nil, errors.New("referenced testcase file not found: " + path)
}
refTestCase := TestCasePath(path)
tc, err := refTestCase.ToTestCase()
if err != nil {
return nil, err
}
step.TestCase = tc
testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{
step: step,
})
} else if step.ThinkTime != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepThinkTime{
step: step,
})
} else if step.Request != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
step: step,
})
} else if step.Transaction != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{
step: step,
})
} else if step.Rendezvous != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepRendezvous{
step: step,
})
} else if step.WebSocket != nil {
testCase.TestSteps = append(testCase.TestSteps, &StepWebSocket{
step: step,
})
} else {
log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step")
}
}
return testCase, nil
}
// TCase represents testcase data structure.
// Each testcase includes one public config and several sequential teststeps.
type TCase struct {
Config *TConfig `json:"config" yaml:"config"`
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
}
// MakeCompat2GoEngine converts TCase compatible with Golang engine style
func (tc *TCase) MakeCompat2GoEngine() (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("[MakeCompat2GoEngine] convert compat testcase error: %v", p)
}
}()
for _, step := range tc.TestSteps {
// 1. deal with request body compatibility
if step.Request != nil && step.Request.Body == nil {
if step.Request.Json != nil {
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
step.Request.Body = step.Request.Json
step.Request.Json = nil
} else if step.Request.Data != nil {
step.Request.Body = step.Request.Data
step.Request.Data = nil
}
}
// 2. deal with validators compatibility
err = convertValidatorCompat2GoEngine(step.Validators)
if err != nil {
return err
}
// 3. deal with extract expr including hyphen
convertExtract(step.Extract)
}
return nil
}
func convertValidatorCompat2GoEngine(Validators []interface{}) (err error) {
for i, iValidator := range Validators {
if _, ok := iValidator.(Validator); ok {
continue
}
validatorMap := iValidator.(map[string]interface{})
validator := Validator{}
iCheck, checkExisted := validatorMap["check"]
iAssert, assertExisted := validatorMap["assert"]
iExpect, expectExisted := validatorMap["expect"]
// validator check priority: Golang > Python engine style
if checkExisted && assertExisted && expectExisted {
// Golang engine style
validator.Check = iCheck.(string)
validator.Assert = iAssert.(string)
validator.Expect = iExpect
if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
validator.Message = iMsg.(string)
}
validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator
continue
}
if len(validatorMap) == 1 {
// Python engine style
for assertMethod, iValidatorContent := range validatorMap {
validatorContent := iValidatorContent.([]interface{})
if len(validatorContent) > 3 {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
validator.Check = validatorContent[0].(string)
validator.Assert = assertMethod
validator.Expect = validatorContent[1]
if len(validatorContent) == 3 {
validator.Message = validatorContent[2].(string)
}
}
validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator
continue
}
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
return nil
}
// convertExtract deals with extract expr including hyphen
func convertExtract(extract map[string]string) {
for key, value := range extract {
extract[key] = convertCheckExpr(value)
}
}
// convertCheckExpr deals with check expression including hyphen
func convertCheckExpr(checkExpr string) string {
if strings.Contains(checkExpr, textExtractorSubRegexp) {
return checkExpr
}
checkItems := strings.Split(checkExpr, ".")
for i, checkItem := range checkItems {
if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") {
checkItems[i] = fmt.Sprintf("\"%s\"", checkItem)
}
}
return strings.Join(checkItems, ".")
}
// MakeCompat2PyEngine converts TCase compatible with Python engine style
func (tc *TCase) MakeCompat2PyEngine() (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("[MakeCompat2PyEngine] convert compat testcase error: %v", p)
}
}()
for _, step := range tc.TestSteps {
// 1. deal with request body compatibility
if step.Request != nil && step.Request.Body != nil {
if strings.HasPrefix(step.Request.Headers["Content-Type"], "application/json") {
step.Request.Json = step.Request.Body
step.Request.Body = nil
continue
}
step.Request.Data = step.Request.Body
step.Request.Body = nil
}
// 2. deal with validators compatibility
err = convertValidatorCompat2PyEngine(step.Validators)
if err != nil {
return err
}
}
return
}
func convertValidatorCompat2PyEngine(Validators []interface{}) (err error) {
for i, iValidator := range Validators {
if v, ok := iValidator.(Validator); ok {
var iValidatorContent []interface{}
iValidatorContent = append(iValidatorContent, v.Check)
iValidatorContent = append(iValidatorContent, v.Expect)
newValidatorMap := make(map[string]interface{})
newValidatorMap[v.Assert] = iValidatorContent
Validators[i] = newValidatorMap
continue
}
validatorMap := iValidator.(map[string]interface{})
// validator check priority: Python > Golang engine style
if len(validatorMap) == 1 {
// Python engine style
for _, iValidatorContent := range validatorMap {
validatorContent := iValidatorContent.([]interface{})
if len(validatorContent) > 3 {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
}
continue
}
iCheck, checkExisted := validatorMap["check"]
iAssert, assertExisted := validatorMap["assert"]
iExpect, expectExisted := validatorMap["expect"]
if checkExisted && assertExisted && expectExisted {
// Golang engine style
var validatorContent []interface{}
validatorContent = append(validatorContent, iCheck)
validatorContent = append(validatorContent, iExpect)
if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
validatorContent = append(validatorContent, iMsg)
}
newValidatorMap := make(map[string]interface{})
newValidatorMap[iAssert.(string)] = validatorContent
Validators[i] = newValidatorMap
continue
}
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
return
}
func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
testCases := make([]*TestCase, 0)
for _, iTestCase := range iTestCases {
if _, ok := iTestCase.(*TestCase); ok {
testcase, err := iTestCase.ToTestCase()
if err != nil {
log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct")
return nil, err
}
testCases = append(testCases, testcase)
continue
}
// iTestCase should be a TestCasePath, file path or folder path
tcPath, ok := iTestCase.(*TestCasePath)
if !ok {
return nil, errors.New("invalid iTestCase type")
}
casePath := tcPath.GetPath()
err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error {
if dir == nil {
// casePath is a file other than a dir
path = casePath
} else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") {
// skip hidden folders
return fs.SkipDir
} else {
// casePath is a dir
path = filepath.Join(casePath, path)
}
// ignore non-testcase files
ext := filepath.Ext(path)
if ext != ".yml" && ext != ".yaml" && ext != ".json" {
return nil
}
// filtered testcases
testCasePath := TestCasePath(path)
tc, err := testCasePath.ToTestCase()
if err != nil {
log.Warn().Err(err).Str("path", path).Msg("load testcase failed")
return nil
}
testCases = append(testCases, tc)
return nil
})
if err != nil {
return nil, errors.Wrap(err, "read dir failed")
}
}
log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
return testCases, nil
}