diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 578091e9..9e7fe395 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -30,7 +30,7 @@ Copyright 2017 debugtalk ### SEE ALSO * [hrp boom](hrp_boom.md) - run load test with boomer -* [hrp convert](hrp_convert.md) - convert external cases to JSON/YAML/gotest/pytest testcases +* [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases * [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files * [hrp pytest](hrp_pytest.md) - run API test with pytest * [hrp run](hrp_run.md) - run API test with go engine diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index d4771aad..80fcaf2f 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -1,6 +1,6 @@ ## hrp convert -convert external cases to JSON/YAML/gotest/pytest testcases +convert to JSON/YAML/gotest/pytest testcases ``` hrp convert $path... [flags] diff --git a/examples/data/postman2case/demo.json b/examples/data/postman/postman_collection.json similarity index 100% rename from examples/data/postman2case/demo.json rename to examples/data/postman/postman_collection.json diff --git a/examples/data/postman2case/profile.yml b/examples/data/postman/profile.yml similarity index 100% rename from examples/data/postman2case/profile.yml rename to examples/data/postman/profile.yml diff --git a/examples/data/postman2case/profile_override.yml b/examples/data/postman/profile_override.yml similarity index 100% rename from examples/data/postman2case/profile_override.yml rename to examples/data/postman/profile_override.yml diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 31c536e4..a4c8d663 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -10,7 +10,7 @@ import ( var convertCmd = &cobra.Command{ Use: "convert $path...", - Short: "convert external cases to JSON/YAML/gotest/pytest testcases", + Short: "convert to JSON/YAML/gotest/pytest testcases", Args: cobra.MinimumNArgs(1), PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) @@ -36,8 +36,7 @@ var convertCmd = &cobra.Command{ if flagCount > 1 { return errors.New("please specify at most one conversion flag") } - iCaseConverters := convert.LoadConverters(outputType, outputDir, profilePath, args) - convert.Run(iCaseConverters) + convert.Run(outputType, outputDir, profilePath, args) return nil }, } diff --git a/hrp/cmd/har2case.go b/hrp/cmd/har2case.go index d26fc4ff..9d5f2f10 100644 --- a/hrp/cmd/har2case.go +++ b/hrp/cmd/har2case.go @@ -3,10 +3,9 @@ package cmd import ( "errors" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/convert/har2case" + "github.com/httprunner/httprunner/v4/hrp/internal/convert" ) // har2caseCmd represents the har2case command @@ -19,39 +18,20 @@ var har2caseCmd = &cobra.Command{ setLogLevel(logLevel) }, RunE: func(cmd *cobra.Command, args []string) error { - var outputFiles []string - for _, arg := range args { - // must choose one - if !har2caseGenYAMLFlag && !har2caseGenJSONFlag { - return errors.New("please select convert format type") - } - var outputPath string - var err error - - har := har2case.NewHAR(arg) - - // specify output dir - if har2caseOutputDir != "" { - har.SetOutputDir(har2caseOutputDir) - } - - // specify profile - if har2caseProfilePath != "" { - har.SetProfile(har2caseProfilePath) - } - - // generate json/yaml files - if har2caseGenYAMLFlag { - outputPath, err = har.GenYAML() - } else { - outputPath, err = har.GenJSON() // default - } - if err != nil { - return err - } - outputFiles = append(outputFiles, outputPath) + var flagCount int + var har2caseOutputType convert.OutputType + if har2caseGenJSONFlag { + flagCount++ } - log.Info().Strs("output", outputFiles).Msg("convert testcase success") + if har2caseGenYAMLFlag { + flagCount++ + har2caseOutputType = convert.OutputTypeYAML + } + if flagCount > 1 { + return errors.New("please specify at most one conversion flag") + + } + convert.Run(har2caseOutputType, har2caseOutputDir, har2caseProfilePath, args) return nil }, } @@ -65,7 +45,7 @@ var ( func init() { rootCmd.AddCommand(har2caseCmd) - har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", true, "convert to JSON format") + har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", false, "convert to JSON format (default)") har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format") har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies") diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index d32adfde..27098249 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -286,7 +286,7 @@ func LoadFile(path string, structObj interface{}) (err error) { return errors.Wrap(err, "read file failed") } // remove BOM at the beginning of file - file = bytes.Trim(file, "\xef\xbb\xbf") + file = bytes.TrimLeft(file, "\xef\xbb\xbf") ext := filepath.Ext(path) switch ext { case ".json", ".har": diff --git a/hrp/internal/convert/README.md b/hrp/internal/convert/README.md index 474c8c0e..d31381be 100644 --- a/hrp/internal/convert/README.md +++ b/hrp/internal/convert/README.md @@ -1,9 +1,10 @@ # hrp convert ## 快速上手 + ```shell $ hrp convert -h -convert external cases to JSON/YAML/gotest/pytest testcases +convert to JSON/YAML/gotest/pytest testcases Usage: hrp convert $path... [flags] @@ -21,22 +22,22 @@ Global Flags: --log-json set log to json format -l, --log-level string set log level (default "INFO") ``` + `hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 等格式的外部脚本转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化,输出的测试用例文件名格式为 `不带扩展名的原文件名称` + `_test` + `json/yaml/go/py` 后缀。 -该指令的所有参数的详细介绍如下: +该指令所有选项的详细说明如下: -1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入的外部脚本转化为对应形态的测试用例,四个参数中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例 +1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入的外部脚本转化为对应形态的测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例 2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹 -3. `--profile` 后接 `profile` 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,`profile` 文件的后缀可以为 `json/yaml/yml`,下面给出两类 `profile` 配置文件的示例: -- 根据 `profile` 替换指定的 `Headers` 和 `Cookies` 信息 +3. `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例: +- 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息 ```yaml headers: Header1: "this header will be created or updated" cookies: Cookie1: "this cookie will be created or updated" - ``` -- 根据 `profile` 覆盖原有的 `Headers` 和 `Cookies` 信息 +- 根据 profile 覆盖原有的 `Headers` 和 `Cookies` 信息 ```yaml override: true headers: @@ -46,22 +47,29 @@ cookies: ``` ## 注意事项 -1. 指定 `override` 为 `false/true` 可以选择 `profile` 的修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 `profile` 的默认修改模式为**替换**模式, -2. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎之间的差异(请求体、断言部分的格式略有不同),输出的 JSON/YAML 则统一采用 Golang 引擎的风格 + +1. `hrp convert` 可以自动识别输入类型,因此不需要通过选项来手动制定输入类型,如遇到无法识别、不支持或转换失败的情况,则会输出错误日志并跳过,不会影响其他转换过程的正常进行 +2. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式 +3. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格 ## 转换流程图 - +`hrp convert` 的转换过程流程图如下: + ## 开发进度 +`hrp convert` 当前的开发进度如下: + | from \ to | JSON | YAML | GoTest | PyTest | |:---------:|:----:|:----:|:------:|:------:| | HAR | ✅ | ✅ | ❌ | ✅ | | Postman | ✅ | ✅ | ❌ | ✅ | | JMeter | ❌ | ❌ | ❌ | ❌ | | Swagger | ❌ | ❌ | ❌ | ❌ | +| curl | ❌ | ❌ | ❌ | ❌ | +| Apache ab | ❌ | ❌ | ❌ | ❌ | | JSON | ✅ | ✅ | ❌ | ✅ | | YAML | ✅ | ✅ | ❌ | ✅ | | GoTest | ❌ | ❌ | ❌ | ❌ | diff --git a/hrp/internal/convert/asset/flowgram.png b/hrp/internal/convert/asset/flowgram.png new file mode 100644 index 00000000..3e676ec7 Binary files /dev/null and b/hrp/internal/convert/asset/flowgram.png differ diff --git a/hrp/internal/convert/asset/flowgram.svg b/hrp/internal/convert/asset/flowgram.svg deleted file mode 100644 index 76652f6b..00000000 --- a/hrp/internal/convert/asset/flowgram.svg +++ /dev/null @@ -1 +0,0 @@ -HTTP 存档格式文件(.har)Postman 项目文件(.json)JMeter 项目文件(.jmx)gotest 测试用例(.go)pytest 测试用例(.py)JSON 测试用例(.json)YAML 测试用例(.yaml)Swagger 脚本文件(.json / .yaml)外部脚本文件JSON/YAML 测试用例代码形态测试用例 \ No newline at end of file diff --git a/hrp/internal/convert/converter.go b/hrp/internal/convert/converter.go index ac6831cc..8733614c 100644 --- a/hrp/internal/convert/converter.go +++ b/hrp/internal/convert/converter.go @@ -117,42 +117,46 @@ func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) { case ".har": caseHAR := new(CaseHar) err = builtin.LoadFile(path, caseHAR) - if err == nil && !reflect.DeepEqual(*caseHAR, CaseHar{}) { + if err == nil && !reflect.ValueOf(*caseHAR).IsZero() { tCaseConverter.InputType = InputTypeHAR tCaseConverter.CaseHAR = caseHAR } case ".json": tCase := new(hrp.TCase) err = builtin.LoadFile(path, tCase) - if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) { + if err == nil && !reflect.ValueOf(*tCase).IsZero() { tCaseConverter.InputType = InputTypeJSON tCaseConverter.TCase = tCase break } casePostman := new(CasePostman) err = builtin.LoadFile(path, casePostman) - if err == nil && !reflect.DeepEqual(*casePostman, CasePostman{}) { + // deal with postman field name conflict with swagger + descriptionBackup := casePostman.Info.Description + casePostman.Info.Description = "" + if err == nil && !reflect.ValueOf(*casePostman).IsZero() { tCaseConverter.InputType = InputTypePostman + casePostman.Info.Description = descriptionBackup tCaseConverter.CasePostman = casePostman break } caseSwagger := new(spec.Swagger) err = builtin.LoadFile(path, caseSwagger) - if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) { + if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() { tCaseConverter.InputType = InputTypeSwagger tCaseConverter.CaseSwagger = caseSwagger } case ".yaml", ".yml": tCase := new(hrp.TCase) err = builtin.LoadFile(path, tCase) - if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) { + if err == nil && !reflect.ValueOf(*tCase).IsZero() { tCaseConverter.InputType = InputTypeYAML tCaseConverter.TCase = tCase break } caseSwagger := new(spec.Swagger) err = builtin.LoadFile(path, caseSwagger) - if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) { + if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() { tCaseConverter.InputType = InputTypeSwagger tCaseConverter.CaseSwagger = caseSwagger } @@ -243,13 +247,14 @@ type ICaseConverter interface { ToPyTest() (string, error) } -func LoadConverters(outputType OutputType, outputDir, profilePath string, args []string) []ICaseConverter { +func Run(outputType OutputType, outputDir, profilePath string, args []string) { // report event sdk.SendEvent(sdk.EventTracking{ Category: "ConvertTests", Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()), }) + // identify input and load converters var iCaseConverters []ICaseConverter for _, arg := range args { tCaseConverter := NewTCaseConverter(arg) @@ -279,10 +284,8 @@ func LoadConverters(outputType OutputType, outputDir, profilePath string, args [ Msg("unknown case type, ignore!") } } - return iCaseConverters -} -func Run(iCaseConverters []ICaseConverter) { + // start converting var outputFiles []string var err error for _, iCaseConverter := range iCaseConverters { diff --git a/hrp/internal/convert/converter_har.go b/hrp/internal/convert/converter_har.go index d34717c9..1ee513ae 100644 --- a/hrp/internal/convert/converter_har.go +++ b/hrp/internal/convert/converter_har.go @@ -3,7 +3,6 @@ package convert import ( "encoding/base64" "fmt" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "net/url" "sort" "strings" @@ -13,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) diff --git a/hrp/internal/convert/converter_postman_test.go b/hrp/internal/convert/converter_postman_test.go index 72994794..9e8ad126 100644 --- a/hrp/internal/convert/converter_postman_test.go +++ b/hrp/internal/convert/converter_postman_test.go @@ -7,9 +7,9 @@ import ( ) var ( - collectionPath = "../../../examples/data/postman2case/demo.json" - collectionProfileOverridePath = "../../../examples/data/postman2case/profile_override.yml" - collectionProfilePath = "../../../examples/data/postman2case/profile.yml" + collectionPath = "../../../examples/data/postman/postman_collection.json" + collectionProfileOverridePath = "../../../examples/data/postman/profile_override.yml" + collectionProfilePath = "../../../examples/data/postman/profile.yml" ) var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath)) diff --git a/hrp/internal/convert/har2case/README.md b/hrp/internal/convert/har2case/README.md deleted file mode 100644 index 08c0b4dc..00000000 --- a/hrp/internal/convert/har2case/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# har2case - -Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+. - -## Install - -## Quick Start - -## Examples diff --git a/hrp/internal/convert/har2case/core.go b/hrp/internal/convert/har2case/core.go deleted file mode 100644 index 25824855..00000000 --- a/hrp/internal/convert/har2case/core.go +++ /dev/null @@ -1,385 +0,0 @@ -package har2case - -import ( - "encoding/base64" - "fmt" - "net/url" - "path/filepath" - "sort" - "strings" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/json" - "github.com/httprunner/httprunner/v4/hrp/internal/sdk" -) - -const ( - suffixJSON = ".json" - suffixYAML = ".yaml" -) - -func NewHAR(path string) *har { - return &har{ - path: path, - } -} - -type har struct { - path string - filterStr string - excludeStr string - profile map[string]interface{} - outputDir string -} - -func (h *har) SetProfile(path string) { - log.Info().Str("path", path).Msg("set profile") - h.profile = make(map[string]interface{}) - err := builtin.LoadFile(path, h.profile) - if err != nil { - log.Warn().Str("path", path). - Msg("invalid profile format, ignore!") - } -} - -func (h *har) SetOutputDir(dir string) { - log.Info().Str("dir", dir).Msg("set output directory") - h.outputDir = dir -} - -func (h *har) GenJSON() (jsonPath string, err error) { - event := sdk.EventTracking{ - Category: "ConvertTests", - Action: "hrp har2case --to-json", - } - // report start event - go sdk.SendEvent(event) - // report running timing event - defer sdk.SendEvent(event.StartTiming("execution")) - - tCase, err := h.makeTestCase() - if err != nil { - return "", err - } - jsonPath = h.genOutputPath(suffixJSON) - err = builtin.Dump2JSON(tCase, jsonPath) - return -} - -func (h *har) GenYAML() (yamlPath string, err error) { - event := sdk.EventTracking{ - Category: "ConvertTests", - Action: "hrp har2case --to-yaml", - } - // report start event - go sdk.SendEvent(event) - // report running timing event - defer sdk.SendEvent(event.StartTiming("execution")) - - tCase, err := h.makeTestCase() - if err != nil { - return "", err - } - yamlPath = h.genOutputPath(suffixYAML) - err = builtin.Dump2YAML(tCase, yamlPath) - return -} - -func (h *har) makeTestCase() (*hrp.TCase, error) { - teststeps, err := h.prepareTestSteps() - if err != nil { - return nil, err - } - - tCase := &hrp.TCase{ - Config: h.prepareConfig(), - TestSteps: teststeps, - } - return tCase, nil -} - -func (h *har) load() (*Har, error) { - har := &Har{} - err := builtin.LoadFile(h.path, har) - if err != nil { - return nil, errors.Wrap(err, "load har failed") - } - return har, nil -} - -func (h *har) prepareConfig() *hrp.TConfig { - return hrp.NewConfig("testcase description"). - SetVerifySSL(false) -} - -func (h *har) prepareTestSteps() ([]*hrp.TStep, error) { - har, err := h.load() - if err != nil { - return nil, err - } - - var steps []*hrp.TStep - for _, entry := range har.Log.Entries { - step, err := h.prepareTestStep(&entry) - if err != nil { - return nil, err - } - steps = append(steps, step) - } - - return steps, nil -} - -func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { - log.Info(). - Str("method", entry.Request.Method). - Str("url", entry.Request.URL). - Msg("convert teststep") - - step := &tStep{ - TStep: hrp.TStep{ - Request: &hrp.Request{}, - Validators: make([]interface{}, 0), - }, - profile: h.profile, - } - if err := step.makeRequestMethod(entry); err != nil { - return nil, err - } - if err := step.makeRequestURL(entry); err != nil { - return nil, err - } - if err := step.makeRequestParams(entry); err != nil { - return nil, err - } - if err := step.makeRequestCookies(entry); err != nil { - return nil, err - } - if err := step.makeRequestHeaders(entry); err != nil { - return nil, err - } - if err := step.makeRequestBody(entry); err != nil { - return nil, err - } - if err := step.makeValidate(entry); err != nil { - return nil, err - } - return &step.TStep, nil -} - -type tStep struct { - hrp.TStep - profile map[string]interface{} -} - -func (s *tStep) makeRequestMethod(entry *Entry) error { - s.Request.Method = hrp.HTTPMethod(entry.Request.Method) - return nil -} - -func (s *tStep) makeRequestURL(entry *Entry) error { - u, err := url.Parse(entry.Request.URL) - if err != nil { - log.Error().Err(err).Msg("make request url failed") - return err - } - s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path) - return nil -} - -func (s *tStep) makeRequestParams(entry *Entry) error { - s.Request.Params = make(map[string]interface{}) - for _, param := range entry.Request.QueryString { - s.Request.Params[param.Name] = param.Value - } - return nil -} - -func (s *tStep) makeRequestCookies(entry *Entry) error { - s.Request.Cookies = make(map[string]string) - cookies, ok := s.profile["cookies"] - if ok { - // use cookies from profile - cookies, ok := cookies.(map[string]interface{}) - if ok { - for k, v := range cookies { - s.Request.Cookies[k] = fmt.Sprintf("%v", v) - } - return nil - } - log.Warn().Interface("cookies", cookies). - Msg("cookies from profile is not a map, ignore!") - } - - // use cookies from har - for _, cookie := range entry.Request.Cookies { - s.Request.Cookies[cookie.Name] = cookie.Value - } - return nil -} - -func (s *tStep) makeRequestHeaders(entry *Entry) error { - s.Request.Headers = make(map[string]string) - headers, ok := s.profile["headers"] - if ok { - // use headers from profile - cookies, ok := headers.(map[string]interface{}) - if ok { - for k, v := range cookies { - s.Request.Headers[k] = fmt.Sprintf("%v", v) - } - return nil - } - log.Warn().Interface("headers", headers). - Msg("headers from profile is not a map, ignore!") - } - - // use headers from har - for _, header := range entry.Request.Headers { - if strings.EqualFold(header.Name, "cookie") { - continue - } - s.Request.Headers[header.Name] = header.Value - } - return nil -} - -func (s *tStep) makeRequestBody(entry *Entry) error { - mimeType := entry.Request.PostData.MimeType - if mimeType == "" { - // GET/HEAD/DELETE without body - return nil - } - - // POST/PUT with body - if strings.HasPrefix(mimeType, "application/json") { - // post json - var body interface{} - if entry.Request.PostData.Text == "" { - body = nil - } else { - err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body) - if err != nil { - log.Error().Err(err).Msg("make request body failed") - return err - } - } - s.Request.Body = body - } else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") { - // post form - var paramsList []string - for _, param := range entry.Request.PostData.Params { - paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value)) - } - s.Request.Body = strings.Join(paramsList, "&") - } else if strings.HasPrefix(mimeType, "text/plain") { - // post raw data - s.Request.Body = entry.Request.PostData.Text - } else { - // TODO - log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType) - } - return nil -} - -func (s *tStep) makeValidate(entry *Entry) error { - // make validator for response status code - s.Validators = append(s.Validators, hrp.Validator{ - Check: "status_code", - Assert: "equals", - Expect: entry.Response.Status, - Message: "assert response status code", - }) - - // make validators for response headers - for _, header := range entry.Response.Headers { - // assert Content-Type - if strings.EqualFold(header.Name, "Content-Type") { - s.Validators = append(s.Validators, hrp.Validator{ - Check: "headers.\"Content-Type\"", - Assert: "equals", - Expect: header.Value, - Message: "assert response header Content-Type", - }) - } - } - - // make validators for response body - respBody := entry.Response.Content - if respBody.Text == "" { - // response body is empty - return nil - } - if strings.HasPrefix(respBody.MimeType, "application/json") { - var data []byte - var err error - // response body is json - if respBody.Encoding == "base64" { - // decode base64 text - data, err = base64.StdEncoding.DecodeString(respBody.Text) - if err != nil { - return errors.Wrap(err, "decode base64 error") - } - } else if respBody.Encoding == "" { - // no encoding - data = []byte(respBody.Text) - } else { - // other encoding type - return nil - } - // convert to json - var body interface{} - if err = json.Unmarshal(data, &body); err != nil { - return errors.Wrap(err, "json.Unmarshal body error") - } - jsonBody, ok := body.(map[string]interface{}) - if !ok { - return fmt.Errorf("response body is not json, not matched with MimeType") - } - - // response body is json - keys := make([]string, 0, len(jsonBody)) - for k := range jsonBody { - keys = append(keys, k) - } - // sort map keys to keep validators in stable order - sort.Strings(keys) - for _, key := range keys { - value := jsonBody[key] - switch v := value.(type) { - case map[string]interface{}: - continue - case []interface{}: - continue - default: - s.Validators = append(s.Validators, hrp.Validator{ - Check: fmt.Sprintf("body.%s", key), - Assert: "equals", - Expect: v, - Message: fmt.Sprintf("assert response body %s", key), - }) - } - } - } - - return nil -} - -func (h *har) genOutputPath(suffix string) string { - file := getFilenameWithoutExtension(h.path) + suffix - if h.outputDir != "" { - return filepath.Join(h.outputDir, file) - } else { - return filepath.Join(filepath.Dir(h.path), file) - } -} - -func getFilenameWithoutExtension(path string) string { - base := filepath.Base(path) - ext := filepath.Ext(base) - return base[0 : len(base)-len(ext)] -} diff --git a/hrp/internal/convert/har2case/core_test.go b/hrp/internal/convert/har2case/core_test.go deleted file mode 100644 index 0fc6a3cb..00000000 --- a/hrp/internal/convert/har2case/core_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package har2case - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/httprunner/httprunner/v4/hrp" -) - -var ( - harPath = "../../../../examples/data/har/demo.har" - harPath2 = "../../../../examples/data/har/postman-echo.har" - profilePath = "../../../../examples/data/har/profile_override.yml" -) - -func TestGenJSON(t *testing.T) { - jsonPath, err := NewHAR(harPath).GenJSON() - if !assert.NoError(t, err) { - t.Fatal() - } - if !assert.NotEmpty(t, jsonPath) { - t.Fatal() - } -} - -func TestGenYAML(t *testing.T) { - yamlPath, err := NewHAR(harPath2).GenYAML() - if !assert.NoError(t, err) { - t.Fatal() - } - if !assert.NotEmpty(t, yamlPath) { - t.Fatal() - } -} - -func TestLoadHAR(t *testing.T) { - har := NewHAR(harPath) - h, err := har.load() - if !assert.NoError(t, err) { - t.Fatal() - } - if !assert.Equal(t, "GET", h.Log.Entries[0].Request.Method) { - t.Fatal() - } - if !assert.Equal(t, "POST", h.Log.Entries[1].Request.Method) { - t.Fatal() - } -} - -func TestLoadHARWithProfile(t *testing.T) { - har := NewHAR(harPath) - har.SetProfile(profilePath) - _, err := har.load() - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, - map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"}, - har.profile["headers"]) { - t.Fatal() - } - if !assert.Equal(t, - map[string]interface{}{"UserName": "debugtalk"}, - har.profile["cookies"]) { - t.Fatal() - } -} - -func TestMakeTestCase(t *testing.T) { - har := NewHAR(harPath) - tCase, err := har.makeTestCase() - if !assert.NoError(t, err) { - t.Fatal() - } - - // make request method - if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) { - t.Fatal() - } - if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) { - t.Fatal() - } - - // make request url - if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) { - t.Fatal() - } - if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) { - t.Fatal() - } - - // make request params - if !assert.Equal(t, "HDnY8", tCase.TestSteps[0].Request.Params["foo1"]) { - t.Fatal() - } - - // make request cookies - if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Cookies["sails.sid"]) { - t.Fatal() - } - - // make request headers - if !assert.Equal(t, "HttpRunnerPlus", tCase.TestSteps[0].Request.Headers["User-Agent"]) { - t.Fatal() - } - if !assert.Equal(t, "postman-echo.com", tCase.TestSteps[0].Request.Headers["Host"]) { - t.Fatal() - } - - // make request data - if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) { - t.Fatal() - } - if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) { - t.Fatal() - } - if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) { - t.Fatal() - } - - // make validators - validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator) - if !ok || !assert.Equal(t, "status_code", validator.Check) { - t.Fatal() - } - validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator) - if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) { - t.Fatal() - } - validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator) - if !ok || !assert.Equal(t, "body.url", validator.Check) { - t.Fatal() - } -} - -func TestGetFilenameWithoutExtension(t *testing.T) { - filename := getFilenameWithoutExtension(harPath2) - if !assert.Equal(t, "postman-echo", filename) { - t.Fatal() - } -} - -func TestMakeRequestURL(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - URL: "http://127.0.0.1:8080/api/login", - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, "http://127.0.0.1:8080/api/login", step.Request.URL) { - t.Fatal() - } -} - -func TestMakeRequestHeaders(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - Method: "POST", - Headers: []NVP{ - {Name: "Content-Type", Value: "application/json; charset=utf-8"}, - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, map[string]string{ - "Content-Type": "application/json; charset=utf-8", - }, step.Request.Headers) { - t.Fatal() - } -} - -func TestMakeRequestHeadersWithProfile(t *testing.T) { - har := NewHAR("") - har.SetProfile(profilePath) - entry := &Entry{ - Request: Request{ - Method: "POST", - Headers: []NVP{ - {Name: "Content-Type", Value: "application/json; charset=utf-8"}, - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, map[string]string{ - "Content-Type": "application/x-www-form-urlencoded", - }, step.Request.Headers) { - t.Fatal() - } -} - -func TestMakeRequestCookies(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - Method: "POST", - Cookies: []Cookie{ - {Name: "abc", Value: "123"}, - {Name: "UserName", Value: "leolee"}, - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, map[string]string{ - "abc": "123", - "UserName": "leolee", - }, step.Request.Cookies) { - t.Fatal() - } -} - -func TestMakeRequestCookiesWithProfile(t *testing.T) { - har := NewHAR("") - har.SetProfile(profilePath) - entry := &Entry{ - Request: Request{ - Method: "POST", - Cookies: []Cookie{ - {Name: "abc", Value: "123"}, - {Name: "UserName", Value: "leolee"}, - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, map[string]string{ - "UserName": "debugtalk", - }, step.Request.Cookies) { - t.Fatal() - } -} - -func TestMakeRequestDataParams(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - Method: "POST", - PostData: PostData{ - MimeType: "application/x-www-form-urlencoded; charset=utf-8", - Params: []PostParam{ - {Name: "a", Value: "1"}, - {Name: "b", Value: "2"}, - }, - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, "a=1&b=2", step.Request.Body) { - t.Fatal() - } -} - -func TestMakeRequestDataJSON(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - Method: "POST", - PostData: PostData{ - MimeType: "application/json; charset=utf-8", - Text: "{\"a\":\"1\",\"b\":\"2\"}", - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, map[string]interface{}{"a": "1", "b": "2"}, step.Request.Body) { - t.Fatal() - } -} - -func TestMakeRequestDataTextEmpty(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Request: Request{ - Method: "POST", - PostData: PostData{ - MimeType: "application/json; charset=utf-8", - Text: "", - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - - if !assert.Equal(t, nil, step.Request.Body) { // TODO - t.Fatal() - } -} - -func TestMakeValidate(t *testing.T) { - har := NewHAR("") - entry := &Entry{ - Response: Response{ - Status: 200, - Headers: []NVP{ - {Name: "Content-Type", Value: "application/json; charset=utf-8"}, - }, - Content: Content{ - Size: 71, - MimeType: "application/json; charset=utf-8", - // map[Code:200 IsSuccess:true Message: Value:map[BlnResult:true]] - Text: "eyJJc1N1Y2Nlc3MiOnRydWUsIkNvZGUiOjIwMCwiTWVzc2FnZSI6bnVsbCwiVmFsdWUiOnsiQmxuUmVzdWx0Ijp0cnVlfX0=", - Encoding: "base64", - }, - }, - } - step, err := har.prepareTestStep(entry) - if !assert.NoError(t, err) { - t.Fatal() - } - validator, ok := step.Validators[0].(hrp.Validator) - if !ok { - t.Fatal() - } - if !assert.Equal(t, validator, - hrp.Validator{ - Check: "status_code", - Expect: 200, - Assert: "equals", - Message: "assert response status code", - }) { - t.Fatal() - } - - validator, ok = step.Validators[1].(hrp.Validator) - if !ok { - t.Fatal() - } - if !assert.Equal(t, validator, - hrp.Validator{ - Check: "headers.\"Content-Type\"", - Expect: "application/json; charset=utf-8", - Assert: "equals", - Message: "assert response header Content-Type", - }) { - t.Fatal() - } - - validator, ok = step.Validators[2].(hrp.Validator) - if !ok { - t.Fatal() - } - if !assert.Equal(t, validator, - hrp.Validator{ - Check: "body.Code", - Expect: float64(200), // TODO - Assert: "equals", - Message: "assert response body Code", - }) { - t.Fatal() - } -} diff --git a/hrp/internal/convert/har2case/har.go b/hrp/internal/convert/har2case/har.go deleted file mode 100644 index 6b98839a..00000000 --- a/hrp/internal/convert/har2case/har.go +++ /dev/null @@ -1,340 +0,0 @@ -package har2case - -import "time" - -/* -HTTP Archive (HAR) format -https://w3c.github.io/web-performance/specs/HAR/Overview.html -this file is copied from https://github.com/mrichman/hargo/blob/master/types.go -*/ - -// Har is a container type for deserialization -type Har struct { - Log Log `json:"log"` -} - -// Log represents the root of the exported data. This object MUST be present and its name MUST be "log". -type Log struct { - // The object contains the following name/value pairs: - - // Required. Version number of the format. - Version string `json:"version"` - // Required. An object of type creator that contains the name and version - // information of the log creator application. - Creator Creator `json:"creator"` - // Optional. An object of type browser that contains the name and version - // information of the user agent. - Browser Browser `json:"browser"` - // Optional. An array of objects of type page, each representing one exported - // (tracked) page. Leave out this field if the application does not support - // grouping by pages. - Pages []Page `json:"pages,omitempty"` - // Required. An array of objects of type entry, each representing one - // exported (tracked) HTTP request. - Entries []Entry `json:"entries"` - // Optional. A comment provided by the user or the application. Sorting - // entries by startedDateTime (starting from the oldest) is preferred way how - // to export data since it can make importing faster. However the reader - // application should always make sure the array is sorted (if required for - // the import). - Comment string `json:"comment"` -} - -// Creator contains information about the log creator application -type Creator struct { - // Required. The name of the application that created the log. - Name string `json:"name"` - // Required. The version number of the application that created the log. - Version string `json:"version"` - // Optional. A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// Browser that created the log -type Browser struct { - // Required. The name of the browser that created the log. - Name string `json:"name"` - // Required. The version number of the browser that created the log. - Version string `json:"version"` - // Optional. A comment provided by the user or the browser. - Comment string `json:"comment"` -} - -// Page object for every exported web page and one object for every HTTP request. -// In case when an HTTP trace tool isn't able to group requests by a page, -// the object is empty and individual requests doesn't have a parent page. -type Page struct { - /* There is one object for every exported web page and one - object for every HTTP request. In case when an HTTP trace tool isn't able to - group requests by a page, the object is empty and individual - requests doesn't have a parent page. - */ - - // Date and time stamp for the beginning of the page load - // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00). - StartedDateTime string `json:"startedDateTime"` - // Unique identifier of a page within the . Entries use it to refer the parent page. - ID string `json:"id"` - // Page title. - Title string `json:"title"` - // Detailed timing info about page load. - PageTiming PageTiming `json:"pageTiming"` - // (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// PageTiming describes timings for various events (states) fired during the page load. -// All times are specified in milliseconds. If a time info is not available appropriate field is set to -1. -type PageTiming struct { - // Content of the page loaded. Number of milliseconds since page load started - // (page.startedDateTime). Use -1 if the timing does not apply to the current - // request. - // Depeding on the browser, onContentLoad property represents DOMContentLoad - // event or document.readyState == interactive. - OnContentLoad int `json:"onContentLoad"` - // Page is loaded (onLoad event fired). Number of milliseconds since page - // load started (page.startedDateTime). Use -1 if the timing does not apply - // to the current request. - OnLoad int `json:"onLoad"` - // (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment"` -} - -// Entry is a unique, optional Reference to the parent page. -// Leave out this field if the application does not support grouping by pages. -type Entry struct { - Pageref string `json:"pageref,omitempty"` - // Date and time stamp of the request start - // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD). - StartedDateTime string `json:"startedDateTime"` - // Total elapsed time of the request in milliseconds. This is the sum of all - // timings available in the timings object (i.e. not including -1 values) . - Time float32 `json:"time"` - // Detailed info about the request. - Request Request `json:"request"` - // Detailed info about the response. - Response Response `json:"response"` - // Info about cache usage. - Cache Cache `json:"cache"` - // Detailed timing info about request/response round trip. - PageTimings PageTimings `json:"pageTimings"` - // optional (new in 1.2) IP address of the server that was connected - // (result of DNS resolution). - ServerIPAddress string `json:"serverIPAddress,omitempty"` - // optional (new in 1.2) Unique ID of the parent TCP/IP connection, can be - // the client port number. Note that a port number doesn't have to be unique - // identifier in cases where the port is shared for more connections. If the - // port isn't available for the application, any other unique connection ID - // can be used instead (e.g. connection index). Leave out this field if the - // application doesn't support this info. - Connection string `json:"connection,omitempty"` - // (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// Request contains detailed info about performed request. -type Request struct { - // Request method (GET, POST, ...). - Method string `json:"method"` - // Absolute URL of the request (fragments are not included). - URL string `json:"url"` - // Request HTTP Version. - HTTPVersion string `json:"httpVersion"` - // List of cookie objects. - Cookies []Cookie `json:"cookies"` - // List of header objects. - Headers []NVP `json:"headers"` - // List of query parameter objects. - QueryString []NVP `json:"queryString"` - // Posted data. - PostData PostData `json:"postData"` - // Total number of bytes from the start of the HTTP request message until - // (and including) the double CRLF before the body. Set to -1 if the info - // is not available. - HeaderSize int `json:"headerSize"` - // Size of the request body (POST data payload) in bytes. Set to -1 if the - // info is not available. - BodySize int `json:"bodySize"` - // (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment"` -} - -// Response contains detailed info about the response. -type Response struct { - // Response status. - Status int `json:"status"` - // Response status description. - StatusText string `json:"statusText"` - // Response HTTP Version. - HTTPVersion string `json:"httpVersion"` - // List of cookie objects. - Cookies []Cookie `json:"cookies"` - // List of header objects. - Headers []NVP `json:"headers"` - // Details about the response body. - Content Content `json:"content"` - // Redirection target URL from the Location response header. - RedirectURL string `json:"redirectURL"` - // Total number of bytes from the start of the HTTP response message until - // (and including) the double CRLF before the body. Set to -1 if the info is - // not available. - // The size of received response-headers is computed only from headers that - // are really received from the server. Additional headers appended by the - // browser are not included in this number, but they appear in the list of - // header objects. - HeadersSize int `json:"headersSize"` - // Size of the received response body in bytes. Set to zero in case of - // responses coming from the cache (304). Set to -1 if the info is not - // available. - BodySize int `json:"bodySize"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// Cookie contains list of all cookies (used in and objects). -type Cookie struct { - // The name of the cookie. - Name string `json:"name"` - // The cookie value. - Value string `json:"value"` - // optional The path pertaining to the cookie. - Path string `json:"path,omitempty"` - // optional The host of the cookie. - Domain string `json:"domain,omitempty"` - // optional Cookie expiration time. - // (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.123+02:00). - Expires string `json:"expires,omitempty"` - // optional Set to true if the cookie is HTTP only, false otherwise. - HTTPOnly bool `json:"httpOnly,omitempty"` - // optional (new in 1.2) True if the cookie was transmitted over ssl, false - // otherwise. - Secure bool `json:"secure,omitempty"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment bool `json:"comment,omitempty"` -} - -// NVP is simply a name/value pair with a comment -type NVP struct { - Name string `json:"name"` - Value string `json:"value"` - Comment string `json:"comment,omitempty"` -} - -// PostData describes posted data, if any (embedded in object). -type PostData struct { - // Mime type of posted data. - MimeType string `json:"mimeType"` - // List of posted parameters (in case of URL encoded parameters). - Params []PostParam `json:"params"` - // Plain text posted data - Text string `json:"text"` - // optional (new in 1.2) A comment provided by the user or the - // application. - Comment string `json:"comment,omitempty"` -} - -// PostParam is a list of posted parameters, if any (embedded in object). -type PostParam struct { - // name of a posted parameter. - Name string `json:"name"` - // optional value of a posted parameter or content of a posted file. - Value string `json:"value,omitempty"` - // optional name of a posted file. - FileName string `json:"fileName,omitempty"` - // optional content type of a posted file. - ContentType string `json:"contentType,omitempty"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// Content describes details about response content (embedded in object). -type Content struct { - // Length of the returned content in bytes. Should be equal to - // response.bodySize if there is no compression and bigger when the content - // has been compressed. - Size int `json:"size"` - // optional Number of bytes saved. Leave out this field if the information - // is not available. - Compression int `json:"compression,omitempty"` - // MIME type of the response text (value of the Content-Type response - // header). The charset attribute of the MIME type is included (if - // available). - MimeType string `json:"mimeType"` - // optional Response body sent from the server or loaded from the browser - // cache. This field is populated with textual content only. The text field - // is either HTTP decoded text or a encoded (e.g. "base64") representation of - // the response body. Leave out this field if the information is not - // available. - Text string `json:"text,omitempty"` - // optional (new in 1.2) Encoding used for response text field e.g - // "base64". Leave out this field if the text field is HTTP decoded - // (decompressed & unchunked), than trans-coded from its original character - // set into UTF-8. - Encoding string `json:"encoding,omitempty"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// Cache contains info about a request coming from browser cache. -type Cache struct { - // optional State of a cache entry before the request. Leave out this field - // if the information is not available. - BeforeRequest CacheObject `json:"beforeRequest,omitempty"` - // optional State of a cache entry after the request. Leave out this field if - // the information is not available. - AfterRequest CacheObject `json:"afterRequest,omitempty"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// CacheObject is used by both beforeRequest and afterRequest -type CacheObject struct { - // optional - Expiration time of the cache entry. - Expires string `json:"expires,omitempty"` - // The last time the cache entry was opened. - LastAccess string `json:"lastAccess"` - // Etag - ETag string `json:"eTag"` - // The number of times the cache entry has been opened. - HitCount int `json:"hitCount"` - // optional (new in 1.2) A comment provided by the user or the application. - Comment string `json:"comment,omitempty"` -} - -// PageTimings describes various phases within request-response round trip. -// All times are specified in milliseconds. -type PageTimings struct { - Blocked int `json:"blocked,omitempty"` - // optional - Time spent in a queue waiting for a network connection. Use -1 - // if the timing does not apply to the current request. - DNS int `json:"dns,omitempty"` - // optional - DNS resolution time. The time required to resolve a host name. - // Use -1 if the timing does not apply to the current request. - Connect int `json:"connect,omitempty"` - // optional - Time required to create TCP connection. Use -1 if the timing - // does not apply to the current request. - Send int `json:"send"` - // Time required to send HTTP request to the server. - Wait int `json:"wait"` - // Waiting for a response from the server. - Receive int `json:"receive"` - // Time required to read entire response from the server (or cache). - Ssl int `json:"ssl,omitempty"` - // optional (new in 1.2) - Time required for SSL/TLS negotiation. If this - // field is defined then the time is also included in the connect field (to - // ensure backward compatibility with HAR 1.1). Use -1 if the timing does not - // apply to the current request. - Comment string `json:"comment,omitempty"` - // optional (new in 1.2) - A comment provided by the user or the application. -} - -// TestResult contains results for an individual HTTP request -type TestResult struct { - URL string `json:"url"` - Status int `json:"status"` // 200, 500, etc. - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` - Latency int `json:"latency"` // milliseconds - Method string `json:"method"` - HarFile string `json:"harfile"` -} diff --git a/hrp/testcase.go b/hrp/testcase.go index 41826716..66690340 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -193,17 +193,17 @@ func convertValidatorCompat2GoEngine(Validators []interface{}) (err error) { } validatorMap := iValidator.(map[string]interface{}) validator := Validator{} - _, checkExisted := validatorMap["check"] - _, assertExisted := validatorMap["assert"] - _, expectExisted := validatorMap["expect"] + 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 = validatorMap["check"].(string) - validator.Assert = validatorMap["assert"].(string) - validator.Expect = validatorMap["expect"] - if msg, existed := validatorMap["msg"]; existed { - validator.Message = msg.(string) + 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 @@ -212,13 +212,16 @@ func convertValidatorCompat2GoEngine(Validators []interface{}) (err error) { if len(validatorMap) == 1 { // Python engine style for assertMethod, iValidatorContent := range validatorMap { - checkAndExpect := iValidatorContent.([]interface{}) - if len(checkAndExpect) != 2 { + validatorContent := iValidatorContent.([]interface{}) + if len(validatorContent) > 3 { return fmt.Errorf("unexpected validator format: %v", validatorMap) } - validator.Check = checkAndExpect[0].(string) + validator.Check = validatorContent[0].(string) validator.Assert = assertMethod - validator.Expect = checkAndExpect[1] + validator.Expect = validatorContent[1] + if len(validatorContent) == 3 { + validator.Message = validatorContent[2].(string) + } } validator.Check = convertCheckExpr(validator.Check) Validators[i] = validator @@ -294,23 +297,26 @@ func convertValidatorCompat2PyEngine(Validators []interface{}) (err error) { if len(validatorMap) == 1 { // Python engine style for _, iValidatorContent := range validatorMap { - checkAndExpect := iValidatorContent.([]interface{}) - if len(checkAndExpect) != 2 { + validatorContent := iValidatorContent.([]interface{}) + if len(validatorContent) > 3 { return fmt.Errorf("unexpected validator format: %v", validatorMap) } } continue } - _, checkExisted := validatorMap["check"] - _, assertExisted := validatorMap["assert"] - _, expectExisted := validatorMap["expect"] + iCheck, checkExisted := validatorMap["check"] + iAssert, assertExisted := validatorMap["assert"] + iExpect, expectExisted := validatorMap["expect"] if checkExisted && assertExisted && expectExisted { // Golang engine style - var iValidatorContent []interface{} - iValidatorContent = append(iValidatorContent, validatorMap["check"]) - iValidatorContent = append(iValidatorContent, validatorMap["expect"]) + 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[validatorMap["assert"].(string)] = iValidatorContent + newValidatorMap[iAssert.(string)] = validatorContent Validators[i] = newValidatorMap continue }