diff --git a/hrp/README.md b/hrp/README.md index 38a41391..683a0e1a 100644 --- a/hrp/README.md +++ b/hrp/README.md @@ -51,28 +51,50 @@ type HRPRunner struct { } func (r *HRPRunner) Run(testcases ...ITestCase) error -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) *SessionRunner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) ``` 重点关注两个方法: - Run:测试执行的主入口,支持运行一个或多个测试用例 -- NewSessionRunner:针对给定的测试用例初始化一个 SessionRunner +- NewCaseRunner:针对给定的测试用例初始化一个 CaseRunner -### 用例执行器 SessionRunner +### 用例执行器 CaseRunner -测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 +针对每个测试用例,采用 CaseRunner 存储其公共信息,包括 plugin/parser + +```go +type CaseRunner struct { + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + + parsedConfig *TConfig + parametersIterator *ParametersIterator + rootDir string // project root dir +} + +func (r *CaseRunner) NewSession() *SessionRunner { +``` + +重点关注一个方法: + +- NewSession:测试用例的每一次执行对应一个 SessionRunner + +### SessionRunner + +测试用例的具体执行都由 `SessionRunner` 完成,每个 session 实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 ```go type SessionRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser + caseRunner *CaseRunner sessionVariables map[string]interface{} - transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary + transactions map[string]map[transactionType]time.Time + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary } + +func (r *SessionRunner) Start(givenVars map[string]interface{}) error ``` 重点关注一个方法: @@ -80,12 +102,29 @@ type SessionRunner struct { - Start:启动执行用例,依次执行所有测试步骤 ```go -func (r *SessionRunner) Start() error { +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { ... + r.resetSession() + + r.InitWithParameters(givenVars) + // run step in sequential order for _, step := range r.testCase.TestSteps { - _, err := step.Run(r) - if err != nil && r.hrpRunner.failfast { + // parse step + + // run step + stepResult, err := step.Run(r) + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + + // update extracted variables + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } + + // check if failfast + if err != nil && r.caseRunner.hrpRunner.failfast { return errors.Wrap(err, "abort running due to failfast setting") } } diff --git a/hrp/boomer.go b/hrp/boomer.go index a1d36aac..c3db1e76 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -136,7 +136,7 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TCase { var parsedTestCases []*TCase for _, tc := range testCases { - caseRunner, err := b.hrpRunner.newCaseRunner(tc) + caseRunner, err := b.hrpRunner.NewCaseRunner(tc) if err != nil { log.Error().Err(err).Msg("failed to create runner") os.Exit(1) @@ -313,9 +313,9 @@ func (b *HRPBoomer) PollTestCases(ctx context.Context) { } func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task { - // init runner for testcase + // init case runner for testcase // this runner is shared by multiple session runners - caseRunner, err := b.hrpRunner.newCaseRunner(testcase) + caseRunner, err := b.hrpRunner.NewCaseRunner(testcase) if err != nil { log.Error().Err(err).Msg("failed to create runner") os.Exit(1) @@ -352,18 +352,19 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend transactionSuccess := true // flag current transaction result // init session runner - sessionRunner := caseRunner.newSession() + sessionRunner := caseRunner.NewSession() mutex.Lock() if parametersIterator.HasNext() { - sessionRunner.updateSessionVariables(parametersIterator.Next()) + sessionRunner.InitWithParameters(parametersIterator.Next()) } mutex.Unlock() startTime := time.Now() for _, step := range testcase.TestSteps { + // TODO: parse step struct // parse step name - parsedName, err := sessionRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) + parsedName, err := caseRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) if err != nil { parsedName = step.Name() } diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go index fd479e84..db5b8409 100644 --- a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go +++ b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go @@ -1,4 +1,4 @@ -// NOTE: Generated By hrp v4.2.0, DO NOT EDIT! +// NOTE: Generated By hrp v4.3.0-beta-10172144, DO NOT EDIT! package main import ( diff --git a/hrp/runner.go b/hrp/runner.go index 58559e85..1b164226 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -2,6 +2,7 @@ package hrp import ( "crypto/tls" + _ "embed" "net" "net/http" "net/http/cookiejar" @@ -206,19 +207,24 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { var runErr error // run testcase one by one for _, testcase := range testCases { - sessionRunner, err := r.NewSessionRunner(testcase) + // each testcase has its own case runner + caseRunner, err := r.NewCaseRunner(testcase) if err != nil { - log.Error().Err(err).Msg("[Run] init session runner failed") + log.Error().Err(err).Msg("[Run] init case runner failed") return err } + // release UI driver session defer func() { - for _, client := range sessionRunner.hrpRunner.uiClients { + for _, client := range r.uiClients { client.Driver.DeleteSession() } }() - for it := sessionRunner.parametersIterator; it.HasNext(); { + for it := caseRunner.parametersIterator; it.HasNext(); { + // case runner can run multiple times with different parameters + // each run has its own session runner + sessionRunner := caseRunner.NewSession() err1 := sessionRunner.Start(it.Next()) caseSummary, err2 := sessionRunner.GetSummary() s.appendCaseSummary(caseSummary) @@ -250,23 +256,10 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { return runErr } -// NewSessionRunner creates a new session runner for testcase. -// each testcase has its own session runner -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) (*SessionRunner, error) { - runner, err := r.newCaseRunner(testcase) - if err != nil { - return nil, err - } - - sessionRunner := &SessionRunner{ - testCaseRunner: runner, - } - sessionRunner.resetSession() - return sessionRunner, nil -} - -func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { - runner := &testCaseRunner{ +// NewCaseRunner creates a new case runner for testcase. +// each testcase has its own case runner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) { + caseRunner := &CaseRunner{ testCase: testcase, hrpRunner: r, parser: newParser(), @@ -278,34 +271,31 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { return nil, errors.Wrap(err, "init plugin failed") } if plugin != nil { - runner.parser.plugin = plugin - runner.rootDir = filepath.Dir(plugin.Path()) + caseRunner.parser.plugin = plugin + caseRunner.rootDir = filepath.Dir(plugin.Path()) } // parse testcase config - if err := runner.parseConfig(); err != nil { + if err := caseRunner.parseConfig(); err != nil { return nil, errors.Wrap(err, "parse testcase config failed") } - // init websocket params - initWebSocket(testcase) - // set testcase timeout in seconds - if runner.testCase.Config.Timeout != 0 { - timeout := time.Duration(runner.testCase.Config.Timeout*1000) * time.Millisecond - runner.hrpRunner.SetTimeout(timeout) + if testcase.Config.Timeout != 0 { + timeout := time.Duration(testcase.Config.Timeout*1000) * time.Millisecond + r.SetTimeout(timeout) } // load plugin info to testcase config if plugin != nil { pluginPath, _ := locatePlugin(testcase.Config.Path) - if runner.parsedConfig.PluginSetting == nil { + if caseRunner.parsedConfig.PluginSetting == nil { pluginContent, err := builtin.ReadFile(pluginPath) if err != nil { return nil, err } tp := strings.Split(plugin.Path(), ".") - runner.parsedConfig.PluginSetting = &PluginConfig{ + caseRunner.parsedConfig.PluginSetting = &PluginConfig{ Path: pluginPath, Content: pluginContent, Type: tp[len(tp)-1], @@ -313,10 +303,10 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { } } - return runner, nil + return caseRunner, nil } -type testCaseRunner struct { +type CaseRunner struct { testCase *TestCase hrpRunner *HRPRunner parser *Parser @@ -327,7 +317,7 @@ type testCaseRunner struct { } // parseConfig parses testcase config, stores to parsedConfig. -func (r *testCaseRunner) parseConfig() error { +func (r *CaseRunner) parseConfig() error { cfg := r.testCase.Config r.parsedConfig = &TConfig{} @@ -441,10 +431,181 @@ func (r *testCaseRunner) parseConfig() error { // each boomer task initiates a new session // in order to avoid data racing -func (r *testCaseRunner) newSession() *SessionRunner { +func (r *CaseRunner) NewSession() *SessionRunner { sessionRunner := &SessionRunner{ - testCaseRunner: r, + caseRunner: r, } sessionRunner.resetSession() return sessionRunner } + +// SessionRunner is used to run testcase and its steps. +// each testcase has its own SessionRunner instance and share session variables. +type SessionRunner struct { + caseRunner *CaseRunner + sessionVariables map[string]interface{} + // 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 + summary *TestCaseSummary // record test case summary + wsConnMap map[string]*websocket.Conn // save all websocket connections + pongResponseChan chan string // channel used to receive pong response message + closeResponseChan chan *wsCloseRespObject // channel used to receive close response message +} + +func (r *SessionRunner) resetSession() { + log.Info().Msg("reset session runner") + r.sessionVariables = make(map[string]interface{}) + r.transactions = make(map[string]map[transactionType]time.Time) + r.startTime = time.Now() + r.summary = newSummary() + r.wsConnMap = make(map[string]*websocket.Conn) + r.pongResponseChan = make(chan string, 1) + r.closeResponseChan = make(chan *wsCloseRespObject, 1) +} + +// Start runs the test steps in sequential order. +// givenVars is used for data driven +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { + config := r.caseRunner.testCase.Config + log.Info().Str("testcase", config.Name).Msg("run testcase start") + + // reset session runner + r.resetSession() + + // update config variables with given variables + r.InitWithParameters(givenVars) + + // run step in sequential order + for _, step := range r.caseRunner.testCase.TestSteps { + // TODO: parse step struct + // parse step name + parsedName, err := r.caseRunner.parser.ParseString(step.Name(), r.sessionVariables) + if err != nil { + parsedName = step.Name() + } + stepName := convertString(parsedName) + log.Info().Str("step", stepName). + Str("type", string(step.Type())).Msg("run step start") + + // run step + stepResult, err := step.Run(r) + stepResult.Name = stepName + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + r.summary.Stat.Total += 1 + if stepResult.Success { + r.summary.Stat.Successes += 1 + } else { + r.summary.Stat.Failures += 1 + // update summary result to failed + r.summary.Success = false + } + + // update extracted variables + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } + + if err == nil { + log.Info().Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", true). + Interface("exportVars", stepResult.ExportVars). + Msg("run step end") + continue + } + + // failed + log.Error().Err(err).Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", false). + Msg("run step end") + + // check if failfast + if r.caseRunner.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") + } + } + + // close websocket connection after all steps done + defer func() { + for _, wsConn := range r.wsConnMap { + if wsConn != nil { + log.Info().Str("testcase", config.Name).Msg("websocket disconnected") + err := wsConn.Close() + if err != nil { + log.Error().Err(err).Msg("websocket disconnection failed") + } + } + } + }() + + log.Info().Str("testcase", config.Name).Msg("run testcase end") + return nil +} + +// ParseStepVariables merges step variables with config variables and session variables +func (r *SessionRunner) ParseStepVariables(stepVariables map[string]interface{}) (map[string]interface{}, error) { + // override variables + // step variables > session variables (extracted variables from previous steps) + overrideVars := mergeVariables(stepVariables, r.sessionVariables) + // step variables > testcase config variables + overrideVars = mergeVariables(overrideVars, r.caseRunner.parsedConfig.Variables) + + // parse step variables + parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars) + if err != nil { + log.Error().Interface("variables", r.caseRunner.parsedConfig.Variables). + Err(err).Msg("parse step variables failed") + return nil, err + } + return parsedVariables, nil +} + +// InitWithParameters updates session variables with given parameters. +// this is used for data driven +func (r *SessionRunner) InitWithParameters(parameters map[string]interface{}) { + if len(parameters) == 0 { + return + } + + log.Info().Interface("parameters", parameters).Msg("update session variables") + for k, v := range parameters { + r.sessionVariables[k] = v + } +} + +func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { + caseSummary := r.summary + caseSummary.Name = r.caseRunner.parsedConfig.Name + caseSummary.Time.StartAt = r.startTime + caseSummary.Time.Duration = time.Since(r.startTime).Seconds() + exportVars := make(map[string]interface{}) + for _, value := range r.caseRunner.parsedConfig.Export { + exportVars[value] = r.sessionVariables[value] + } + caseSummary.InOut.ExportVars = exportVars + caseSummary.InOut.ConfigVars = r.caseRunner.parsedConfig.Variables + + for uuid, client := range r.caseRunner.hrpRunner.uiClients { + // add WDA/UIA logs to summary + log, err := client.Driver.StopCaptureLog() + if err != nil { + return caseSummary, err + } + logs := map[string]interface{}{ + "uuid": uuid, + "content": log, + } + + // stop performance monitor + logs["performance"] = client.GetPerfData() + + caseSummary.Logs = append(caseSummary.Logs, logs) + } + + return caseSummary, nil +} diff --git a/hrp/session.go b/hrp/session.go deleted file mode 100644 index b752ccea..00000000 --- a/hrp/session.go +++ /dev/null @@ -1,191 +0,0 @@ -package hrp - -import ( - _ "embed" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" -) - -// SessionRunner is used to run testcase and its steps. -// each testcase has its own SessionRunner instance and share session variables. -type SessionRunner struct { - *testCaseRunner - sessionVariables map[string]interface{} - // 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 - summary *TestCaseSummary // record test case summary - wsConnMap map[string]*websocket.Conn // save all websocket connections - pongResponseChan chan string // channel used to receive pong response message - closeResponseChan chan *wsCloseRespObject // channel used to receive close response message -} - -func (r *SessionRunner) resetSession() { - log.Info().Msg("reset session runner") - r.sessionVariables = make(map[string]interface{}) - r.transactions = make(map[string]map[transactionType]time.Time) - r.startTime = time.Now() - r.summary = newSummary() - r.wsConnMap = make(map[string]*websocket.Conn) - r.pongResponseChan = make(chan string, 1) - r.closeResponseChan = make(chan *wsCloseRespObject, 1) -} - -func (r *SessionRunner) HTTPStatOn() bool { - return r.hrpRunner.httpStatOn -} - -func (r *SessionRunner) LogOn() bool { - return r.hrpRunner.requestsLogOn -} - -// Start runs the test steps in sequential order. -// givenVars is used for data driven -func (r *SessionRunner) Start(givenVars map[string]interface{}) error { - config := r.testCase.Config - log.Info().Str("testcase", config.Name).Msg("run testcase start") - - // reset session runner - r.resetSession() - - // update config variables with given variables - r.updateSessionVariables(givenVars) - - // run step in sequential order - for _, step := range r.testCase.TestSteps { - // TODO: parse step - // parse step name - parsedName, err := r.parser.ParseString(step.Name(), r.sessionVariables) - if err != nil { - parsedName = step.Name() - } - stepName := convertString(parsedName) - log.Info().Str("step", stepName). - Str("type", string(step.Type())).Msg("run step start") - - // merge step variables with session variables - step.Struct().Variables, err = r.mergeStepVariables(step.Struct().Variables) - if err != nil { - return errors.Wrap(err, "merge step variables with session variables failed") - } - - // run step - stepResult, err := step.Run(r) - stepResult.Name = stepName - - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - log.Info(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", true). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - log.Error(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", false). - Msg("run step end") - } - - // check if failfast - if err != nil && r.hrpRunner.failfast { - return errors.Wrap(err, "abort running due to failfast setting") - } - - // update extracted variables - for k, v := range stepResult.ExportVars { - r.sessionVariables[k] = v - } - } - - // close websocket connection after all steps done - defer func() { - for _, wsConn := range r.wsConnMap { - if wsConn != nil { - log.Info().Str("testcase", config.Name).Msg("websocket disconnected") - err := wsConn.Close() - if err != nil { - log.Error().Err(err).Msg("websocket disconnection failed") - } - } - } - }() - - log.Info().Str("testcase", config.Name).Msg("run testcase end") - return nil -} - -// mergeStepVariables merges step variables with config variables and session variables -func (r *SessionRunner) mergeStepVariables(vars map[string]interface{}) (map[string]interface{}, error) { - // override variables - // step variables > session variables (extracted variables from previous steps) - overrideVars := mergeVariables(vars, r.sessionVariables) - // step variables > testcase config variables - overrideVars = mergeVariables(overrideVars, r.parsedConfig.Variables) - - // parse step variables - parsedVariables, err := r.parser.ParseVariables(overrideVars) - if err != nil { - log.Error().Interface("variables", r.parsedConfig.Variables). - Err(err).Msg("parse step variables failed") - return nil, err - } - return parsedVariables, nil -} - -// updateSessionVariables updates session variables with given variables. -// this is used for data driven -func (r *SessionRunner) updateSessionVariables(parameters map[string]interface{}) { - if len(parameters) == 0 { - return - } - - log.Info().Interface("parameters", parameters).Msg("update session variables") - for k, v := range parameters { - r.sessionVariables[k] = v - } -} - -func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { - caseSummary := r.summary - caseSummary.Name = r.parsedConfig.Name - caseSummary.Time.StartAt = r.startTime - caseSummary.Time.Duration = time.Since(r.startTime).Seconds() - exportVars := make(map[string]interface{}) - for _, value := range r.parsedConfig.Export { - exportVars[value] = r.sessionVariables[value] - } - caseSummary.InOut.ExportVars = exportVars - caseSummary.InOut.ConfigVars = r.parsedConfig.Variables - - for uuid, client := range r.hrpRunner.uiClients { - // add WDA/UIA logs to summary - log, err := client.Driver.StopCaptureLog() - if err != nil { - return caseSummary, err - } - logs := map[string]interface{}{ - "uuid": uuid, - "content": log, - } - - // stop performance monitor - logs["performance"] = client.GetPerfData() - - caseSummary.Logs = append(caseSummary.Logs, logs) - } - - return caseSummary, nil -} diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index cb44af82..a6dad716 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -554,7 +554,13 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err ContentSize: 0, } screenshots := make([]string, 0) - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := s.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } var osType string var mobileStep *MobileStep @@ -569,7 +575,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err } // init wda/uia driver - uiDriver, err := s.hrpRunner.initUIClient(mobileStep.Serial, osType) + uiDriver, err := s.caseRunner.hrpRunner.initUIClient(mobileStep.Serial, osType) if err != nil { return } @@ -602,7 +608,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { - if action.Params, err = s.parser.Parse(action.Params, stepVariables); err != nil { + if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } if err := uiDriver.DoAction(action); err != nil { diff --git a/hrp/step_rendezvous.go b/hrp/step_rendezvous.go index edd9cf84..a9e5f0e0 100644 --- a/hrp/step_rendezvous.go +++ b/hrp/step_rendezvous.go @@ -44,7 +44,7 @@ func (s *StepRendezvous) Run(r *SessionRunner) (*StepResult, error) { } // pass current rendezvous if already released, activate rendezvous sequentially after spawn done - if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.testCase.ToTCase()) || !rendezvous.isSpawnDone() { + if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.caseRunner.testCase.ToTCase()) || !rendezvous.isSpawnDone() { return stepResult, nil } diff --git a/hrp/step_request.go b/hrp/step_request.go index b86ddabd..89269d6f 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -291,7 +291,13 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err Success: false, ContentSize: 0, } - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -300,14 +306,14 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } }() - err = prepareUpload(r.parser, step, stepVariables) + err = prepareUpload(r.caseRunner.parser, step, stepVariables) if err != nil { return } sessionData := newSessionData() - parser := r.parser - config := r.parsedConfig + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig rb := newRequestBuilder(parser, config, step.Request) rb.req.Method = string(step.Request.Method) @@ -340,7 +346,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } // log & print request - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printRequest(rb.req); err != nil { return stepResult, err } @@ -348,7 +354,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // stat HTTP request var httpStat httpstat.Stat - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { ctx := httpstat.WithHTTPStat(rb.req, &httpStat) rb.req = rb.req.WithContext(ctx) } @@ -356,9 +362,9 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // select HTTP client var client *http.Client if step.Request.HTTP2 { - client = r.hrpRunner.http2Client + client = r.caseRunner.hrpRunner.http2Client } else { - client = r.hrpRunner.httpClient + client = r.caseRunner.hrpRunner.httpClient } // set step timeout @@ -384,21 +390,21 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err defer resp.Body.Close() // log & print response - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printResponse(resp); err != nil { return stepResult, err } } // new response object - respObj, err := newHttpResponseObject(r.hrpRunner.t, parser, resp) + respObj, err := newHttpResponseObject(r.caseRunner.hrpRunner.t, parser, resp) if err != nil { err = errors.Wrap(err, "init ResponseObject error") return } stepResult.Elapsed = time.Since(start).Milliseconds() - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { // resp.Body has been ReadAll httpStat.Finish() stepResult.HttpStat = httpStat.Durations() diff --git a/hrp/step_request_test.go b/hrp/step_request_test.go index d49a9253..3f7e63c5 100644 --- a/hrp/step_request_test.go +++ b/hrp/step_request_test.go @@ -77,29 +77,13 @@ func TestRunRequestPostDataToStruct(t *testing.T) { } } -func TestRunRequestRun(t *testing.T) { - testcase := &TestCase{ - Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{stepGET, stepPOSTData}, - } - runner := NewRunner(t).SetRequestsLogOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) - - if _, err := stepGET.Run(sessionRunner); err != nil { - t.Fatalf("stepGET.Run() error: %v", err) - } - if _, err := stepPOSTData.Run(sessionRunner); err != nil { - t.Fatalf("stepPOSTData.Run() error: %v", err) - } -} - func TestRunRequestStatOn(t *testing.T) { testcase := &TestCase{ Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), TestSteps: []IStep{stepGET, stepPOSTData}, } - runner := NewRunner(t).SetHTTPStatOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) + caseRunner, _ := NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase) + sessionRunner := caseRunner.NewSession() if err := sessionRunner.Start(nil); err != nil { t.Fatal() } diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index a581a289..1464735e 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -51,7 +51,13 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe StepType: stepTypeTestCase, Success: false, } - stepVariables := s.step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(s.step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -77,11 +83,12 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe // merge & override extractors copiedTestCase.Config.Export = mergeSlices(s.step.Export, copiedTestCase.Config.Export) - sessionRunner, err := r.hrpRunner.NewSessionRunner(copiedTestCase) + caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(copiedTestCase) if err != nil { - log.Error().Err(err).Msg("create session runner failed") + log.Error().Err(err).Msg("create case runner failed") return stepResult, err } + sessionRunner := caseRunner.NewSession() start := time.Now() // run referenced testcase with step variables diff --git a/hrp/step_thinktime.go b/hrp/step_thinktime.go index 6b14b462..7a505cf8 100644 --- a/hrp/step_thinktime.go +++ b/hrp/step_thinktime.go @@ -39,7 +39,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { Success: true, } - cfg := r.parsedConfig.ThinkTimeSetting + cfg := r.caseRunner.parsedConfig.ThinkTimeSetting if cfg == nil { cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} } diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 0beb2147..fbb5cfd5 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -231,20 +231,19 @@ type WebSocketAction struct { Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` } -func initWebSocket(testcase *TestCase) { - for _, step := range testcase.TestSteps { - if step.Struct().WebSocket == nil { - continue - } - // init websocket action parameters - if step.Struct().WebSocket.Timeout <= 0 { - step.Struct().WebSocket.Timeout = defaultTimeout - } - // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 - if step.Struct().WebSocket.CloseStatusCode < 1000 || step.Struct().WebSocket.CloseStatusCode > 4999 { - step.Struct().WebSocket.CloseStatusCode = defaultCloseStatus - } +func (w *WebSocketAction) GetTimeout() int64 { + if w.Timeout <= 0 { + return defaultTimeout } + return w.Timeout +} + +func (w *WebSocketAction) GetCloseStatusCode() int64 { + // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 + if w.CloseStatusCode < 1000 || w.CloseStatusCode > 4999 { + return defaultCloseStatus + } + return w.CloseStatusCode } func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { @@ -254,7 +253,13 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er Success: false, ContentSize: 0, } - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -264,8 +269,8 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er }() sessionData := newSessionData() - parser := r.parser - config := r.parsedConfig + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig dummyReq := &Request{ URL: step.WebSocket.URL, @@ -302,12 +307,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er start := time.Now() // do websocket action - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { fmt.Printf("-------------------- websocket action: %v --------------------\n", step.WebSocket.Type.toString()) } switch step.WebSocket.Type { case wsOpen: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("open websocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("open websocket connection") // use the current websocket connection if existed if r.wsConnMap[parsedURL] != nil { break @@ -317,12 +322,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "open connection failed") } case wsPing: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("send ping and expect pong") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("send ping and expect pong") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "send ping message failed") } - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) // asynchronous receiving pong message with timeout go func() { select { @@ -335,7 +340,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } }() case wsWriteAndRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("write a message and read response") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("write a message and read response") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "write message failed") @@ -345,7 +350,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "read message failed") } case wsRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("read only") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("read only") resp, err = readMessageWithTimeout(parsedURL, r, step) if err != nil { return stepResult, errors.Wrap(err, "read message failed") @@ -357,7 +362,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "write message failed") } case wsClose: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("close webSocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("close webSocket connection") resp, err = closeWithTimeout(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "close connection failed") @@ -365,7 +370,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er default: return stepResult, errors.Errorf("unexpected websocket frame type: %v", step.WebSocket.Type) } - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { err = printWebSocketResponse(resp) if err != nil { return stepResult, errors.Wrap(err, "print response failed") @@ -373,7 +378,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } stepResult.Elapsed = time.Since(start).Milliseconds() - respObj, err := getResponseObject(r.hrpRunner.t, r.parser, resp) + respObj, err := getResponseObject(r.caseRunner.hrpRunner.t, r.caseRunner.parser, resp) if err != nil { err = errors.Wrap(err, "get response object error") return @@ -455,7 +460,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan := make(chan *http.Response) errorChan := make(chan error) go func() { - conn, resp, err := r.hrpRunner.wsDialer.Dial(urlStr, requestHeader) + conn, resp, err := r.caseRunner.hrpRunner.wsDialer.Dial(urlStr, requestHeader) if err != nil { errorChan <- errors.Wrap(err, "dial tcp failed") return @@ -481,7 +486,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan <- resp }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -511,7 +516,7 @@ func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*w } } }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -530,7 +535,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl } // check priority: text message > binary message if step.WebSocket.TextMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.TextMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.TextMessage, stepVariables) if parseErr != nil { return parseErr } @@ -539,7 +544,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl return writeErr } } else if step.WebSocket.BinaryMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) if parseErr != nil { return parseErr } @@ -582,7 +587,7 @@ func writeWithAction(c *websocket.Conn, step *TStep, messageType int, message [] case wsPing: return c.WriteControl(websocket.PingMessage, message, time.Now().Add(defaultWriteWait)) case wsClose: - closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.CloseStatusCode), string(message)) + closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.GetCloseStatusCode()), string(message)) return c.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(defaultWriteWait)) default: return c.WriteMessage(messageType, message) @@ -622,7 +627,7 @@ func closeWithTimeout(urlString string, r *SessionRunner, step *TStep, stepVaria // r.wsConn.Close() will be called at the end of current session, so no need to Close here log.Info().Str("msg", readErr.Error()).Msg("connection closed") }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop()