package hrp import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "reflect" "strings" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" ) func loadFromJSON(path string) (*TCase, error) { path, err := filepath.Abs(path) if err != nil { log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") return nil, err } log.Info().Str("path", path).Msg("load json testcase") file, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("load json path failed") return nil, err } tc := &TCase{} decoder := json.NewDecoder(bytes.NewReader(file)) decoder.UseNumber() err = decoder.Decode(tc) if err != nil { return tc, err } err = convertCompatTestCase(tc) return tc, err } func loadFromYAML(path string) (*TCase, error) { path, err := filepath.Abs(path) if err != nil { log.Error().Str("path", path).Err(err).Msg("convert absolute path failed") return nil, err } log.Info().Str("path", path).Msg("load yaml testcase") file, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("load yaml path failed") return nil, err } tc := &TCase{} err = yaml.Unmarshal(file, tc) if err != nil { return tc, nil } err = convertCompatTestCase(tc) return tc, err } func convertCompatTestCase(tc *TCase) (err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("convert compat testcase error: %v", p) } }() for _, step := range tc.TestSteps { // 1. deal with body compatible with HttpRunner if step.Request.Body == nil { if step.Request.Json != nil { // Content-Type is "application/json; charset=UTF-8" step.Request.Body = step.Request.Json } else if step.Request.Data != nil { dataValue := reflect.ValueOf(step.Request.Data) switch dataValue.Kind() { case reflect.String: // Content-Type is "text/plain" step.Request.Body = dataValue.String() case reflect.Map: // Content-Type is "application/x-www-form-urlencoded" var paramsList []string mapRange := dataValue.MapRange() for mapRange.Next() { paramsList = append(paramsList, fmt.Sprintf("%s=%s", mapRange.Key(), mapRange.Value())) } step.Request.Body = strings.Join(paramsList, "&") default: log.Error().Msgf("[convert compat testcase] unexpected body type: %v", dataValue.Kind()) } } } // 2. deal with validators compatible with HttpRunner for _, iValidator := range step.ValidatorsCompat { validatorMap, ok := iValidator.(map[string]interface{}) if !ok || len(validatorMap) == 0 { // pass invalid or empty validator continue } // check priority: HRP > HttpRunner validator := Validator{} if len(validatorMap) == 4 { // HRP validator format validator.Check = validatorMap["check"].(string) validator.Assert = validatorMap["assert"].(string) validator.Expect = validatorMap["expect"] if msg, exist := validatorMap["msg"]; exist { validator.Message = msg.(string) } } else if len(validatorMap) == 1 { // HttpRunner validator format validatorValue := reflect.ValueOf(validatorMap) assertMethod := validatorValue.MapKeys()[0] iValidatorContent := validatorValue.MapIndex(assertMethod).Interface().([]interface{}) validator.Check = iValidatorContent[0].(string) validator.Assert = assertMethod.String() validator.Expect = iValidatorContent[1] } else { log.Error().Msgf("[convert compat testcase] unexpected validator format: %v", validatorMap) } // deal with headers format in HttpRunner // e.g. headers.Content-Type => headers.\"Content-Type\" if strings.Contains(validator.Check, "headers.") && !strings.Contains(validator.Check, "\"") && strings.Contains(validator.Check, "-") { replacedHeader := fmt.Sprintf("headers.\"%s\"", validator.Check[len("headers."):]) validator.Check = replacedHeader } step.Validators = append(step.Validators, validator) } } return err } func (tc *TCase) ToTestCase() (*TestCase, error) { testCase := &TestCase{ Config: tc.Config, } for _, step := range tc.TestSteps { if step.Request != nil { testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{ step: step, }) } else if step.TestCase != nil { testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{ step: step, }) } else if step.Transaction != nil { testCase.TestSteps = append(testCase.TestSteps, &StepTransaction{ step: step, }) } else if step.Rendezvous != nil { testCase.TestSteps = append(testCase.TestSteps, &StepRendezvous{ step: step, }) } else { log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step") } } return testCase, nil } var ErrUnsupportedFileExt = fmt.Errorf("unsupported testcase file extension") // TestCasePath implements ITestCase interface. type TestCasePath struct { Path string } func (path *TestCasePath) ToTestCase() (*TestCase, error) { var tc *TCase var err error casePath := path.Path ext := filepath.Ext(casePath) switch ext { case ".json": tc, err = loadFromJSON(casePath) case ".yaml", ".yml": tc, err = loadFromYAML(casePath) default: err = ErrUnsupportedFileExt } if err != nil { return nil, err } tc.Config.Path = path.Path testcase, err := tc.ToTestCase() if err != nil { return nil, err } return testcase, nil } func (path *TestCasePath) ToTCase() (*TCase, error) { testcase, err := path.ToTestCase() if err != nil { return nil, err } return testcase.ToTCase() }