mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-14 03:39:41 +08:00
fix comment
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# har2case
|
||||
|
||||
Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
|
||||
|
||||
## Install
|
||||
|
||||
## Quick Start
|
||||
|
||||
## Examples
|
||||
@@ -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)]
|
||||
}
|
||||
@@ -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:<nil> 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()
|
||||
}
|
||||
}
|
||||
@@ -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 <entry> object for every HTTP request.
|
||||
// In case when an HTTP trace tool isn't able to group requests by a page,
|
||||
// the <pages> object is empty and individual requests doesn't have a parent page.
|
||||
type Page struct {
|
||||
/* There is one <page> object for every exported web page and one <entry>
|
||||
object for every HTTP request. In case when an HTTP trace tool isn't able to
|
||||
group requests by a page, the <pages> 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 <request> and <response> 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 <request> 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 <postData> 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 <response> 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user