mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-16 21:27:35 +08:00
377 lines
10 KiB
Go
377 lines
10 KiB
Go
package convert
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
|
|
"github.com/go-openapi/spec"
|
|
"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/sdk"
|
|
)
|
|
|
|
const (
|
|
suffixJSON = ".json"
|
|
suffixYAML = ".yaml"
|
|
suffixGoTest = ".go"
|
|
suffixPyTest = ".py"
|
|
)
|
|
|
|
type InputType int
|
|
|
|
const (
|
|
InputTypeUnknown InputType = iota // default input type: unknown
|
|
InputTypeHAR
|
|
InputTypePostman
|
|
InputTypeSwagger
|
|
InputTypeJMeter
|
|
InputTypeJSON
|
|
InputTypeYAML
|
|
InputTypeGoTest
|
|
InputTypePyTest
|
|
)
|
|
|
|
func (inputType InputType) String() string {
|
|
switch inputType {
|
|
case InputTypeHAR:
|
|
return "har"
|
|
case InputTypePostman:
|
|
return "postman"
|
|
case InputTypeSwagger:
|
|
return "swagger"
|
|
case InputTypeJMeter:
|
|
return "jmeter"
|
|
case InputTypeJSON:
|
|
return "json testcase"
|
|
case InputTypeYAML:
|
|
return "yaml testcase"
|
|
case InputTypeGoTest:
|
|
return "gotest script"
|
|
case InputTypePyTest:
|
|
return "pytest script"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
// TCaseConverter holds the common properties of case converter
|
|
type TCaseConverter struct {
|
|
InputPath string
|
|
OutputDir string
|
|
Profile *Profile
|
|
InputType InputType
|
|
OutputType OutputType
|
|
CaseHAR *CaseHar
|
|
CasePostman *CasePostman
|
|
CaseSwagger *spec.Swagger
|
|
TCase *hrp.TCase
|
|
}
|
|
|
|
// 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 NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
|
|
tCaseConverter = &TCaseConverter{
|
|
InputPath: path,
|
|
InputType: InputTypeUnknown,
|
|
}
|
|
extName := filepath.Ext(path)
|
|
if extName == "" {
|
|
log.Warn().Msg("extension name should be specified")
|
|
return
|
|
}
|
|
var err error
|
|
switch extName {
|
|
case ".har":
|
|
caseHAR := new(CaseHar)
|
|
err = builtin.LoadFile(path, 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.ValueOf(*tCase).IsZero() {
|
|
tCaseConverter.InputType = InputTypeJSON
|
|
tCaseConverter.TCase = tCase
|
|
break
|
|
}
|
|
casePostman := new(CasePostman)
|
|
err = builtin.LoadFile(path, 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.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.ValueOf(*tCase).IsZero() {
|
|
tCaseConverter.InputType = InputTypeYAML
|
|
tCaseConverter.TCase = tCase
|
|
break
|
|
}
|
|
caseSwagger := new(spec.Swagger)
|
|
err = builtin.LoadFile(path, caseSwagger)
|
|
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
|
|
tCaseConverter.InputType = InputTypeSwagger
|
|
tCaseConverter.CaseSwagger = caseSwagger
|
|
}
|
|
case ".go": // TODO
|
|
tCaseConverter.InputType = InputTypeGoTest
|
|
case ".py": // TODO
|
|
tCaseConverter.InputType = InputTypePyTest
|
|
case ".jmx": // TODO
|
|
tCaseConverter.InputType = InputTypeJMeter
|
|
default:
|
|
log.Warn().
|
|
Str("input path", tCaseConverter.InputPath).
|
|
Msgf("unsupported file type: %v", extName)
|
|
}
|
|
if tCaseConverter.InputType != InputTypeUnknown {
|
|
log.Info().
|
|
Str("input path", tCaseConverter.InputPath).
|
|
Msgf("load case as: %s", tCaseConverter.InputType.String())
|
|
} else {
|
|
log.Error().Err(err).
|
|
Str("input path", tCaseConverter.InputPath).
|
|
Msgf("failed to load case")
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *TCaseConverter) SetProfile(path string) {
|
|
log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
|
|
profile := new(Profile)
|
|
err := builtin.LoadFile(path, profile)
|
|
if err != nil {
|
|
log.Warn().Str("path", path).
|
|
Msg("failed to load profile, ignore!")
|
|
return
|
|
}
|
|
c.Profile = profile
|
|
}
|
|
|
|
func (c *TCaseConverter) SetOutputDir(dir string) {
|
|
log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
|
|
c.OutputDir = dir
|
|
}
|
|
|
|
func (c *TCaseConverter) genOutputPath(suffix string) string {
|
|
outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
|
|
if c.OutputDir != "" {
|
|
return filepath.Join(c.OutputDir, outFileFullName)
|
|
} else {
|
|
return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
|
|
}
|
|
// TODO avoid outFileFullName conflict?
|
|
}
|
|
|
|
func (c *TCaseConverter) ToPyTest() (string, error) {
|
|
script := convertConfig(c.TCase.Config)
|
|
println(script)
|
|
return script, nil
|
|
}
|
|
|
|
func convertConfig(config *hrp.TConfig) string {
|
|
script := fmt.Sprintf("Config('%s')", config.Name)
|
|
|
|
if config.Variables != nil {
|
|
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
|
|
}
|
|
if config.BaseURL != "" {
|
|
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
|
|
}
|
|
if config.Export != nil {
|
|
script += fmt.Sprintf(".export(*%v)", config.Export)
|
|
}
|
|
script += fmt.Sprintf(".verify(%v)", config.Verify)
|
|
|
|
return script
|
|
}
|
|
|
|
func (c *TCaseConverter) ToGoTest() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
|
|
type ICaseConverter interface {
|
|
Struct() *TCaseConverter
|
|
ToJSON() (string, error)
|
|
ToYAML() (string, error)
|
|
ToGoTest() (string, error)
|
|
ToPyTest() (string, error)
|
|
}
|
|
|
|
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)
|
|
tCaseConverter.OutputType = outputType
|
|
if outputDir != "" {
|
|
tCaseConverter.SetOutputDir(outputDir)
|
|
}
|
|
if profilePath != "" {
|
|
tCaseConverter.SetProfile(profilePath)
|
|
}
|
|
switch tCaseConverter.InputType {
|
|
case InputTypeHAR:
|
|
iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
|
|
case InputTypePostman:
|
|
iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
|
|
case InputTypeJSON:
|
|
iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
|
|
case InputTypeYAML:
|
|
iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
|
|
case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
|
|
log.Warn().
|
|
Str("input path", tCaseConverter.InputPath).
|
|
Msg("case type not supported yet, ignore!")
|
|
default:
|
|
log.Warn().
|
|
Str("input path", tCaseConverter.InputPath).
|
|
Msg("unknown case type, ignore!")
|
|
}
|
|
}
|
|
|
|
// start converting
|
|
var outputFiles []string
|
|
var err error
|
|
for _, iCaseConverter := range iCaseConverters {
|
|
log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
|
|
var outputFile string
|
|
switch iCaseConverter.Struct().OutputType {
|
|
case OutputTypeYAML:
|
|
outputFile, err = iCaseConverter.ToYAML()
|
|
case OutputTypeGoTest:
|
|
outputFile, err = iCaseConverter.ToGoTest()
|
|
case OutputTypePyTest:
|
|
outputFile, err = iCaseConverter.ToPyTest()
|
|
default:
|
|
outputFile, err = iCaseConverter.ToJSON()
|
|
}
|
|
if err != nil {
|
|
log.Error().Err(err).
|
|
Str("input path", iCaseConverter.Struct().InputPath).
|
|
Msg("error occurs during converting")
|
|
continue
|
|
}
|
|
outputFiles = append(outputFiles, outputFile)
|
|
}
|
|
log.Info().Strs("output files", outputFiles).Msg("conversion completed")
|
|
}
|
|
|
|
func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
|
|
tCase := iCaseConverter.Struct().TCase
|
|
if tCase == nil {
|
|
return nil, errors.Errorf("empty json/yaml testcase occurs")
|
|
}
|
|
profile := iCaseConverter.Struct().Profile
|
|
if profile == nil {
|
|
return tCase, nil
|
|
}
|
|
for _, step := range 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 tCase, nil
|
|
}
|
|
|
|
func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
|
|
// convert to temporary json testcase
|
|
jsonPath, err := iCaseConverter.ToJSON()
|
|
inputType := iCaseConverter.Struct().InputType
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
|
|
}
|
|
defer func() {
|
|
if jsonPath != "" {
|
|
if err = os.Remove(jsonPath); err != nil {
|
|
log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
|
|
}
|
|
}
|
|
}()
|
|
|
|
// convert from temporary json testcase to pytest
|
|
converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
|
|
pyTestPath, err := converterJSON.MakePyTestScript()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
|
|
}
|
|
|
|
// rename resultant pytest
|
|
renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
|
|
err = os.Rename(pyTestPath, renamedPyTestPath)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
|
|
return pyTestPath, nil
|
|
}
|
|
return renamedPyTestPath, nil
|
|
}
|