Files
httprunner/hrp/internal/convert/converter.go
2022-05-26 16:04:46 +08:00

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
}