From d96baa789b529cfbd0feb5272153af4c2e41cebc Mon Sep 17 00:00:00 2001 From: buyuxiang <347586493@qq.com> Date: Tue, 24 May 2022 20:50:53 +0800 Subject: [PATCH] fix comment --- docs/cmd/hrp.md | 2 +- docs/cmd/hrp_convert.md | 2 +- .../postman_collection.json} | 0 .../{postman2case => postman}/profile.yml | 0 .../profile_override.yml | 0 hrp/cmd/convert.go | 5 +- hrp/cmd/har2case.go | 50 +-- hrp/internal/builtin/utils.go | 2 +- hrp/internal/convert/README.md | 28 +- hrp/internal/convert/asset/flowgram.png | Bin 0 -> 59725 bytes hrp/internal/convert/asset/flowgram.svg | 1 - hrp/internal/convert/converter.go | 23 +- hrp/internal/convert/converter_har.go | 2 +- .../convert/converter_postman_test.go | 6 +- hrp/internal/convert/har2case/README.md | 9 - hrp/internal/convert/har2case/core.go | 385 ------------------ hrp/internal/convert/har2case/core_test.go | 383 ----------------- hrp/internal/convert/har2case/har.go | 340 ---------------- hrp/testcase.go | 48 ++- 19 files changed, 82 insertions(+), 1204 deletions(-) rename examples/data/{postman2case/demo.json => postman/postman_collection.json} (100%) rename examples/data/{postman2case => postman}/profile.yml (100%) rename examples/data/{postman2case => postman}/profile_override.yml (100%) create mode 100644 hrp/internal/convert/asset/flowgram.png delete mode 100644 hrp/internal/convert/asset/flowgram.svg delete mode 100644 hrp/internal/convert/har2case/README.md delete mode 100644 hrp/internal/convert/har2case/core.go delete mode 100644 hrp/internal/convert/har2case/core_test.go delete mode 100644 hrp/internal/convert/har2case/har.go 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 引擎的风格 ## 转换流程图 -![flow chart](asset/flowgram.svg) +`hrp convert` 的转换过程流程图如下: +![flow chart](asset/flowgram.png) ## 开发进度 +`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 0000000000000000000000000000000000000000..3e676ec718bca2e95130b5df9f145921d8a76784 GIT binary patch literal 59725 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 literal 0 HcmV?d00001 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 }