From e3596a6051a43d246ad4a37f65b1cace451eea85 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 23 Dec 2022 12:03:49 +0800 Subject: [PATCH] refactor: convert postman case --- hrp/cmd/convert.go | 114 ++++--- hrp/cmd/curl.go | 98 ------ hrp/convert.go | 1 + hrp/pkg/convert/README.md | 31 +- hrp/pkg/convert/converter.go | 278 ------------------ hrp/pkg/convert/from_ab.go | 1 + hrp/pkg/convert/from_gotest.go | 4 +- hrp/pkg/convert/from_jmeter.go | 1 + hrp/pkg/convert/from_json.go | 3 +- hrp/pkg/convert/from_postman.go | 2 +- hrp/pkg/convert/from_yaml.go | 2 +- hrp/pkg/convert/main.go | 205 +++++++++++++ .../{converter_test.go => main_test.go} | 36 +-- hrp/pkg/convert/to_gotest.go | 6 + hrp/pkg/convert/to_json.go | 13 + hrp/pkg/convert/to_pytest.go | 22 ++ hrp/pkg/convert/to_yaml.go | 13 + 17 files changed, 371 insertions(+), 459 deletions(-) delete mode 100644 hrp/cmd/curl.go create mode 100644 hrp/convert.go delete mode 100644 hrp/pkg/convert/converter.go create mode 100644 hrp/pkg/convert/from_ab.go create mode 100644 hrp/pkg/convert/from_jmeter.go create mode 100644 hrp/pkg/convert/main.go rename hrp/pkg/convert/{converter_test.go => main_test.go} (77%) create mode 100644 hrp/pkg/convert/to_gotest.go create mode 100644 hrp/pkg/convert/to_json.go create mode 100644 hrp/pkg/convert/to_pytest.go create mode 100644 hrp/pkg/convert/to_yaml.go diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 5ec00480..e2a73e6e 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -1,7 +1,6 @@ package cmd import ( - "errors" "fmt" "github.com/rs/zerolog/log" @@ -13,65 +12,86 @@ import ( ) var convertCmd = &cobra.Command{ - Use: "convert $path...", - Short: "convert to JSON/YAML/gotest/pytest testcases", - Args: cobra.MinimumNArgs(1), + Use: "convert $path...", + Short: "convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases", + Args: cobra.MinimumNArgs(1), + SilenceUsage: false, PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - RunE: convertRun, + RunE: func(cmd *cobra.Command, args []string) error { + caseConverter := convert.NewConverter(outputDir, profilePath) + + var fromType convert.FromType + if fromYAMLFlag { + fromType = convert.FromTypeYAML + } else if fromPostmanFlag { + fromType = convert.FromTypePostman + } else if fromHARFlag { + fromType = convert.FromTypeHAR + } else { + fromType = convert.FromTypeJSON + log.Info().Str("fromType", fromType.String()).Msg("set default") + } + + var outputType convert.OutputType + if toYAMLFlag { + outputType = convert.OutputTypeYAML + } else if toPyTestFlag { + packages := []string{ + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), + } + _, err := myexec.EnsurePython3Venv(venv, packages...) + if err != nil { + log.Error().Err(err).Msg("python3 venv is not ready") + return err + } + + outputType = convert.OutputTypePyTest + } else { + outputType = convert.OutputTypeJSON + log.Info().Str("outputType", outputType.String()).Msg("set default") + } + + for _, arg := range args { + if err := caseConverter.Convert(arg, fromType, outputType); err != nil { + log.Error().Err(err).Str("path", arg). + Str("outputType", outputType.String()). + Msg("convert case failed") + return err + } + } + + return nil + }, } var ( + outputDir string + profilePath string + + fromJSONFlag bool + fromYAMLFlag bool + fromPostmanFlag bool + fromHARFlag bool + toJSONFlag bool toYAMLFlag bool - toGoTestFlag bool toPyTestFlag bool - outputDir string - profilePath string - - outputType convert.OutputType ) func init() { rootCmd.AddCommand(convertCmd) + + convertCmd.Flags().BoolVar(&fromJSONFlag, "from-json", true, "load from json case format") + convertCmd.Flags().BoolVar(&fromYAMLFlag, "from-yaml", false, "load from yaml case format") + convertCmd.Flags().BoolVar(&fromHARFlag, "from-har", false, "load from HAR format") + convertCmd.Flags().BoolVar(&fromPostmanFlag, "from-postman", false, "load from postman format") + + convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", true, "convert to JSON case scripts") + convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML case scripts") convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts") - convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)") - convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)") - convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts") - convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") + + convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory") convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies") } - -func convertRun(cmd *cobra.Command, args []string) error { - var flagCount int - if toJSONFlag { - flagCount++ - } - if toYAMLFlag { - flagCount++ - outputType = convert.OutputTypeYAML - } - if toGoTestFlag { - flagCount++ - outputType = convert.OutputTypeGoTest - } - if toPyTestFlag { - flagCount++ - outputType = convert.OutputTypePyTest - - packages := []string{ - fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), - } - _, err := myexec.EnsurePython3Venv(venv, packages...) - if err != nil { - log.Error().Err(err).Msg("python3 venv is not ready") - return err - } - } - if flagCount > 1 { - return errors.New("please specify at most one conversion flag") - } - convert.Run(outputType, outputDir, profilePath, args) - return nil -} diff --git a/hrp/cmd/curl.go b/hrp/cmd/curl.go deleted file mode 100644 index a75ae84b..00000000 --- a/hrp/cmd/curl.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/env" - "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" - "github.com/httprunner/httprunner/v4/hrp/pkg/convert" -) - -var runCurlCmd = &cobra.Command{ - Use: "curl URLs", - Short: "run API test with curl command", - Args: cobra.MinimumNArgs(1), - DisableFlagParsing: true, - PreRun: func(cmd *cobra.Command, args []string) { - setLogLevel(logLevel) - }, - RunE: func(cmd *cobra.Command, args []string) error { - runner := makeHRPRunner() - return runner.Run(makeCurlTestCase(args)) - }, -} - -var boomCurlCmd = &cobra.Command{ - Use: "curl URLs", - Short: "run load test with curl command", - Args: cobra.MinimumNArgs(1), - DisableFlagParsing: true, - PreRun: func(cmd *cobra.Command, args []string) { - boomer.SetUlimit(10240) - if !strings.EqualFold(logLevel, "DEBUG") { - logLevel = "WARN" // disable info logs for load testing - } - setLogLevel(logLevel) - }, - RunE: func(cmd *cobra.Command, args []string) error { - boomer, err := makeHRPBoomer() - if err != nil { - return err - } - boomer.Run(makeCurlTestCase(args)) - return nil - }, -} - -var convertCurlCmd = &cobra.Command{ - Use: "curl URLs", - Short: "convert curl command to httprunner testcase", - Args: cobra.MinimumNArgs(1), - DisableFlagParsing: true, - PreRun: func(cmd *cobra.Command, args []string) { - setLogLevel(logLevel) - }, - RunE: func(cmd *cobra.Command, args []string) error { - curlCommand := makeCurlCommand(args) - return convertRun(cmd, []string{curlCommand}) - }, -} - -func init() { - runCmd.AddCommand(runCurlCmd) - boomCmd.AddCommand(boomCurlCmd) - convertCmd.AddCommand(convertCurlCmd) -} - -func makeCurlTestCase(args []string) *hrp.TestCase { - curlCommand := makeCurlCommand(args) - tCase, err := convert.LoadSingleCurlCase(curlCommand) - if err != nil { - log.Error().Err(err).Msg("convert curl command failed") - os.Exit(1) - } - testCase, err := tCase.ToTestCase(env.RootDir) - if err != nil { - log.Error().Err(err).Msg("convert testcase to failed") - os.Exit(1) - } - return testCase -} - -func makeCurlCommand(args []string) string { - for i := 0; i < len(args); i++ { - if !strings.HasPrefix(args[i], "-") { - args[i] = fmt.Sprintf("\"%s\"", args[i]) - } - } - var curlCmd []string - curlCmd = append(curlCmd, "curl") - curlCmd = append(curlCmd, args...) - return strings.Join(curlCmd, " ") -} diff --git a/hrp/convert.go b/hrp/convert.go new file mode 100644 index 00000000..d10a1454 --- /dev/null +++ b/hrp/convert.go @@ -0,0 +1 @@ +package hrp diff --git a/hrp/pkg/convert/README.md b/hrp/pkg/convert/README.md index 90c0314f..56e8b3ad 100644 --- a/hrp/pkg/convert/README.md +++ b/hrp/pkg/convert/README.md @@ -4,32 +4,36 @@ ```shell $ hrp convert -h -convert to JSON/YAML/gotest/pytest testcases +convert multiple source format to HttpRunner JSON/YAML/gotest/pytest cases Usage: hrp convert $path... [flags] Flags: + --from-har load from HAR format + --from-json load from json case format (default true) + --from-postman load from postman format + --from-yaml load from yaml case format -h, --help help for convert - -d, --output-dir string specify output directory, default to the same dir with har file - -p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies - --to-gotest convert to gotest scripts (TODO) - --to-json convert to JSON scripts (default true) + -d, --output-dir string specify output directory + -p, --profile string specify profile path to override headers and cookies + --to-json convert to JSON case scripts (default true) --to-pytest convert to pytest scripts - --to-yaml convert to YAML scripts + --to-yaml convert to YAML case scripts Global Flags: --log-json set log to json format -l, --log-level string set log level (default "INFO") + --venv string specify python3 venv path ``` -`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化。 +`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 HttpRunner JSON/YAML/gotest/pytest 形态的测试用例,同时也支持 HttpRunner 测试用例各个形态之间的相互转化。 该指令所有选项的详细说明如下: -1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例 -2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹 -3. `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例: +- `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的 HttpRunner 测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例 +- `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹;默认输出的文件夹为源文件所在的文件夹 +- `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例: - 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息 @@ -52,10 +56,9 @@ cookies: ## 注意事项 -1. 输出的测试用例文件名格式为 `Postman 工程文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖 -2. `hrp convert` 可以自动识别输入类型,因此不需要通过选项来手动制定输入类型,如遇到无法识别、不支持或转换失败的情况,则会输出错误日志并跳过,不会影响其他转换过程的正常进行 -3. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式 -4. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格 +1. 输出的测试用例文件名格式为 `源文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖 +2. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式 +3. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格 ## 转换流程图 diff --git a/hrp/pkg/convert/converter.go b/hrp/pkg/convert/converter.go deleted file mode 100644 index c849df65..00000000 --- a/hrp/pkg/convert/converter.go +++ /dev/null @@ -1,278 +0,0 @@ -package convert - -import ( - _ "embed" - "fmt" - "path/filepath" - "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/env" - "github.com/httprunner/httprunner/v4/hrp/internal/myexec" - "github.com/httprunner/httprunner/v4/hrp/internal/sdk" -) - -// target testcase format extensions -const ( - suffixJSON = ".json" - suffixYAML = ".yaml" - suffixGoTest = ".go" - suffixPyTest = ".py" -) - -type OutputType int - -const ( - OutputTypeJSON OutputType = iota // default output type: JSON - OutputTypeYAML - OutputTypeGoTest - OutputTypePyTest -) - -func (outputType OutputType) String() string { - switch outputType { - case OutputTypeYAML: - return "yaml" - case OutputTypeGoTest: - return "gotest" - case OutputTypePyTest: - return "pytest" - default: - return "json" - } -} - -// Profile is used to override or update(create if not existed) original headers and cookies -type Profile struct { - Override bool `json:"override" yaml:"override"` - Headers map[string]string `json:"headers" yaml:"headers"` - Cookies map[string]string `json:"cookies" yaml:"cookies"` -} - -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()), - }) - - var outputFiles []string - for _, inputSample := range args { - // loads source file and convert to TCase format - tCase, err := LoadTCase(inputSample) - if err != nil { - log.Warn().Err(err).Str("input sample", inputSample).Msg("convert input sample failed") - continue - } - - caseConverter := &TCaseConverter{ - InputSample: inputSample, - OutputDir: outputDir, - TCase: tCase, - } - - // override TCase with profile - if profilePath != "" { - caseConverter.overrideWithProfile(profilePath) - } - - // convert TCase format to target case format - var outputFile string - switch outputType { - case OutputTypeYAML: - outputFile, err = caseConverter.ToYAML() - case OutputTypeGoTest: - outputFile, err = caseConverter.ToGoTest() - case OutputTypePyTest: - outputFile, err = caseConverter.ToPyTest() - default: - outputFile, err = caseConverter.ToJSON() - } - if err != nil { - log.Error().Err(err). - Str("input sample", caseConverter.InputSample). - Msg("convert case failed") - continue - } - outputFiles = append(outputFiles, outputFile) - } - log.Info().Strs("output files", outputFiles).Msg("conversion completed") -} - -// LoadTCase loads source file and convert to TCase type -func LoadTCase(inputSample string) (*hrp.TCase, error) { - if strings.HasPrefix(inputSample, "curl ") { - // 'path' contains curl command - curlCase, err := LoadSingleCurlCase(inputSample) - if err != nil { - return nil, err - } - return curlCase, nil - } - extName := filepath.Ext(inputSample) - if extName == "" { - return nil, errors.New("file extension is not specified") - } - switch extName { - case ".har": - tCase, err := LoadHARCase(inputSample) - if err != nil { - return nil, err - } - return tCase, nil - case ".json": - // priority: hrp JSON case > postman > swagger - // check if hrp JSON case - tCase, err := LoadJSONCase(inputSample) - if err == nil { - return tCase, nil - } - - // check if postman format - casePostman, err := LoadPostmanCase(inputSample) - if err == nil { - return casePostman, nil - } - - // check if swagger format - caseSwagger, err := LoadSwaggerCase(inputSample) - if err == nil { - return caseSwagger, nil - } - - return nil, errors.New("unexpected JSON format") - case ".yaml", ".yml": - // priority: hrp YAML case > swagger - // check if hrp YAML case - tCase, err := NewYAMLCase(inputSample) - if err == nil { - return tCase, nil - } - - // check if swagger format - caseSwagger, err := LoadSwaggerCase(inputSample) - if err == nil { - return caseSwagger, nil - } - - return nil, errors.New("unexpected YAML format") - case ".go": // TODO - return nil, errors.New("convert gotest is not implemented") - case ".py": // TODO - return nil, errors.New("convert pytest is not implemented") - case ".jmx": // TODO - return nil, errors.New("convert JMeter jmx is not implemented") - case ".txt": - curlCase, err := LoadCurlCase(inputSample) - if err != nil { - return nil, err - } - return curlCase, nil - } - - return nil, fmt.Errorf("unsupported file type: %v", extName) -} - -// TCaseConverter holds the common properties of case converter -type TCaseConverter struct { - InputSample string - OutputDir string - TCase *hrp.TCase -} - -func (c *TCaseConverter) genOutputPath(suffix string) string { - var outFileFullName string - if curlCmd := strings.TrimSpace(c.InputSample); strings.HasPrefix(curlCmd, "curl ") { - outFileFullName = fmt.Sprintf("curl_%v_test%v", env.StartTimeStr, suffix) - if c.OutputDir != "" { - return filepath.Join(c.OutputDir, outFileFullName) - } else { - return filepath.Join(env.RootDir, outFileFullName) - } - } - outFileFullName = builtin.GetFileNameWithoutExtension(c.InputSample) + "_test" + suffix - if c.OutputDir != "" { - return filepath.Join(c.OutputDir, outFileFullName) - } else { - return filepath.Join(filepath.Dir(c.InputSample), outFileFullName) - } - // TODO avoid outFileFullName conflict? -} - -// convert TCase to pytest case -func (c *TCaseConverter) ToPyTest() (string, error) { - jsonPath, err := c.ToJSON() - if err != nil { - return "", errors.Wrap(err, "convert to JSON case failed") - } - - args := append([]string{"make"}, jsonPath) - err = myexec.ExecPython3Command("httprunner", args...) - if err != nil { - return "", err - } - return c.genOutputPath(suffixPyTest), nil -} - -// TODO: convert TCase to gotest case -func (c *TCaseConverter) ToGoTest() (string, error) { - return "", nil -} - -// convert TCase to JSON case -func (c *TCaseConverter) ToJSON() (string, error) { - jsonPath := c.genOutputPath(suffixJSON) - err := builtin.Dump2JSON(c.TCase, jsonPath) - if err != nil { - return "", err - } - return jsonPath, nil -} - -// convert TCase to YAML case -func (c *TCaseConverter) ToYAML() (string, error) { - yamlPath := c.genOutputPath(suffixYAML) - err := builtin.Dump2YAML(c.TCase, yamlPath) - if err != nil { - return "", err - } - return yamlPath, nil -} - -func (c *TCaseConverter) overrideWithProfile(path string) error { - log.Info().Str("path", path).Msg("load profile") - profile := new(Profile) - err := builtin.LoadFile(path, profile) - if err != nil { - log.Warn().Str("path", path). - Msg("failed to load profile, ignore!") - return err - } - - log.Info().Interface("profile", profile).Msg("override with profile") - for _, step := range c.TCase.TestSteps { - // override original headers and cookies - if profile.Override { - step.Request.Headers = make(map[string]string) - step.Request.Cookies = make(map[string]string) - } - // update (create if not existed) original headers and cookies - if step.Request.Headers == nil { - step.Request.Headers = make(map[string]string) - } - if step.Request.Cookies == nil { - step.Request.Cookies = make(map[string]string) - } - for k, v := range profile.Headers { - step.Request.Headers[k] = v - } - for k, v := range profile.Cookies { - step.Request.Cookies[k] = v - } - } - return nil -} diff --git a/hrp/pkg/convert/from_ab.go b/hrp/pkg/convert/from_ab.go new file mode 100644 index 00000000..233bcded --- /dev/null +++ b/hrp/pkg/convert/from_ab.go @@ -0,0 +1 @@ +package convert diff --git a/hrp/pkg/convert/from_gotest.go b/hrp/pkg/convert/from_gotest.go index eecde5a5..d25dff85 100644 --- a/hrp/pkg/convert/from_gotest.go +++ b/hrp/pkg/convert/from_gotest.go @@ -31,9 +31,9 @@ func convert2GoTestScripts(paths ...string) error { for _, testCase := range testCases { tc := testCase.ToTCase() converter := TCaseConverter{ - TCase: tc, + tCase: tc, } - pytestPath, err := converter.ToPyTest() + pytestPath, err := converter.toPyTest() if err != nil { log.Error().Err(err). Str("originPath", tc.Config.Path). diff --git a/hrp/pkg/convert/from_jmeter.go b/hrp/pkg/convert/from_jmeter.go new file mode 100644 index 00000000..233bcded --- /dev/null +++ b/hrp/pkg/convert/from_jmeter.go @@ -0,0 +1 @@ +package convert diff --git a/hrp/pkg/convert/from_json.go b/hrp/pkg/convert/from_json.go index 8e40f88b..b1c05ac5 100644 --- a/hrp/pkg/convert/from_json.go +++ b/hrp/pkg/convert/from_json.go @@ -2,13 +2,14 @@ package convert import ( "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) func LoadJSONCase(path string) (*hrp.TCase, error) { - // load json case file + log.Info().Str("path", path).Msg("load json case file") caseJSON := new(hrp.TCase) err := builtin.LoadFile(path, caseJSON) if err != nil { diff --git a/hrp/pkg/convert/from_postman.go b/hrp/pkg/convert/from_postman.go index 9dbbfa7c..bd8e1527 100644 --- a/hrp/pkg/convert/from_postman.go +++ b/hrp/pkg/convert/from_postman.go @@ -113,7 +113,7 @@ var contentTypeMap = map[string]string{ } func LoadPostmanCase(path string) (*hrp.TCase, error) { - // load postman file + log.Info().Str("path", path).Msg("load postman case file") casePostman, err := loadCasePostman(path) if err != nil { return nil, err diff --git a/hrp/pkg/convert/from_yaml.go b/hrp/pkg/convert/from_yaml.go index 977e47de..b96f0a2d 100644 --- a/hrp/pkg/convert/from_yaml.go +++ b/hrp/pkg/convert/from_yaml.go @@ -9,7 +9,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) -func NewYAMLCase(path string) (*hrp.TCase, error) { +func LoadYAMLCase(path string) (*hrp.TCase, error) { // load yaml case file caseJSON := new(hrp.TCase) err := builtin.LoadFile(path, caseJSON) diff --git a/hrp/pkg/convert/main.go b/hrp/pkg/convert/main.go new file mode 100644 index 00000000..602489fe --- /dev/null +++ b/hrp/pkg/convert/main.go @@ -0,0 +1,205 @@ +package convert + +import ( + _ "embed" + "fmt" + "path/filepath" + + "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/sdk" +) + +// target testcase format extensions +const ( + suffixJSON = ".json" + suffixYAML = ".yaml" + suffixGoTest = ".go" + suffixPyTest = ".py" +) + +type FromType int + +const ( + FromTypeJSON FromType = iota + FromTypeYAML + FromTypeHAR + FromTypePostman + FromTypeCurl + FromTypeSwagger + FromTypePyest + FromTypeGotest +) + +func (fromType FromType) String() string { + switch fromType { + case FromTypeYAML: + return "yaml" + case FromTypeHAR: + return "har" + case FromTypePostman: + return "postman" + case FromTypeSwagger: + return "swagger" + case FromTypeCurl: + return "curl" + case FromTypeGotest: + return "gotest" + case FromTypePyest: + return "pytest" + default: + return "json" + } +} + +type OutputType int + +const ( + OutputTypeJSON OutputType = iota // default output type: JSON + OutputTypeYAML + OutputTypeGoTest + OutputTypePyTest +) + +func (outputType OutputType) String() string { + switch outputType { + case OutputTypeYAML: + return "yaml" + case OutputTypeGoTest: + return "gotest" + case OutputTypePyTest: + return "pytest" + default: + return "json" + } +} + +// Profile is used to override or update(create if not existed) original headers and cookies +type Profile struct { + Override bool `json:"override" yaml:"override"` + Headers map[string]string `json:"headers" yaml:"headers"` + Cookies map[string]string `json:"cookies" yaml:"cookies"` +} + +func NewConverter(outputDir, profilePath string) *TCaseConverter { + return &TCaseConverter{ + profilePath: profilePath, + outputDir: outputDir, + } +} + +// TCaseConverter holds the common properties of case converter +type TCaseConverter struct { + fromFile string + profilePath string + outputDir string + tCase *hrp.TCase +} + +// LoadCase loads source file and convert to TCase type +func (c *TCaseConverter) loadCase(casePath string, fromType FromType) error { + c.fromFile = casePath + var err error + switch fromType { + case FromTypeJSON: + c.tCase, err = LoadJSONCase(casePath) + case FromTypeYAML: + c.tCase, err = LoadYAMLCase(casePath) + case FromTypeHAR: + c.tCase, err = LoadHARCase(casePath) + case FromTypePostman: + c.tCase, err = LoadPostmanCase(casePath) + case FromTypeSwagger: + c.tCase, err = LoadSwaggerCase(casePath) + case FromTypeCurl: + c.tCase, err = LoadCurlCase(casePath) + } + return err +} + +func (c *TCaseConverter) Convert(casePath string, fromType FromType, outputType OutputType) error { + // report event + sdk.SendEvent(sdk.EventTracking{ + Category: "ConvertTests", + Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()), + }) + log.Info().Str("path", casePath). + Str("fromType", fromType.String()). + Str("outputType", outputType.String()). + Msg("convert testcase") + + // load source file + err := c.loadCase(casePath, fromType) + if err != nil { + return err + } + + // override TCase with profile + if c.profilePath != "" { + c.overrideWithProfile(c.profilePath) + } + + // convert to target format + var outputFile string + switch outputType { + case OutputTypeYAML: + outputFile, err = c.toYAML() + case OutputTypeGoTest: + outputFile, err = c.toGoTest() + case OutputTypePyTest: + outputFile, err = c.toPyTest() + default: + outputFile, err = c.toJSON() + } + if err != nil { + return err + } + + log.Info().Str("outputFile", outputFile).Msg("conversion completed") + return nil +} + +func (c *TCaseConverter) genOutputPath(suffix string) string { + outFileFullName := builtin.GetFileNameWithoutExtension(c.fromFile) + "_test" + suffix + if c.outputDir != "" { + return filepath.Join(c.outputDir, outFileFullName) + } else { + return filepath.Join(filepath.Dir(c.fromFile), outFileFullName) + } +} + +func (c *TCaseConverter) overrideWithProfile(path string) error { + log.Info().Str("path", path).Msg("load profile") + profile := new(Profile) + err := builtin.LoadFile(path, profile) + if err != nil { + log.Warn().Str("path", path). + Msg("failed to load profile, ignore!") + return err + } + + log.Info().Interface("profile", profile).Msg("override with profile") + for _, step := range c.tCase.TestSteps { + // override original headers and cookies + if profile.Override { + step.Request.Headers = make(map[string]string) + step.Request.Cookies = make(map[string]string) + } + // update (create if not existed) original headers and cookies + if step.Request.Headers == nil { + step.Request.Headers = make(map[string]string) + } + if step.Request.Cookies == nil { + step.Request.Cookies = make(map[string]string) + } + for k, v := range profile.Headers { + step.Request.Headers[k] = v + } + for k, v := range profile.Cookies { + step.Request.Cookies[k] = v + } + } + return nil +} diff --git a/hrp/pkg/convert/converter_test.go b/hrp/pkg/convert/main_test.go similarity index 77% rename from hrp/pkg/convert/converter_test.go rename to hrp/pkg/convert/main_test.go index 4c6c0f58..9ed3e432 100644 --- a/hrp/pkg/convert/converter_test.go +++ b/hrp/pkg/convert/main_test.go @@ -13,31 +13,33 @@ const ( profileOverridePath = "../../../examples/data/profile_override.yml" ) +var converter *TCaseConverter + +func init() { + converter = NewConverter("", "") +} + func TestLoadTCase(t *testing.T) { - tCase, err := LoadTCase(harPath) + err := converter.loadCase(harPath, FromTypeHAR) if !assert.NoError(t, err) { t.Fatal() } - if !assert.NotEmpty(t, tCase) { + if !assert.NotEmpty(t, converter.tCase) { t.Fatal() } } func TestLoadHARWithProfileOverride(t *testing.T) { - tCase, err := LoadTCase(harPath) + err := converter.loadCase(harPath, FromTypeHAR) if !assert.NoError(t, err) { t.Fatal() } - if !assert.NotEmpty(t, tCase) { + if !assert.NotEmpty(t, converter.tCase) { t.Fatal() } - caseConverter := &TCaseConverter{ - TCase: tCase, - } - // override TCase with profile - err = caseConverter.overrideWithProfile(profileOverridePath) + err = converter.overrideWithProfile(profileOverridePath) if !assert.NoError(t, err) { t.Fatal() } @@ -45,12 +47,12 @@ func TestLoadHARWithProfileOverride(t *testing.T) { for i := 0; i < 3; i++ { if !assert.Equal(t, map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, - caseConverter.TCase.TestSteps[i].Request.Headers) { + converter.tCase.TestSteps[i].Request.Headers) { t.FailNow() } if !assert.Equal(t, map[string]string{"UserName": "debugtalk"}, - caseConverter.TCase.TestSteps[i].Request.Cookies) { + converter.tCase.TestSteps[i].Request.Cookies) { t.FailNow() } } @@ -58,7 +60,7 @@ func TestLoadHARWithProfileOverride(t *testing.T) { func TestMakeRequestWithProfile(t *testing.T) { caseConverter := &TCaseConverter{ - TCase: &hrp.TCase{ + tCase: &hrp.TCase{ TestSteps: []*hrp.TStep{ { Request: &hrp.Request{ @@ -87,19 +89,19 @@ func TestMakeRequestWithProfile(t *testing.T) { if !assert.Equal(t, map[string]string{ "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "hrp", - }, caseConverter.TCase.TestSteps[0].Request.Headers) { + }, caseConverter.tCase.TestSteps[0].Request.Headers) { t.Fatal() } if !assert.Equal(t, map[string]string{ "UserName": "debugtalk", "abc": "123", - }, caseConverter.TCase.TestSteps[0].Request.Cookies) { + }, caseConverter.tCase.TestSteps[0].Request.Cookies) { t.Fatal() } } func TestMakeRequestWithProfileOverride(t *testing.T) { caseConverter := &TCaseConverter{ - TCase: &hrp.TCase{ + tCase: &hrp.TCase{ TestSteps: []*hrp.TStep{ { Request: &hrp.Request{ @@ -129,12 +131,12 @@ func TestMakeRequestWithProfileOverride(t *testing.T) { if !assert.Equal(t, map[string]string{ "Content-Type": "application/x-www-form-urlencoded", - }, caseConverter.TCase.TestSteps[0].Request.Headers) { + }, caseConverter.tCase.TestSteps[0].Request.Headers) { t.Fatal() } if !assert.Equal(t, map[string]string{ "UserName": "debugtalk", - }, caseConverter.TCase.TestSteps[0].Request.Cookies) { + }, caseConverter.tCase.TestSteps[0].Request.Cookies) { t.Fatal() } } diff --git a/hrp/pkg/convert/to_gotest.go b/hrp/pkg/convert/to_gotest.go new file mode 100644 index 00000000..643c896a --- /dev/null +++ b/hrp/pkg/convert/to_gotest.go @@ -0,0 +1,6 @@ +package convert + +// TODO: convert TCase to gotest case +func (c *TCaseConverter) toGoTest() (string, error) { + return "", nil +} diff --git a/hrp/pkg/convert/to_json.go b/hrp/pkg/convert/to_json.go new file mode 100644 index 00000000..1fb47ed4 --- /dev/null +++ b/hrp/pkg/convert/to_json.go @@ -0,0 +1,13 @@ +package convert + +import "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + +// convert TCase to JSON case +func (c *TCaseConverter) toJSON() (string, error) { + jsonPath := c.genOutputPath(suffixJSON) + err := builtin.Dump2JSON(c.tCase, jsonPath) + if err != nil { + return "", err + } + return jsonPath, nil +} diff --git a/hrp/pkg/convert/to_pytest.go b/hrp/pkg/convert/to_pytest.go new file mode 100644 index 00000000..224974c3 --- /dev/null +++ b/hrp/pkg/convert/to_pytest.go @@ -0,0 +1,22 @@ +package convert + +import ( + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" +) + +// convert TCase to pytest case +func (c *TCaseConverter) toPyTest() (string, error) { + jsonPath, err := c.toJSON() + if err != nil { + return "", errors.Wrap(err, "convert to JSON case failed") + } + + args := append([]string{"make"}, jsonPath) + err = myexec.ExecPython3Command("httprunner", args...) + if err != nil { + return "", err + } + return c.genOutputPath(suffixPyTest), nil +} diff --git a/hrp/pkg/convert/to_yaml.go b/hrp/pkg/convert/to_yaml.go new file mode 100644 index 00000000..0ada6511 --- /dev/null +++ b/hrp/pkg/convert/to_yaml.go @@ -0,0 +1,13 @@ +package convert + +import "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + +// convert TCase to YAML case +func (c *TCaseConverter) toYAML() (string, error) { + yamlPath := c.genOutputPath(suffixYAML) + err := builtin.Dump2YAML(c.tCase, yamlPath) + if err != nil { + return "", err + } + return yamlPath, nil +}