feat: record execution data for report #25

This commit is contained in:
xucong053
2022-01-26 13:38:18 +08:00
parent 92f7430634
commit 56cc99bbc0
13 changed files with 337 additions and 124 deletions

View File

@@ -122,7 +122,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
// step failed // step failed
var elapsed int64 var elapsed int64
if stepData != nil { if stepData != nil {
elapsed = stepData.elapsed elapsed = stepData.Elapsed
} }
b.RecordFailure(step.Type(), step.Name(), elapsed, err.Error()) b.RecordFailure(step.Type(), step.Name(), elapsed, err.Error())
@@ -139,14 +139,14 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
} }
// step success // step success
if stepData.stepType == stepTypeTransaction { if stepData.StepType == stepTypeTransaction {
// transaction // transaction
// FIXME: support nested transactions // FIXME: support nested transactions
if step.ToStruct().Transaction.Type == transactionEnd { // only record when transaction ends 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 transactionSuccess = true // reset flag for next transaction
} }
} else if stepData.stepType == stepTypeRendezvous { } else if stepData.StepType == stepTypeRendezvous {
// rendezvous // rendezvous
// TODO: implement rendezvous in boomer // TODO: implement rendezvous in boomer
rendezvous := step.ToStruct().Rendezvous rendezvous := step.ToStruct().Rendezvous
@@ -155,7 +155,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase) *boomer.Task {
} }
} else { } else {
// request or testcase step // 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() endTime := time.Now()

View File

@@ -27,7 +27,8 @@ var runCmd = &cobra.Command{
} }
runner := hrp.NewRunner(nil). runner := hrp.NewRunner(nil).
SetDebug(!silentFlag). SetDebug(!silentFlag).
SetFailfast(!continueOnFailure) SetFailfast(!continueOnFailure).
SetSaveTests(saveTests)
if proxyUrl != "" { if proxyUrl != "" {
runner.SetProxyUrl(proxyUrl) runner.SetProxyUrl(proxyUrl)
} }
@@ -42,6 +43,7 @@ var (
continueOnFailure bool continueOnFailure bool
silentFlag bool silentFlag bool
proxyUrl string proxyUrl string
saveTests bool
) )
func init() { 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().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().BoolVarP(&silentFlag, "silent", "s", false, "disable logging request & response details")
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url") 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") // runCmd.Flags().BoolP("gen-html-report", "r", false, "Generate HTML report")
} }

View File

@@ -11,49 +11,6 @@ import (
"gopkg.in/yaml.v3" "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) { func loadFromJSON(path string) (*TCase, error) {
path, err := filepath.Abs(path) path, err := filepath.Abs(path)
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/httprunner/hrp" "github.com/httprunner/hrp"
"github.com/httprunner/hrp/internal/builtin"
) )
var rendezvousTestcase = &hrp.TestCase{ var rendezvousTestcase = &hrp.TestCase{
@@ -58,7 +59,7 @@ func TestRendezvousDump2JSON(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("ToTCase error: %v", err) t.Fatalf("ToTCase error: %v", err)
} }
err = tCase.Dump2JSON("rendezvous_test.json") err = builtin.Dump2JSON(tCase, "rendezvous_test.json")
if err != nil { if err != nil {
t.Fatalf("dump to json error: %v", err) t.Fatalf("dump to json error: %v", err)
} }

View File

@@ -16,7 +16,7 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual
"greater_or_equals": assert.GreaterOrEqual, "greater_or_equals": assert.GreaterOrEqual,
"less_or_equals": assert.LessOrEqual, "less_or_equals": assert.LessOrEqual,
"not_equal": assert.NotEqual, "not_equal": assert.NotEqual,
"contained_by": assert.Contains, "contained_by": assert.Contains,
"regex_match": assert.Regexp, "regex_match": assert.Regexp,
"type_match": assert.IsType, "type_match": assert.IsType,
// custom assertions // custom assertions
@@ -28,7 +28,7 @@ var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual
"length_less_or_equals": LessOrEqualsLength, "length_less_or_equals": LessOrEqualsLength,
"length_greater_than": GreaterThanLength, "length_greater_than": GreaterThanLength,
"length_greater_or_equals": GreaterOrEqualsLength, "length_greater_or_equals": GreaterOrEqualsLength,
"contains": Contains, "contains": Contains,
"string_equals": EqualString, "string_equals": EqualString,
} }

View File

@@ -1,9 +1,11 @@
package builtin package builtin
import ( import (
"bytes"
"crypto/md5" "crypto/md5"
"encoding/csv" "encoding/csv"
"encoding/hex" "encoding/hex"
"encoding/json"
"io/ioutil" "io/ioutil"
"math" "math"
"math/rand" "math/rand"
@@ -11,6 +13,8 @@ import (
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v3"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -19,7 +23,7 @@ var Functions = map[string]interface{}{
"sleep": sleep, // call with one argument "sleep": sleep, // call with one argument
"gen_random_string": genRandomString, // call with one argument "gen_random_string": genRandomString, // call with one argument
"max": math.Max, // call with two arguments "max": math.Max, // call with two arguments
"md5": MD5, // call with one argument "md5": MD5, // call with one argument
"parameterize": loadFromCSV, "parameterize": loadFromCSV,
"P": loadFromCSV, "P": loadFromCSV,
} }
@@ -82,3 +86,46 @@ func loadFromCSV(path string) []map[string]interface{} {
} }
return result 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
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp" "github.com/httprunner/hrp"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
) )
@@ -55,7 +56,7 @@ func (h *har) GenJSON() (jsonPath string, err error) {
return "", err return "", err
} }
jsonPath = h.genOutputPath(suffixJSON) jsonPath = h.genOutputPath(suffixJSON)
err = tCase.Dump2JSON(jsonPath) err = builtin.Dump2JSON(tCase, jsonPath)
return return
} }
@@ -74,7 +75,7 @@ func (h *har) GenYAML() (yamlPath string, err error) {
return "", err return "", err
} }
yamlPath = h.genOutputPath(suffixYAML) yamlPath = h.genOutputPath(suffixYAML)
err = tCase.Dump2YAML(yamlPath) err = builtin.Dump2YAML(tCase, yamlPath)
return return
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp" "github.com/httprunner/hrp"
"github.com/httprunner/hrp/internal/builtin"
) )
var ( var (
@@ -32,11 +33,11 @@ func removeHashicorpPlugin() {
func TestGenDemoTestCase(t *testing.T) { func TestGenDemoTestCase(t *testing.T) {
tCase, _ := demoTestCase.ToTCase() tCase, _ := demoTestCase.ToTCase()
err := tCase.Dump2JSON(demoTestCaseJSONPath) err := builtin.Dump2JSON(tCase, demoTestCaseJSONPath)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
err = tCase.Dump2YAML(demoTestCaseYAMLPath) err = builtin.Dump2YAML(tCase, demoTestCaseYAMLPath)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }

View File

@@ -8,6 +8,7 @@ import (
"path" "path"
"strings" "strings"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -48,12 +49,12 @@ func CreateScaffold(projectName string) error {
// create demo testcases // create demo testcases
tCase, _ := demoTestCase.ToTCase() 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 { if err != nil {
log.Error().Err(err).Msg("create demo.json testcase failed") log.Error().Err(err).Msg("create demo.json testcase failed")
return err return err
} }
err = tCase.Dump2YAML(path.Join(projectName, "testcases", "demo.yaml")) err = builtin.Dump2YAML(tCase, path.Join(projectName, "testcases", "demo.yaml"))
if err != nil { if err != nil {
log.Error().Err(err).Msg("create demo.yml testcase failed") log.Error().Err(err).Msg("create demo.yml testcase failed")
return err return err

137
models.go
View File

@@ -1,9 +1,13 @@
package hrp package hrp
import ( import (
"fmt"
"math/rand" "math/rand"
"runtime"
"sync" "sync"
"time" "time"
"github.com/httprunner/hrp/internal/version"
) )
const ( const (
@@ -209,13 +213,132 @@ func (tc *TestCase) ToTCase() (*TCase, error) {
return &tCase, nil 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 { type stepData struct {
name string // step name Name string `json:"name" yaml:"name"` // step name
stepType stepType // step type, testcase/request/transaction/rendezvous StepType stepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
success bool // step execution result Success bool `json:"success" yaml:"success"` // step execution result
elapsed int64 // step execution time in millisecond(ms) Elapsed int64 `json:"elapsed_ms" yaml:"elapsed_ms"` // step execution time in millisecond(ms)
contentSize int64 // response body length Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data
exportVars map[string]interface{} // extract variables 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,
}
} }

View File

@@ -79,7 +79,7 @@ type responseObject struct {
t *testing.T t *testing.T
parser *parser parser *parser
respObjMeta interface{} respObjMeta interface{}
validationResults map[string]interface{} validationResults []*validationResult
} }
func (v *responseObject) Extract(extractors map[string]string) map[string]interface{} { 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 { if err != nil {
return err return err
} }
validResult := &validationResult{
Validator: Validator{
Check: validator.Check,
Expect: expectValue,
Assert: assertMethod,
Message: validator.Message,
},
CheckValue: checkValue,
CheckResult: "fail",
}
// do assertion // do assertion
result := assertFunc(v.t, expectValue, checkValue) result := assertFunc(v.t, expectValue, checkValue)
if result {
validResult.CheckResult = "pass"
}
v.validationResults = append(v.validationResults, validResult)
log.Info(). log.Info().
Str("assertMethod", assertMethod). Str("assertMethod", assertMethod).
Interface("expectValue", expectValue). Interface("expectValue", expectValue).

172
runner.go
View File

@@ -23,10 +23,15 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga" "github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common" "github.com/httprunner/hrp/plugin/common"
) )
const (
summaryPath string = "summary.json"
)
// Run starts to run API test with default configs. // Run starts to run API test with default configs.
func Run(testcases ...ITestCase) error { func Run(testcases ...ITestCase) error {
t := &testing.T{} t := &testing.T{}
@@ -52,10 +57,11 @@ func NewRunner(t *testing.T) *HRPRunner {
} }
type HRPRunner struct { type HRPRunner struct {
t *testing.T t *testing.T
failfast bool failfast bool
debug bool debug bool
client *http.Client saveTests bool
client *http.Client
} }
// SetFailfast configures whether to stop running when one step fails. // SetFailfast configures whether to stop running when one step fails.
@@ -87,6 +93,13 @@ func (r *HRPRunner) SetProxyUrl(proxyUrl string) *HRPRunner {
return r 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. // Run starts to execute one or multiple testcases.
func (r *HRPRunner) Run(testcases ...ITestCase) error { func (r *HRPRunner) Run(testcases ...ITestCase) error {
event := ga.EventTracking{ 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") log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed")
return err return err
} }
s := newOutSummary()
// 在runner模式下指定整体策略cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器 // 在runner模式下指定整体策略cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器
for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); { for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); {
// iterate through all parameter iterators and update case variables // 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) 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") log.Error().Err(err).Msg("[Run] run testcase failed")
return err 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 return nil
@@ -133,6 +155,7 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) *caseRunner {
TestCase: testcase, TestCase: testcase,
hrpRunner: r, hrpRunner: r,
parser: newParser(), parser: newParser(),
summary: newSummary(),
} }
caseRunner.reset() caseRunner.reset()
return caseRunner return caseRunner
@@ -148,7 +171,8 @@ type caseRunner struct {
// transactions stores transaction timing info. // transactions stores transaction timing info.
// key is transaction name, value is map of transaction type and time, e.g. start time and end time. // 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 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. // reset clears runner session variables.
@@ -157,6 +181,7 @@ func (r *caseRunner) reset() *caseRunner {
r.sessionVariables = make(map[string]interface{}) r.sessionVariables = make(map[string]interface{})
r.transactions = make(map[string]map[transactionType]time.Time) r.transactions = make(map[string]map[transactionType]time.Time)
r.startTime = time.Now() r.startTime = time.Now()
r.summary.Name = r.Config.Name
return r return r
} }
@@ -179,12 +204,16 @@ func (r *caseRunner) run() error {
r.startTime = time.Now() r.startTime = time.Now()
for index := range r.TestCase.TestSteps { for index := range r.TestCase.TestSteps {
_, err := r.runStep(index, config) stepData, err := r.runStep(index, config)
if err != nil { if err != nil {
if r.hrpRunner.failfast { if r.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting") 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") 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) stepResult, err = r.runStepTestCase(copiedStep)
if err != nil { if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed") log.Error().Err(err).Msg("run referenced testcase step failed")
return
} }
} else { } 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 // run request
copiedStep.Request.URL = buildURL(caseConfig.BaseURL, copiedStep.Request.URL) // avoid data racing
stepResult, err = r.runStepRequest(copiedStep) stepResult, err = r.runStepRequest(copiedStep)
if err != nil { if err != nil {
log.Error().Err(err).Msg("run request step failed") log.Error().Err(err).Msg("run request step failed")
return
} }
} }
// update extracted variables // update extracted variables
for k, v := range stepResult.exportVars { for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v r.sessionVariables[k] = v
} }
log.Info(). log.Info().
Str("step", step.Name()). Str("step", step.Name()).
Bool("success", stepResult.success). Bool("success", stepResult.Success).
Interface("exportVars", stepResult.exportVars). Interface("exportVars", stepResult.ExportVars).
Msg("run step end") Msg("run step end")
return stepResult, nil return stepResult, err
} }
func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) { func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) {
@@ -301,11 +335,11 @@ func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *s
Msg("transaction") Msg("transaction")
stepResult = &stepData{ stepResult = &stepData{
name: transaction.Name, Name: transaction.Name,
stepType: stepTypeTransaction, StepType: stepTypeTransaction,
success: true, Success: true,
elapsed: 0, Elapsed: 0,
contentSize: 0, // TODO: record transaction total response length ContentSize: 0, // TODO: record transaction total response length
} }
// create transaction if not exists // create transaction if not exists
@@ -329,7 +363,7 @@ func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *s
// calculate transaction duration // calculate transaction duration
duration := r.transactions[transaction.Name][transactionEnd].Sub( duration := r.transactions[transaction.Name][transactionEnd].Sub(
r.transactions[transaction.Name][transactionStart]) r.transactions[transaction.Name][transactionStart])
stepResult.elapsed = duration.Milliseconds() stepResult.Elapsed = duration.Milliseconds()
log.Info().Str("name", transaction.Name).Dur("elapsed", duration).Msg("transaction") 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). Int64("timeout", rendezvous.Timeout).
Msg("rendezvous") Msg("rendezvous")
stepResult = &stepData{ stepResult = &stepData{
name: rendezvous.Name, Name: rendezvous.Name,
stepType: stepTypeRendezvous, StepType: stepTypeRendezvous,
success: true, Success: true,
} }
// pass current rendezvous if already released, activate rendezvous sequentially after spawn done // 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) { func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err error) {
stepResult = &stepData{ stepResult = &stepData{
name: step.Name, Name: step.Name,
stepType: stepTypeRequest, StepType: stepTypeRequest,
success: false, Success: false,
contentSize: 0, 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 rawUrl := step.Request.URL
method := step.Request.Method method := step.Request.Method
req := &http.Request{ req := &http.Request{
@@ -538,7 +576,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
if len(step.Request.Headers) > 0 { if len(step.Request.Headers) > 0 {
headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables) headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "parse headers failed") return stepResult, errors.Wrap(err, "parse headers failed")
} }
for key, value := range headers { for key, value := range headers {
req.Header.Add(key, value) 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 { if len(step.Request.Params) > 0 {
params, err := r.parser.parseData(step.Request.Params, step.Variables) params, err := r.parser.parseData(step.Request.Params, step.Variables)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "parse data failed") return stepResult, errors.Wrap(err, "parse data failed")
} }
parsedParams := params.(map[string]interface{}) parsedParams := params.(map[string]interface{})
sessionData.ReqResps.Request.Params = parsedParams
if len(parsedParams) > 0 { if len(parsedParams) > 0 {
queryParams = make(url.Values) queryParams = make(url.Values)
for k, v := range parsedParams { for k, v := range parsedParams {
@@ -587,8 +626,9 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
if step.Request.Body != nil { if step.Request.Body != nil {
data, err := r.parser.parseData(step.Request.Body, step.Variables) data, err := r.parser.parseData(step.Request.Body, step.Variables)
if err != nil { if err != nil {
return nil, err return stepResult, err
} }
sessionData.ReqResps.Request.Body = data
var dataBytes []byte var dataBytes []byte
switch vv := data.(type) { switch vv := data.(type) {
case map[string]interface{}: case map[string]interface{}:
@@ -604,7 +644,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
// post json // post json
dataBytes, err = json.Marshal(vv) dataBytes, err = json.Marshal(vv)
if err != nil { if err != nil {
return nil, err return stepResult, err
} }
req.Header.Set("Content-Type", "application/json; charset=UTF-8") 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: case bytes.Buffer:
dataBytes = vv.Bytes() dataBytes = vv.Bytes()
default: // unexpected body type default: // unexpected body type
return nil, errors.New("unexpected request body type") return stepResult, errors.New("unexpected request body type")
} }
setBodyBytes(req, dataBytes) 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 // prepare url
u, err := url.Parse(rawUrl) u, err := url.Parse(rawUrl)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "parse url failed") return stepResult, errors.Wrap(err, "parse url failed")
} }
req.URL = u req.URL = u
req.Host = u.Host req.Host = u.Host
@@ -632,7 +677,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
if r.hrpRunner.debug { if r.hrpRunner.debug {
reqDump, err := httputil.DumpRequest(req, true) reqDump, err := httputil.DumpRequest(req, true)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "dump request failed") return stepResult, errors.Wrap(err, "dump request failed")
} }
fmt.Println("-------------------- request --------------------") fmt.Println("-------------------- request --------------------")
fmt.Println(string(reqDump)) fmt.Println(string(reqDump))
@@ -641,9 +686,9 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
// do request action // do request action
start := time.Now() start := time.Now()
resp, err := r.hrpRunner.client.Do(req) resp, err := r.hrpRunner.client.Do(req)
stepResult.elapsed = time.Since(start).Milliseconds() stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "do request failed") return stepResult, errors.Wrap(err, "do request failed")
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -652,7 +697,7 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
fmt.Println("==================== response ===================") fmt.Println("==================== response ===================")
respDump, err := httputil.DumpResponse(resp, true) respDump, err := httputil.DumpResponse(resp, true)
if err != nil { 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(string(respDump))
fmt.Println("--------------------------------------------------") fmt.Println("--------------------------------------------------")
@@ -664,31 +709,33 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro
err = errors.Wrap(err, "init ResponseObject error") err = errors.Wrap(err, "init ResponseObject error")
return return
} }
sessionData.ReqResps.Response = respObj.respObjMeta
// extract variables from response // extract variables from response
extractors := step.Extract extractors := step.Extract
extractMapping := respObj.Extract(extractors) extractMapping := respObj.Extract(extractors)
stepResult.exportVars = extractMapping stepResult.ExportVars = extractMapping
// override step variables with extracted variables // override step variables with extracted variables
stepVariables := mergeVariables(step.Variables, extractMapping) stepVariables := mergeVariables(step.Variables, extractMapping)
// validate response // validate response
err = respObj.Validate(step.Validators, stepVariables) err = respObj.Validate(step.Validators, stepVariables)
if err != nil { sessionData.Validators = respObj.validationResults
return if err == nil {
sessionData.Success = true
stepResult.Success = true
} }
stepResult.ContentSize = resp.ContentLength
stepResult.success = true stepResult.Data = sessionData
stepResult.contentSize = resp.ContentLength return stepResult, err
return stepResult, nil
} }
func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err error) { func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err error) {
stepResult = &stepData{ stepResult = &stepData{
name: step.Name, Name: step.Name,
stepType: stepTypeTestCase, StepType: stepTypeTestCase,
success: false, Success: false,
} }
testcase := step.TestCase testcase := step.TestCase
@@ -696,16 +743,18 @@ func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err err
copiedTestCase := &TestCase{} copiedTestCase := &TestCase{}
if err = copier.Copy(copiedTestCase, testcase); err != nil { if err = copier.Copy(copiedTestCase, testcase); err != nil {
log.Error().Err(err).Msg("copy testcase failed") log.Error().Err(err).Msg("copy testcase failed")
return nil, err return stepResult, err
} }
start := time.Now() start := time.Now()
err = r.hrpRunner.newCaseRunner(copiedTestCase).run() caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase)
stepResult.elapsed = time.Since(start).Milliseconds() err = caseRunnerObj.run()
stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil { if err != nil {
return stepResult, err return stepResult, err
} }
stepResult.success = true stepResult.Data = caseRunnerObj.getSummary()
stepResult.Success = true
return stepResult, nil return stepResult, nil
} }
@@ -735,8 +784,25 @@ func (r *caseRunner) parseConfig(cfg *TConfig) error {
return nil return nil
} }
func newSummary() *testCaseSummary {
return &testCaseSummary{
Success: true,
Time: &testCaseTime{},
InOut: &testCaseInOut{},
}
}
func (r *caseRunner) getSummary() *testCaseSummary { 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) { func setBodyBytes(req *http.Request, data []byte) {

View File

@@ -105,7 +105,7 @@ func (s *StepRequestValidation) AssertTypeMatch(jmesPath string, expected interf
v := Validator{ v := Validator{
Check: jmesPath, Check: jmesPath,
Assert: "type_match", Assert: "type_match",
Expect: expected, Expect: expected,
Message: msg, Message: msg,
} }
s.step.Validators = append(s.step.Validators, v) s.step.Validators = append(s.step.Validators, v)
@@ -156,12 +156,11 @@ func (s *StepRequestValidation) AssertLengthEqual(jmesPath string, expected inte
return s return s
} }
func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation { func (s *StepRequestValidation) AssertContainedBy(jmesPath string, expected interface{}, msg string) *StepRequestValidation {
v := Validator{ v := Validator{
Check: jmesPath, Check: jmesPath,
Assert: "contained_by", Assert: "contained_by",
Expect: expected, Expect: expected,
Message: msg, Message: msg,
} }
s.step.Validators = append(s.step.Validators, v) s.step.Validators = append(s.step.Validators, v)
@@ -183,7 +182,7 @@ func (s *StepRequestValidation) AssertStringEqual(jmesPath string, expected inte
v := Validator{ v := Validator{
Check: jmesPath, Check: jmesPath,
Assert: "string_equals", Assert: "string_equals",
Expect: expected, Expect: expected,
Message: msg, Message: msg,
} }
s.step.Validators = append(s.step.Validators, v) s.step.Validators = append(s.step.Validators, v)