From 3085a8492a7c0e9ff26af0944d4c678a9ca87524 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sat, 9 Nov 2024 21:05:30 +0800 Subject: [PATCH] refactor: TestCase/IConfig models --- hrp/README.md | 16 +++++++++----- hrp/boomer.go | 26 ++++++++++++---------- hrp/config.go | 8 +++++++ hrp/internal/version/VERSION | 2 +- hrp/runner.go | 43 +++++++++++++++++++----------------- hrp/step_request.go | 2 +- hrp/step_shell.go | 2 +- hrp/step_testcase.go | 7 +++--- hrp/step_thinktime.go | 2 +- hrp/step_websocket.go | 4 ++-- hrp/testcase.go | 11 ++++----- 11 files changed, 71 insertions(+), 52 deletions(-) diff --git a/hrp/README.md b/hrp/README.md index 5b263ba6..6180bde0 100644 --- a/hrp/README.md +++ b/hrp/README.md @@ -6,9 +6,12 @@ HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤 ```go type TestCase struct { - Config *TConfig `json:"config" yaml:"config"` - Steps []*TStep `json:"teststeps" yaml:"teststeps"` - TestSteps []IStep `json:"-" yaml:"-"` + Config IConfig `json:"config" yaml:"config"` + TestSteps []IStep `json:"teststeps" yaml:"teststeps"` +} + +type IConfig interface { + Get() *TConfig } ``` @@ -18,7 +21,7 @@ type TestCase struct { type IStep interface { Name() string Type() StepType - Struct() *TStep + Config() *StepConfig Run(*SessionRunner) (*StepResult, error) } ``` @@ -32,6 +35,7 @@ type IStep interface { - [transaction](step_transaction.go):事务机制,用于压测 - [rendezvous](step_rendezvous.go):集合点机制,用于压测 - [mobile_UI](step_mobile_ui.go):移动端 UI 自动化 +- [shell](step_shell.go):执行 shell 命令 基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。 @@ -53,7 +57,7 @@ type HRPRunner struct { } func (r *HRPRunner) Run(testcases ...ITestCase) error -func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { +func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) ``` 重点关注两个方法: @@ -75,7 +79,7 @@ type CaseRunner struct { parametersIterator *ParametersIterator } -func (r *CaseRunner) NewSession() *SessionRunner { +func (r *CaseRunner) NewSession() *SessionRunner ``` 重点关注一个方法: diff --git a/hrp/boomer.go b/hrp/boomer.go index f4e2c899..2aeb78a9 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -138,9 +138,10 @@ func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TestCase { log.Error().Err(err).Msg("failed to create runner") os.Exit(code.GetErrorCode(err)) } - caseRunner.Config.Parameters = caseRunner.parametersIterator.outParameters() + caseConfig := caseRunner.TestCase.Config.Get() + caseConfig.Parameters = caseRunner.parametersIterator.outParameters() parsedTestCases = append(parsedTestCases, &TestCase{ - Config: caseRunner.Config, + Config: caseConfig, TestSteps: caseRunner.TestSteps, }) } @@ -184,19 +185,20 @@ func (b *HRPBoomer) parseTCases(testCases []*TestCase) (testcases []ITestCase) { return } - if tc.Config.PluginSetting != nil { - tc.Config.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", tc.Config.PluginSetting.Type)) - err = builtin.Bytes2File(tc.Config.PluginSetting.Content, tc.Config.PluginSetting.Path) + caseConfig := tc.Config.Get() + if caseConfig.PluginSetting != nil { + caseConfig.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", caseConfig.PluginSetting.Type)) + err = builtin.Bytes2File(caseConfig.PluginSetting.Content, caseConfig.PluginSetting.Path) if err != nil { log.Error().Err(err).Msg("failed to save plugin file") return } - tc.Config.PluginSetting.Content = nil // remove the content in testcase + caseConfig.PluginSetting.Content = nil // remove the content in testcase } - if tc.Config.Environs != nil { + if caseConfig.Environs != nil { envContent := "" - for k, v := range tc.Config.Environs { + for k, v := range caseConfig.Environs { envContent += fmt.Sprintf("%s=%s\n", k, v) } err = os.WriteFile(filepath.Join(tempDir, ".env"), []byte(envContent), 0o644) @@ -206,8 +208,8 @@ func (b *HRPBoomer) parseTCases(testCases []*TestCase) (testcases []ITestCase) { } } - tc.Config.Path = filepath.Join(tempDir, "test-case.json") - err = builtin.Dump2JSON(tc, tc.Config.Path) + caseConfig.Path = filepath.Join(tempDir, "test-case.json") + err = builtin.Dump2JSON(tc, caseConfig.Path) if err != nil { log.Error().Err(err).Msg("failed to dump testcases") return @@ -335,8 +337,8 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend mutex := sync.Mutex{} return &boomer.Task{ - Name: testcase.Config.Name, - Weight: testcase.Config.Weight, + Name: testcase.Config.Get().Name, + Weight: testcase.Config.Get().Weight, Fn: func() { testcaseSuccess := true // flag whole testcase result transactionSuccess := true // flag current transaction result diff --git a/hrp/config.go b/hrp/config.go index b90f18a7..85b63568 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -7,6 +7,10 @@ import ( "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) +type IConfig interface { + Get() *TConfig +} + // NewConfig returns a new constructed testcase config with specified testcase name. func NewConfig(name string) *TConfig { return &TConfig{ @@ -39,6 +43,10 @@ type TConfig struct { PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config } +func (c *TConfig) Get() *TConfig { + return c +} + // WithVariables sets variables for current testcase. func (c *TConfig) WithVariables(variables map[string]interface{}) *TConfig { c.Variables = variables diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index c1d48e7f..77ffe6e2 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2411092015 +v5.0.0+2411092105 diff --git a/hrp/runner.go b/hrp/runner.go index 6d379ee9..d97ac505 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -282,9 +282,10 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { hrpRunner: r, parser: newParser(), } + config := testcase.Config.Get() // init parser plugin - plugin, err := initPlugin(testcase.Config.Path, r.venv, r.pluginLogOn) + plugin, err := initPlugin(config.Path, r.venv, r.pluginLogOn) if err != nil { return nil, errors.Wrap(err, "init plugin failed") } @@ -297,27 +298,26 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { if err != nil { return nil, errors.Wrap(err, "parse testcase config failed") } - caseRunner.TestCase.Config = parsedConfig // set request timeout in seconds - if testcase.Config.RequestTimeout != 0 { - r.SetRequestTimeout(testcase.Config.RequestTimeout) + if config.RequestTimeout != 0 { + r.SetRequestTimeout(config.RequestTimeout) } // set testcase timeout in seconds - if testcase.Config.CaseTimeout != 0 { - r.SetCaseTimeout(testcase.Config.CaseTimeout) + if config.CaseTimeout != 0 { + r.SetCaseTimeout(config.CaseTimeout) } // load plugin info to testcase config if plugin != nil { - pluginPath, _ := locatePlugin(testcase.Config.Path) - if caseRunner.Config.PluginSetting == nil { + pluginPath, _ := locatePlugin(config.Path) + if parsedConfig.PluginSetting == nil { pluginContent, err := readFile(pluginPath) if err != nil { return nil, err } tp := strings.Split(plugin.Path(), ".") - caseRunner.Config.PluginSetting = &PluginConfig{ + parsedConfig.PluginSetting = &PluginConfig{ Path: pluginPath, Content: pluginContent, Type: tp[len(tp)-1], @@ -325,6 +325,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) { } } + caseRunner.TestCase.Config = parsedConfig return caseRunner, nil } @@ -339,7 +340,7 @@ type CaseRunner struct { // parseConfig parses testcase config, stores to parsedConfig. func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { - cfg := r.TestCase.Config + cfg := r.TestCase.Config.Get() parsedConfig = &TConfig{} // deep copy config to avoid data racing @@ -541,7 +542,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa // report GA event sdk.SendGA4Event("hrp_session_runner_start", nil) - config := r.caseRunner.Config + config := r.caseRunner.TestCase.Config.Get() log.Info().Str("testcase", config.Name).Msg("run testcase start") // update config variables with given variables @@ -552,14 +553,14 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa r.releaseResources() summary = r.summary - summary.Name = r.caseRunner.Config.Name + summary.Name = config.Name summary.Time.Duration = time.Since(summary.Time.StartAt).Seconds() exportVars := make(map[string]interface{}) - for _, value := range r.caseRunner.Config.Export { + for _, value := range config.Export { exportVars[value] = r.sessionVariables[value] } summary.InOut.ExportVars = exportVars - summary.InOut.ConfigVars = r.caseRunner.Config.Variables + summary.InOut.ConfigVars = config.Variables // TODO: move to mobile ui step for uuid, client := range uiClients { @@ -682,18 +683,19 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa } func (r *SessionRunner) parseStep(step IStep) error { + caseConfig := r.caseRunner.TestCase.Config.Get() stepConfig := step.Config() // update step variables: merges step variables with config variables and session variables // variables priority: step variables > session variables (extracted variables from previous steps) overrideVars := mergeVariables(stepConfig.Variables, r.sessionVariables) // step variables > testcase config variables - overrideVars = mergeVariables(overrideVars, r.caseRunner.Config.Variables) + overrideVars = mergeVariables(overrideVars, caseConfig.Variables) // parse step variables parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars) if err != nil { - log.Error().Interface("variables", r.caseRunner.Config.Variables). + log.Error().Interface("variables", caseConfig.Variables). Err(err).Msg("parse step variables failed") return errors.Wrap(err, "parse step variables failed") } @@ -750,11 +752,12 @@ func (r *SessionRunner) initWithParameters(parameters map[string]interface{}) { } func (r *SessionRunner) IgnorePopup() bool { - if r.caseRunner.TestCase.Config.Android != nil { - return r.caseRunner.TestCase.Config.Android[0].IgnorePopup + caseConfig := r.caseRunner.TestCase.Config.Get() + if caseConfig.Android != nil { + return caseConfig.Android[0].IgnorePopup } - if r.caseRunner.TestCase.Config.IOS != nil { - return r.caseRunner.TestCase.Config.IOS[0].IgnorePopup + if caseConfig.IOS != nil { + return caseConfig.IOS[0].IgnorePopup } return false } diff --git a/hrp/step_request.go b/hrp/step_request.go index 56719f9a..9de8f472 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -299,7 +299,7 @@ func runStepRequest(r *SessionRunner, step IStep) (stepResult *StepResult, err e sessionData := newSessionData() parser := r.caseRunner.parser - config := r.caseRunner.Config + config := r.caseRunner.Config.Get() rb := newRequestBuilder(parser, config, stepRequest.Request) rb.req.Method = strings.ToUpper(string(stepRequest.Request.Method)) diff --git a/hrp/step_shell.go b/hrp/step_shell.go index 5a3ba692..11a722cb 100644 --- a/hrp/step_shell.go +++ b/hrp/step_shell.go @@ -103,7 +103,7 @@ func runStepShell(r *SessionRunner, step IStep) (stepResult *StepResult, err err ContentSize: 0, } - vars := r.caseRunner.Config.Variables + vars := r.caseRunner.Config.Get().Variables for key, value := range vars { os.Setenv(key, fmt.Sprintf("%v", value)) } diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index dce4f7e5..1d319da7 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -32,7 +32,7 @@ func (s *StepTestCaseWithOptionalArgs) Name() string { } ts, ok := s.TestCase.(*TestCase) if ok { - return ts.Config.Name + return ts.Config.Get().Name } return "" } @@ -68,13 +68,14 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe return stepResult, err } + config := copiedTestCase.Config.Get() // override testcase config // override testcase name if s.StepName != "" { - copiedTestCase.Config.Name = s.StepName + config.Name = s.StepName } // merge & override extractors - copiedTestCase.Config.Export = mergeSlices(s.StepExport, copiedTestCase.Config.Export) + config.Export = mergeSlices(s.StepExport, config.Export) caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(*copiedTestCase) if err != nil { diff --git a/hrp/step_thinktime.go b/hrp/step_thinktime.go index 01c21395..9a22cac0 100644 --- a/hrp/step_thinktime.go +++ b/hrp/step_thinktime.go @@ -40,7 +40,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { Success: true, } - cfg := r.caseRunner.Config.ThinkTimeSetting + cfg := r.caseRunner.Config.Get().ThinkTimeSetting if cfg == nil { cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} } diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index ebd5cc0a..ddf3dfb0 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -289,7 +289,7 @@ func runStepWebSocket(r *SessionRunner, step IStep) (stepResult *StepResult, err sessionData := newSessionData() parser := r.caseRunner.parser - config := r.caseRunner.Config + config := r.caseRunner.Config.Get() dummyReq := &Request{ URL: webSocket.URL, @@ -706,7 +706,7 @@ func (r *SessionRunner) releaseResources() { // close websocket connections for _, wsConn := range r.ws.wsConnMap { if wsConn != nil { - log.Info().Str("testcase", r.caseRunner.Config.Name).Msg("websocket disconnected") + log.Info().Str("testcase", r.caseRunner.Config.Get().Name).Msg("websocket disconnected") err := wsConn.Close() if err != nil { log.Error().Err(err).Msg("websocket disconnection failed") diff --git a/hrp/testcase.go b/hrp/testcase.go index 93693f30..8fc5649b 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -46,8 +46,8 @@ func (path *TestCasePath) GetTestCase() (*TestCase, error) { // TestCase is a container for one testcase, which is used for testcase runner. // TestCase implements ITestCase interface. type TestCase struct { - Config *TConfig `json:"config" yaml:"config"` - TestSteps []IStep `json:"teststeps" yaml:"teststeps"` + Config IConfig `json:"config" yaml:"config"` + TestSteps []IStep `json:"teststeps" yaml:"teststeps"` } func (tc *TestCase) GetTestCase() (*TestCase, error) { @@ -126,13 +126,14 @@ func (tc *TestCaseDef) loadISteps() (*TestCase, error) { return nil, errors.Wrap(err, "failed to load .env file") } + config := testCase.Config.Get() // override testcase config env with variables loaded from .env file // priority: .env file > testcase config env - if testCase.Config.Environs == nil { - testCase.Config.Environs = make(map[string]string) + if config.Environs == nil { + config.Environs = make(map[string]string) } for key, value := range envVars { - testCase.Config.Environs[key] = value + config.Environs[key] = value } }