diff --git a/boomer.go b/boomer.go index 292178e6..c9baf9dd 100644 --- a/boomer.go +++ b/boomer.go @@ -122,7 +122,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { // step failed var elapsed int64 if stepData != nil { - elapsed = stepData.elapsed + elapsed = stepData.Elapsed } b.RecordFailure(step.Type(), step.Name(), elapsed, err.Error()) @@ -139,14 +139,14 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { } // step success - if stepData.stepType == stepTypeTransaction { + if stepData.StepType == stepTypeTransaction { // transaction // FIXME: support nested transactions if step.ToStruct().Transaction.Type == transactionEnd { // only record when transaction ends - b.RecordTransaction(stepData.name, transactionSuccess, stepData.elapsed, 0) + b.RecordTransaction(stepData.Name, transactionSuccess, stepData.Elapsed, 0) transactionSuccess = true // reset flag for next transaction } - } else if stepData.stepType == stepTypeRendezvous { + } else if stepData.StepType == stepTypeRendezvous { // rendezvous // TODO: implement rendezvous in boomer rendezvous := step.ToStruct().Rendezvous @@ -155,7 +155,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task { } } else { // request or testcase step - b.RecordSuccess(step.Type(), step.Name(), stepData.elapsed, stepData.contentSize) + b.RecordSuccess(step.Type(), step.Name(), stepData.Elapsed, stepData.ContentSize) } } endTime := time.Now() diff --git a/cli/hrp/cmd/run.go b/cli/hrp/cmd/run.go index 23d3de9f..54e32fd9 100644 --- a/cli/hrp/cmd/run.go +++ b/cli/hrp/cmd/run.go @@ -27,7 +27,8 @@ var runCmd = &cobra.Command{ } runner := hrp.NewRunner(nil). SetDebug(!silentFlag). - SetFailfast(!continueOnFailure) + SetFailfast(!continueOnFailure). + SetSaveTests(saveTests) if proxyUrl != "" { runner.SetProxyUrl(proxyUrl) } @@ -42,6 +43,7 @@ var ( continueOnFailure bool silentFlag bool proxyUrl string + saveTests bool ) func init() { @@ -49,5 +51,6 @@ func init() { runCmd.Flags().BoolVar(&continueOnFailure, "continue-on-failure", false, "continue running next step when failure occurs") runCmd.Flags().BoolVarP(&silentFlag, "silent", "s", false, "disable logging request & response details") runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url") + runCmd.Flags().BoolVar(&saveTests, "save-tests", false, "save tests summary") // runCmd.Flags().BoolP("gen-html-report", "r", false, "Generate HTML report") } diff --git a/convert.go b/convert.go index 593ecfdc..485a3dee 100644 --- a/convert.go +++ b/convert.go @@ -11,49 +11,6 @@ import ( "gopkg.in/yaml.v3" ) -func (tc *TCase) Dump2JSON(path string) error { - path, err := filepath.Abs(path) - if err != nil { - log.Error().Err(err).Msg("convert absolute path failed") - return err - } - log.Info().Str("path", path).Msg("dump testcase to json") - file, _ := json.MarshalIndent(tc, "", " ") - err = ioutil.WriteFile(path, file, 0644) - if err != nil { - log.Error().Err(err).Msg("dump json path failed") - return err - } - return nil -} - -func (tc *TCase) Dump2YAML(path string) error { - path, err := filepath.Abs(path) - if err != nil { - log.Error().Err(err).Msg("convert absolute path failed") - return err - } - log.Info().Str("path", path).Msg("dump testcase to yaml") - - // init yaml encoder - buffer := new(bytes.Buffer) - encoder := yaml.NewEncoder(buffer) - encoder.SetIndent(4) - - // encode - err = encoder.Encode(tc) - if err != nil { - return err - } - - err = ioutil.WriteFile(path, buffer.Bytes(), 0644) - if err != nil { - log.Error().Err(err).Msg("dump yaml path failed") - return err - } - return nil -} - func loadFromJSON(path string) (*TCase, error) { path, err := filepath.Abs(path) if err != nil { diff --git a/examples/rendezvous_test.go b/examples/rendezvous_test.go index c9a1c365..1d004a2e 100644 --- a/examples/rendezvous_test.go +++ b/examples/rendezvous_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/httprunner/hrp" + "github.com/httprunner/hrp/internal/builtin" ) var rendezvousTestcase = &hrp.TestCase{ @@ -58,7 +59,7 @@ func TestRendezvousDump2JSON(t *testing.T) { if err != nil { t.Fatalf("ToTCase error: %v", err) } - err = tCase.Dump2JSON("rendezvous_test.json") + err = builtin.Dump2JSON(tCase, "rendezvous_test.json") if err != nil { t.Fatalf("dump to json error: %v", err) } diff --git a/internal/builtin/assertion.go b/internal/builtin/assertion.go index d59f6366..6acb06ed 100644 --- a/internal/builtin/assertion.go +++ b/internal/builtin/assertion.go @@ -16,7 +16,7 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual "greater_or_equals": assert.GreaterOrEqual, "less_or_equals": assert.LessOrEqual, "not_equal": assert.NotEqual, - "contained_by": assert.Contains, + "contained_by": assert.Contains, "regex_match": assert.Regexp, "type_match": assert.IsType, // custom assertions @@ -28,7 +28,7 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual "length_less_or_equals": LessOrEqualsLength, "length_greater_than": GreaterThanLength, "length_greater_or_equals": GreaterOrEqualsLength, - "contains": Contains, + "contains": Contains, "string_equals": EqualString, } diff --git a/internal/builtin/function.go b/internal/builtin/function.go index 5554eaf6..ca61adc1 100644 --- a/internal/builtin/function.go +++ b/internal/builtin/function.go @@ -1,9 +1,11 @@ package builtin import ( + "bytes" "crypto/md5" "encoding/csv" "encoding/hex" + "encoding/json" "io/ioutil" "math" "math/rand" @@ -11,6 +13,8 @@ import ( "strings" "time" + "gopkg.in/yaml.v3" + "github.com/rs/zerolog/log" ) @@ -19,7 +23,7 @@ var Functions = map[string]interface{}{ "sleep": sleep, // call with one argument "gen_random_string": genRandomString, // call with one argument "max": math.Max, // call with two arguments - "md5": MD5, // call with one argument + "md5": MD5, // call with one argument "parameterize": loadFromCSV, "P": loadFromCSV, } @@ -82,3 +86,46 @@ func loadFromCSV(path string) []map[string]interface{} { } return result } + +func Dump2JSON(data interface{}, path string) error { + path, err := filepath.Abs(path) + if err != nil { + log.Error().Err(err).Msg("convert absolute path failed") + return err + } + log.Info().Str("path", path).Msg("dump data to json") + file, _ := json.MarshalIndent(data, "", " ") + err = ioutil.WriteFile(path, file, 0644) + if err != nil { + log.Error().Err(err).Msg("dump json path failed") + return err + } + return nil +} + +func Dump2YAML(data interface{}, path string) error { + path, err := filepath.Abs(path) + if err != nil { + log.Error().Err(err).Msg("convert absolute path failed") + return err + } + log.Info().Str("path", path).Msg("dump data to yaml") + + // init yaml encoder + buffer := new(bytes.Buffer) + encoder := yaml.NewEncoder(buffer) + encoder.SetIndent(4) + + // encode + err = encoder.Encode(data) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, buffer.Bytes(), 0644) + if err != nil { + log.Error().Err(err).Msg("dump yaml path failed") + return err + } + return nil +} diff --git a/internal/har2case/core.go b/internal/har2case/core.go index b2f33f10..9683ddb4 100644 --- a/internal/har2case/core.go +++ b/internal/har2case/core.go @@ -15,6 +15,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/hrp" + "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" ) @@ -55,7 +56,7 @@ func (h *har) GenJSON() (jsonPath string, err error) { return "", err } jsonPath = h.genOutputPath(suffixJSON) - err = tCase.Dump2JSON(jsonPath) + err = builtin.Dump2JSON(tCase, jsonPath) return } @@ -74,7 +75,7 @@ func (h *har) GenYAML() (yamlPath string, err error) { return "", err } yamlPath = h.genOutputPath(suffixYAML) - err = tCase.Dump2YAML(yamlPath) + err = builtin.Dump2YAML(tCase, yamlPath) return } diff --git a/internal/scaffold/demo_test.go b/internal/scaffold/demo_test.go index 9e5dd5e9..eb9c101f 100644 --- a/internal/scaffold/demo_test.go +++ b/internal/scaffold/demo_test.go @@ -8,6 +8,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/hrp" + "github.com/httprunner/hrp/internal/builtin" ) var ( @@ -32,11 +33,11 @@ func removeHashicorpPlugin() { func TestGenDemoTestCase(t *testing.T) { tCase, _ := demoTestCase.ToTCase() - err := tCase.Dump2JSON(demoTestCaseJSONPath) + err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath) if err != nil { t.Fail() } - err = tCase.Dump2YAML(demoTestCaseYAMLPath) + err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath) if err != nil { t.Fail() } diff --git a/internal/scaffold/main.go b/internal/scaffold/main.go index 5db35179..04fd84d8 100644 --- a/internal/scaffold/main.go +++ b/internal/scaffold/main.go @@ -8,6 +8,7 @@ import ( "path" "strings" + "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" "github.com/rs/zerolog/log" ) @@ -48,12 +49,12 @@ func CreateScaffold(projectName string) error { // create demo testcases tCase, _ := demoTestCase.ToTCase() - err := tCase.Dump2JSON(path.Join(projectName, "testcases", "demo.json")) + err := builtin.Dump2JSON(tCase, path.Join(projectName, "testcases", "demo.json")) if err != nil { log.Error().Err(err).Msg("create demo.json testcase failed") return err } - err = tCase.Dump2YAML(path.Join(projectName, "testcases", "demo.yaml")) + err = builtin.Dump2YAML(tCase, path.Join(projectName, "testcases", "demo.yaml")) if err != nil { log.Error().Err(err).Msg("create demo.yml testcase failed") return err diff --git a/models.go b/models.go index c4b3f249..a6d0a495 100644 --- a/models.go +++ b/models.go @@ -1,9 +1,13 @@ package hrp import ( + "fmt" "math/rand" + "runtime" "sync" "time" + + "github.com/httprunner/hrp/internal/version" ) const ( @@ -209,13 +213,132 @@ func (tc *TestCase) ToTCase() (*TCase, error) { return &tCase, nil } -type testCaseSummary struct{} +type testCaseStat struct { + Total int `json:"total" yaml:"total"` + Success int `json:"success" yaml:"success"` + Fail int `json:"fail" yaml:"fail"` +} + +type testStepStat struct { + Total int `json:"total" yaml:"total"` + Successes int `json:"successes" yaml:"successes"` + Failures int `json:"failures" yaml:"failures"` +} + +type stat struct { + TestCases testCaseStat `json:"testcases" yaml:"test_cases"` + TestSteps testStepStat `json:"teststeps" yaml:"test_steps"` +} + +type testCaseTime struct { + StartAt time.Time `json:"start_at,omitempty" yaml:"start_at,omitempty"` + Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` +} + +type platform struct { + HttprunnerVersion string `json:"httprunner_version" yaml:"httprunner_version"` + GoVersion string `json:"go_version" yaml:"go_version"` + Platform string `json:"platform" yaml:"platform"` +} + +// summary stores tests summary for current task execution, maybe include one or multiple testcases +type summary struct { + Success bool `json:"success" yaml:"success"` + Stat *stat `json:"stat" yaml:"stat"` + Time *testCaseTime `json:"time" yaml:"time"` + Platform *platform `json:"platform" yaml:"platform"` + Details []*testCaseSummary `json:"details" yaml:"details"` +} + +func newOutSummary() *summary { + platForm := &platform{ + HttprunnerVersion: version.VERSION, + GoVersion: runtime.Version(), + Platform: fmt.Sprintf("%v-%v", runtime.GOOS, runtime.GOARCH), + } + return &summary{ + Success: true, + Stat: &stat{}, + Time: &testCaseTime{ + StartAt: time.Now(), + }, + Platform: platForm, + } +} + +func (s *summary) appendCaseSummary(caseSummary *testCaseSummary) { + s.Success = s.Success && caseSummary.Success + s.Stat.TestCases.Total += 1 + s.Stat.TestSteps.Total += len(caseSummary.Records) + if caseSummary.Success { + s.Stat.TestCases.Success += 1 + s.Stat.TestSteps.Successes += len(caseSummary.Records) + } else { + s.Stat.TestCases.Fail += 1 + s.Stat.TestSteps.Successes += len(caseSummary.Records) - 1 + s.Stat.TestSteps.Failures += 1 + } + s.Details = append(s.Details, caseSummary) + s.Success = s.Success && caseSummary.Success +} type stepData struct { - name string // step name - stepType stepType // step type, testcase/request/transaction/rendezvous - success bool // step execution result - elapsed int64 // step execution time in millisecond(ms) - contentSize int64 // response body length - exportVars map[string]interface{} // extract variables + Name string `json:"name" yaml:"name"` // step name + StepType stepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous + Success bool `json:"success" yaml:"success"` // step execution result + Elapsed int64 `json:"elapsed_ms" yaml:"elapsed_ms"` // step execution time in millisecond(ms) + Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data + ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length + ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables +} + +type testCaseInOut struct { + ConfigVars map[string]interface{} `json:"config_vars" yaml:"config_vars"` + ExportVars map[string]interface{} `json:"export_vars" yaml:"export_vars"` +} + +// testCaseSummary stores tests summary for one testcase +type testCaseSummary struct { + Name string `json:"name" yaml:"name"` + Success bool `json:"success" yaml:"success"` + CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` //TODO + Time *testCaseTime `json:"time" yaml:"time"` + InOut *testCaseInOut `json:"in_out" yaml:"in_out"` + Log string `json:"log,omitempty" yaml:"log,omitempty"` //TODO + Records []*stepData `json:"records" yaml:"records"` +} + +type validationResult struct { + Validator + CheckValue interface{} `json:"check_value" yaml:"check_value"` + CheckResult string `json:"check_result" yaml:"check_result"` +} + +type reqResps struct { + Request *Request `json:"request" yaml:"request"` + Response interface{} `json:"response" yaml:"response"` +} + +type address struct { + ClientIP string `json:"client_ip,omitempty" yaml:"client_ip,omitempty"` + ClientPort string `json:"client_port,omitempty" yaml:"client_port,omitempty"` + ServerIP string `json:"server_ip,omitempty" yaml:"server_ip,omitempty"` + ServerPort string `json:"server_port,omitempty" yaml:"server_port,omitempty"` +} + +type SessionData struct { + Success bool `json:"success" yaml:"success"` + ReqResps *reqResps `json:"req_resps" yaml:"req_resps"` + Address *address `json:"address,omitempty" yaml:"address,omitempty"` //TODO + Validators []*validationResult `json:"validators,omitempty" yaml:"validators,omitempty"` +} + +func newSessionData() *SessionData { + reqResps := &reqResps{ + Request: &Request{}, + } + return &SessionData{ + Success: false, + ReqResps: reqResps, + } } diff --git a/response.go b/response.go index 77abb4dc..94a27d5b 100644 --- a/response.go +++ b/response.go @@ -79,7 +79,7 @@ type responseObject struct { t *testing.T parser *parser respObjMeta interface{} - validationResults map[string]interface{} + validationResults []*validationResult } func (v *responseObject) Extract(extractors map[string]string) map[string]interface{} { @@ -122,9 +122,23 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s if err != nil { return err } + validResult := &validationResult{ + Validator: Validator{ + Check: validator.Check, + Expect: expectValue, + Assert: assertMethod, + Message: validator.Message, + }, + CheckValue: checkValue, + CheckResult: "fail", + } // do assertion result := assertFunc(v.t, expectValue, checkValue) + if result { + validResult.CheckResult = "pass" + } + v.validationResults = append(v.validationResults, validResult) log.Info(). Str("assertMethod", assertMethod). Interface("expectValue", expectValue). diff --git a/runner.go b/runner.go index dc05483c..b6f42789 100644 --- a/runner.go +++ b/runner.go @@ -23,10 +23,15 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/plugin/common" ) +const ( + summaryPath string = "summary.json" +) + // Run starts to run API test with default configs. func Run(testcases ...ITestCase) error { t := &testing.T{} @@ -52,10 +57,11 @@ func NewRunner(t *testing.T) *HRPRunner { } type HRPRunner struct { - t *testing.T - failfast bool - debug bool - client *http.Client + t *testing.T + failfast bool + debug bool + saveTests bool + client *http.Client } // SetFailfast configures whether to stop running when one step fails. @@ -87,6 +93,13 @@ func (r *HRPRunner) SetProxyUrl(proxyUrl string) *HRPRunner { return r } +// SetSaveTests configures whether to save summary of tests. +func (r *HRPRunner) SetSaveTests(saveTests bool) *HRPRunner { + log.Info().Bool("saveTests", saveTests).Msg("[init] SetSaveTests") + r.saveTests = saveTests + return r +} + // Run starts to execute one or multiple testcases. func (r *HRPRunner) Run(testcases ...ITestCase) error { event := ga.EventTracking{ @@ -111,6 +124,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed") return err } + s := newOutSummary() // 在runner模式下,指定整体策略,cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器 for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); { // iterate through all parameter iterators and update case variables @@ -119,10 +133,18 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { cfg.Variables = mergeVariables(it.Next(), cfg.Variables) } } - if err := r.newCaseRunner(testcase).run(); err != nil { + caseRunnerObj := r.newCaseRunner(testcase) + if err := caseRunnerObj.run(); err != nil { log.Error().Err(err).Msg("[Run] run testcase failed") return err } + caseSummary := caseRunnerObj.getSummary() + s.appendCaseSummary(caseSummary) + } + s.Time.Duration = time.Since(s.Time.StartAt).Seconds() + if r.saveTests { + err = builtin.Dump2JSON(s, summaryPath) + return err } } return nil @@ -133,6 +155,7 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) *caseRunner { TestCase: testcase, hrpRunner: r, parser: newParser(), + summary: newSummary(), } caseRunner.reset() return caseRunner @@ -148,7 +171,8 @@ type caseRunner struct { // transactions stores transaction timing info. // key is transaction name, value is map of transaction type and time, e.g. start time and end time. transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase + startTime time.Time // record start time of the testcase + summary *testCaseSummary // record test case summary } // reset clears runner session variables. @@ -157,6 +181,7 @@ func (r *caseRunner) reset() *caseRunner { r.sessionVariables = make(map[string]interface{}) r.transactions = make(map[string]map[transactionType]time.Time) r.startTime = time.Now() + r.summary.Name = r.Config.Name return r } @@ -179,12 +204,16 @@ func (r *caseRunner) run() error { r.startTime = time.Now() for index := range r.TestCase.TestSteps { - _, err := r.runStep(index, config) + stepData, err := r.runStep(index, config) if err != nil { if r.hrpRunner.failfast { return errors.Wrap(err, "abort running due to failfast setting") } } + if stepData != nil { + r.summary.Records = append(r.summary.Records, stepData) + r.summary.Success = r.summary.Success && stepData.Success + } } log.Info().Str("testcase", config.Name).Msg("run testcase end") @@ -269,29 +298,34 @@ func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepDa stepResult, err = r.runStepTestCase(copiedStep) if err != nil { log.Error().Err(err).Msg("run referenced testcase step failed") - return } } else { + // parse step request url + var requestUrl interface{} + requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables) + if err != nil { + log.Error().Err(err).Msg("parse request url failed") + requestUrl = copiedStep.Variables + } + copiedStep.Request.URL = buildURL(caseConfig.BaseURL, convertString(requestUrl)) // avoid data racing // run request - copiedStep.Request.URL = buildURL(caseConfig.BaseURL, copiedStep.Request.URL) // avoid data racing stepResult, err = r.runStepRequest(copiedStep) if err != nil { log.Error().Err(err).Msg("run request step failed") - return } } // update extracted variables - for k, v := range stepResult.exportVars { + for k, v := range stepResult.ExportVars { r.sessionVariables[k] = v } log.Info(). Str("step", step.Name()). - Bool("success", stepResult.success). - Interface("exportVars", stepResult.exportVars). + Bool("success", stepResult.Success). + Interface("exportVars", stepResult.ExportVars). Msg("run step end") - return stepResult, nil + return stepResult, err } func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) { @@ -301,11 +335,11 @@ func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *s Msg("transaction") stepResult = &stepData{ - name: transaction.Name, - stepType: stepTypeTransaction, - success: true, - elapsed: 0, - contentSize: 0, // TODO: record transaction total response length + Name: transaction.Name, + StepType: stepTypeTransaction, + Success: true, + Elapsed: 0, + ContentSize: 0, // TODO: record transaction total response length } // create transaction if not exists @@ -329,7 +363,7 @@ func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *s // calculate transaction duration duration := r.transactions[transaction.Name][transactionEnd].Sub( r.transactions[transaction.Name][transactionStart]) - stepResult.elapsed = duration.Milliseconds() + stepResult.Elapsed = duration.Milliseconds() log.Info().Str("name", transaction.Name).Dur("elapsed", duration).Msg("transaction") } @@ -344,9 +378,9 @@ func (r *caseRunner) runStepRendezvous(rendezvous *Rendezvous) (stepResult *step Int64("timeout", rendezvous.Timeout). Msg("rendezvous") stepResult = &stepData{ - name: rendezvous.Name, - stepType: stepTypeRendezvous, - success: true, + Name: rendezvous.Name, + StepType: stepTypeRendezvous, + Success: true, } // pass current rendezvous if already released, activate rendezvous sequentially after spawn done @@ -518,12 +552,16 @@ func waitSingleRendezvous(rendezvous *Rendezvous, rendezvousList []*Rendezvous, func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err error) { stepResult = &stepData{ - name: step.Name, - stepType: stepTypeRequest, - success: false, - contentSize: 0, + Name: step.Name, + StepType: stepTypeRequest, + Success: false, + ContentSize: 0, + } + sessionData := newSessionData() + if err = copier.Copy(&sessionData.ReqResps.Request, step.Request); err != nil { + log.Error().Err(err).Msg("copy step request data failed") + return } - rawUrl := step.Request.URL method := step.Request.Method req := &http.Request{ @@ -538,7 +576,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro if len(step.Request.Headers) > 0 { headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables) if err != nil { - return nil, errors.Wrap(err, "parse headers failed") + return stepResult, errors.Wrap(err, "parse headers failed") } for key, value := range headers { req.Header.Add(key, value) @@ -555,9 +593,10 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro if len(step.Request.Params) > 0 { params, err := r.parser.parseData(step.Request.Params, step.Variables) if err != nil { - return nil, errors.Wrap(err, "parse data failed") + return stepResult, errors.Wrap(err, "parse data failed") } parsedParams := params.(map[string]interface{}) + sessionData.ReqResps.Request.Params = parsedParams if len(parsedParams) > 0 { queryParams = make(url.Values) for k, v := range parsedParams { @@ -587,8 +626,9 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro if step.Request.Body != nil { data, err := r.parser.parseData(step.Request.Body, step.Variables) if err != nil { - return nil, err + return stepResult, err } + sessionData.ReqResps.Request.Body = data var dataBytes []byte switch vv := data.(type) { case map[string]interface{}: @@ -604,7 +644,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro // post json dataBytes, err = json.Marshal(vv) if err != nil { - return nil, err + return stepResult, err } req.Header.Set("Content-Type", "application/json; charset=UTF-8") } @@ -615,15 +655,20 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro case bytes.Buffer: dataBytes = vv.Bytes() default: // unexpected body type - return nil, errors.New("unexpected request body type") + return stepResult, errors.New("unexpected request body type") } setBodyBytes(req, dataBytes) } + // update header + sessionData.ReqResps.Request.Headers = make(map[string]string) + for key, value := range req.Header { + sessionData.ReqResps.Request.Headers[key] = value[0] + } // prepare url u, err := url.Parse(rawUrl) if err != nil { - return nil, errors.Wrap(err, "parse url failed") + return stepResult, errors.Wrap(err, "parse url failed") } req.URL = u req.Host = u.Host @@ -632,7 +677,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro if r.hrpRunner.debug { reqDump, err := httputil.DumpRequest(req, true) if err != nil { - return nil, errors.Wrap(err, "dump request failed") + return stepResult, errors.Wrap(err, "dump request failed") } fmt.Println("-------------------- request --------------------") fmt.Println(string(reqDump)) @@ -641,9 +686,9 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro // do request action start := time.Now() resp, err := r.hrpRunner.client.Do(req) - stepResult.elapsed = time.Since(start).Milliseconds() + stepResult.Elapsed = time.Since(start).Milliseconds() if err != nil { - return nil, errors.Wrap(err, "do request failed") + return stepResult, errors.Wrap(err, "do request failed") } defer resp.Body.Close() @@ -652,7 +697,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro fmt.Println("==================== response ===================") respDump, err := httputil.DumpResponse(resp, true) if err != nil { - return nil, errors.Wrap(err, "dump response failed") + return stepResult, errors.Wrap(err, "dump response failed") } fmt.Println(string(respDump)) fmt.Println("--------------------------------------------------") @@ -664,31 +709,33 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro err = errors.Wrap(err, "init ResponseObject error") return } + sessionData.ReqResps.Response = respObj.respObjMeta // extract variables from response extractors := step.Extract extractMapping := respObj.Extract(extractors) - stepResult.exportVars = extractMapping + stepResult.ExportVars = extractMapping // override step variables with extracted variables stepVariables := mergeVariables(step.Variables, extractMapping) // validate response err = respObj.Validate(step.Validators, stepVariables) - if err != nil { - return + sessionData.Validators = respObj.validationResults + if err == nil { + sessionData.Success = true + stepResult.Success = true } - - stepResult.success = true - stepResult.contentSize = resp.ContentLength - return stepResult, nil + stepResult.ContentSize = resp.ContentLength + stepResult.Data = sessionData + return stepResult, err } func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err error) { stepResult = &stepData{ - name: step.Name, - stepType: stepTypeTestCase, - success: false, + Name: step.Name, + StepType: stepTypeTestCase, + Success: false, } testcase := step.TestCase @@ -696,16 +743,18 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err copiedTestCase := &TestCase{} if err = copier.Copy(copiedTestCase, testcase); err != nil { log.Error().Err(err).Msg("copy testcase failed") - return nil, err + return stepResult, err } start := time.Now() - err = r.hrpRunner.newCaseRunner(copiedTestCase).run() - stepResult.elapsed = time.Since(start).Milliseconds() + caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase) + err = caseRunnerObj.run() + stepResult.Elapsed = time.Since(start).Milliseconds() if err != nil { return stepResult, err } - stepResult.success = true + stepResult.Data = caseRunnerObj.getSummary() + stepResult.Success = true return stepResult, nil } @@ -735,8 +784,25 @@ func (r *caseRunner) parseConfig(cfg *TConfig) error { return nil } +func newSummary() *testCaseSummary { + return &testCaseSummary{ + Success: true, + Time: &testCaseTime{}, + InOut: &testCaseInOut{}, + } +} + func (r *caseRunner) getSummary() *testCaseSummary { - return &testCaseSummary{} + caseSummary := r.summary + caseSummary.Time.StartAt = r.startTime + caseSummary.Time.Duration = time.Since(r.startTime).Seconds() + exportVars := make(map[string]interface{}) + for _, value := range r.Config.Export { + exportVars[value] = r.Config.Variables[value] + } + caseSummary.InOut.ExportVars = exportVars + caseSummary.InOut.ConfigVars = r.Config.Variables + return caseSummary } func setBodyBytes(req *http.Request, data []byte) { diff --git a/validate.go b/validate.go index 8e2e857b..51150563 100644 --- a/validate.go +++ b/validate.go @@ -105,7 +105,7 @@ func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interf v := Validator{ Check: jmesPath, Assert: "type_match", - Expect: expected, + Expect: expected, Message: msg, } s.step.Validators = append(s.step.Validators, v) @@ -156,12 +156,11 @@ func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected inte return s } - func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation { v := Validator{ Check: jmesPath, Assert: "contained_by", - Expect: expected, + Expect: expected, Message: msg, } s.step.Validators = append(s.step.Validators, v) @@ -183,7 +182,7 @@ func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected inte v := Validator{ Check: jmesPath, Assert: "string_equals", - Expect: expected, + Expect: expected, Message: msg, } s.step.Validators = append(s.step.Validators, v)