refactor: CaseRunner, parseConfig

This commit is contained in:
lilong.129
2024-08-20 12:09:15 +08:00
parent 9efbd04055
commit 263ee681be
8 changed files with 70 additions and 124 deletions
+63 -117
View File
@@ -10,7 +10,6 @@ import (
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
@@ -23,11 +22,9 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version" "github.com/httprunner/httprunner/v4/hrp/internal/version"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
) )
// Run starts to run testcase with default configs. // Run starts to run testcase with default configs.
@@ -227,19 +224,12 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
// run testcase one by one // run testcase one by one
for _, testcase := range testCases { for _, testcase := range testCases {
// each testcase has its own case runner // each testcase has its own case runner
caseRunner, err := r.NewCaseRunner(testcase) caseRunner, err := r.NewCaseRunner(*testcase)
if err != nil { if err != nil {
log.Error().Err(err).Msg("[Run] init case runner failed") log.Error().Err(err).Msg("[Run] init case runner failed")
return err return err
} }
// release UI driver session
defer func() {
for _, client := range caseRunner.uiClients {
client.Driver.DeleteSession()
}
}()
for it := caseRunner.parametersIterator; it.HasNext(); { for it := caseRunner.parametersIterator; it.HasNext(); {
// case runner can run multiple times with different parameters // case runner can run multiple times with different parameters
// each run has its own session runner // each run has its own session runner
@@ -286,9 +276,9 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
// NewCaseRunner creates a new case runner for testcase. // NewCaseRunner creates a new case runner for testcase.
// each testcase has its own case runner // each testcase has its own case runner
func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) { func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
caseRunner := &CaseRunner{ caseRunner := &CaseRunner{
testCase: testcase, TestCase: testcase,
hrpRunner: r, hrpRunner: r,
parser: newParser(), parser: newParser(),
} }
@@ -300,13 +290,14 @@ func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) {
} }
if plugin != nil { if plugin != nil {
caseRunner.parser.plugin = plugin caseRunner.parser.plugin = plugin
caseRunner.rootDir = filepath.Dir(plugin.Path())
} }
// parse testcase config // parse testcase config
if err := caseRunner.parseConfig(); err != nil { parsedConfig, err := caseRunner.parseConfig()
if err != nil {
return nil, errors.Wrap(err, "parse testcase config failed") return nil, errors.Wrap(err, "parse testcase config failed")
} }
caseRunner.TestCase.Config = parsedConfig
// set request timeout in seconds // set request timeout in seconds
if testcase.Config.RequestTimeout != 0 { if testcase.Config.RequestTimeout != 0 {
@@ -320,13 +311,13 @@ func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) {
// load plugin info to testcase config // load plugin info to testcase config
if plugin != nil { if plugin != nil {
pluginPath, _ := locatePlugin(testcase.Config.Path) pluginPath, _ := locatePlugin(testcase.Config.Path)
if caseRunner.parsedConfig.PluginSetting == nil { if caseRunner.Config.PluginSetting == nil {
pluginContent, err := builtin.ReadFile(pluginPath) pluginContent, err := readFile(pluginPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tp := strings.Split(plugin.Path(), ".") tp := strings.Split(plugin.Path(), ".")
caseRunner.parsedConfig.PluginSetting = &PluginConfig{ caseRunner.Config.PluginSetting = &PluginConfig{
Path: pluginPath, Path: pluginPath,
Content: pluginContent, Content: pluginContent,
Type: tp[len(tp)-1], Type: tp[len(tp)-1],
@@ -338,130 +329,84 @@ func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) {
} }
type CaseRunner struct { type CaseRunner struct {
TestCase // each testcase init its own CaseRunner
hrpRunner *HRPRunner // all case runners share one HRPRunner hrpRunner *HRPRunner // all case runners share one HRPRunner
parser *Parser // each CaseRunner init its own Parser
testCase *TestCase // each testcase init its own CaseRunner
parser *Parser // each CaseRunner init its own Parser
parsedConfig *TConfig
parametersIterator *ParametersIterator parametersIterator *ParametersIterator
rootDir string // project root dir
uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial
} }
// parseConfig parses testcase config, stores to parsedConfig. // parseConfig parses testcase config, stores to parsedConfig.
func (r *CaseRunner) parseConfig() error { func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
cfg := r.testCase.Config cfg := r.TestCase.Config
r.parsedConfig = &TConfig{} parsedConfig = &TConfig{}
// deep copy config to avoid data racing // deep copy config to avoid data racing
if err := copier.Copy(r.parsedConfig, cfg); err != nil { if err := copier.Copy(parsedConfig, cfg); err != nil {
log.Error().Err(err).Msg("copy testcase config failed") log.Error().Err(err).Msg("copy testcase config failed")
return err return nil, err
} }
// parse config variables // parse config variables
parsedVariables, err := r.parser.ParseVariables(cfg.Variables) parsedVariables, err := r.parser.ParseVariables(cfg.Variables)
if err != nil { if err != nil {
log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed") log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed")
return err return nil, err
} }
r.parsedConfig.Variables = parsedVariables parsedConfig.Variables = parsedVariables
// parse config name // parse config name
parsedName, err := r.parser.ParseString(cfg.Name, parsedVariables) parsedName, err := r.parser.ParseString(cfg.Name, parsedVariables)
if err != nil { if err != nil {
return errors.Wrap(err, "parse config name failed") return nil, errors.Wrap(err, "parse config name failed")
} }
r.parsedConfig.Name = convertString(parsedName) parsedConfig.Name = convertString(parsedName)
// parse config base url // parse config base url
parsedBaseURL, err := r.parser.ParseString(cfg.BaseURL, parsedVariables) parsedBaseURL, err := r.parser.ParseString(cfg.BaseURL, parsedVariables)
if err != nil { if err != nil {
return errors.Wrap(err, "parse config base url failed") return nil, errors.Wrap(err, "parse config base url failed")
} }
r.parsedConfig.BaseURL = convertString(parsedBaseURL) parsedConfig.BaseURL = convertString(parsedBaseURL)
// merge config environment variables with base_url // merge config environment variables with base_url
// priority: env base_url > base_url // priority: env base_url > base_url
if cfg.Environs != nil { if cfg.Environs != nil {
r.parsedConfig.Environs = cfg.Environs parsedConfig.Environs = cfg.Environs
} else { } else {
r.parsedConfig.Environs = make(map[string]string) parsedConfig.Environs = make(map[string]string)
} }
if value, ok := r.parsedConfig.Environs["base_url"]; !ok || value == "" { if value, ok := parsedConfig.Environs["base_url"]; !ok || value == "" {
if r.parsedConfig.BaseURL != "" { if parsedConfig.BaseURL != "" {
r.parsedConfig.Environs["base_url"] = r.parsedConfig.BaseURL parsedConfig.Environs["base_url"] = parsedConfig.BaseURL
} }
} }
// merge config variables with environment variables // merge config variables with environment variables
// priority: env > config variables // priority: env > config variables
for k, v := range r.parsedConfig.Environs { for k, v := range parsedConfig.Environs {
r.parsedConfig.Variables[k] = v parsedConfig.Variables[k] = v
} }
// ensure correction of think time config // ensure correction of think time config
r.parsedConfig.ThinkTimeSetting.checkThinkTime() parsedConfig.ThinkTimeSetting.checkThinkTime()
// ensure correction of websocket config // ensure correction of websocket config
r.parsedConfig.WebSocketSetting.checkWebSocket() parsedConfig.WebSocketSetting.checkWebSocket()
// parse testcase config parameters // parse testcase config parameters
parametersIterator, err := r.parser.initParametersIterator(r.parsedConfig) parametersIterator, err := r.parser.initParametersIterator(parsedConfig)
if err != nil { if err != nil {
log.Error().Err(err). log.Error().Err(err).
Interface("parameters", r.parsedConfig.Parameters). Interface("parameters", parsedConfig.Parameters).
Interface("parametersSetting", r.parsedConfig.ParametersSetting). Interface("parametersSetting", parsedConfig.ParametersSetting).
Msg("parse config parameters failed") Msg("parse config parameters failed")
return errors.Wrap(err, "parse testcase config parameters failed") return nil, errors.Wrap(err, "parse testcase config parameters failed")
} }
r.parametersIterator = parametersIterator r.parametersIterator = parametersIterator
// init iOS/Android clients return parsedConfig, nil
if r.uiClients == nil {
r.uiClients = make(map[string]*uixt.DriverExt)
}
for _, iosDeviceConfig := range r.parsedConfig.IOS {
if iosDeviceConfig.UDID != "" {
udid, err := r.parser.ParseString(iosDeviceConfig.UDID, parsedVariables)
if err != nil {
return errors.Wrap(err, "failed to parse ios device udid")
}
iosDeviceConfig.UDID = udid.(string)
}
device, err := uixt.NewIOSDevice(uixt.GetIOSDeviceOptions(iosDeviceConfig)...)
if err != nil {
return errors.Wrap(err, "init iOS device failed")
}
client, err := device.NewDriver(uixt.WithDriverPlugin(r.parser.plugin))
if err != nil {
return errors.Wrap(err, "init iOS WDA client failed")
}
r.uiClients[device.UDID] = client
}
for _, androidDeviceConfig := range r.parsedConfig.Android {
if androidDeviceConfig.SerialNumber != "" {
sn, err := r.parser.ParseString(androidDeviceConfig.SerialNumber, parsedVariables)
if err != nil {
return errors.Wrap(err, "failed to parse android device serial")
}
androidDeviceConfig.SerialNumber = sn.(string)
}
device, err := uixt.NewAndroidDevice(uixt.GetAndroidDeviceOptions(androidDeviceConfig)...)
if err != nil {
return errors.Wrap(err, "init Android device failed")
}
client, err := device.NewDriver(uixt.WithDriverPlugin(r.parser.plugin))
if err != nil {
return errors.Wrap(err, "init Android client failed")
}
r.uiClients[device.SerialNumber] = client
}
return nil
} }
// each boomer task initiates a new session // each boomer task initiates a new session
@@ -520,7 +465,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
// report GA event // report GA event
sdk.SendGA4Event("hrp_session_runner_start", nil) sdk.SendGA4Event("hrp_session_runner_start", nil)
config := r.caseRunner.testCase.Config config := r.caseRunner.Config
log.Info().Str("testcase", config.Name).Msg("run testcase start") log.Info().Str("testcase", config.Name).Msg("run testcase start")
// update config variables with given variables // update config variables with given variables
@@ -532,7 +477,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
}() }()
// run step in sequential order // run step in sequential order
for _, step := range r.caseRunner.testCase.TestSteps { for _, step := range r.caseRunner.TestSteps {
select { select {
case <-r.caseRunner.hrpRunner.caseTimeoutTimer.C: case <-r.caseRunner.hrpRunner.caseTimeoutTimer.C:
log.Warn().Msg("timeout in session runner") log.Warn().Msg("timeout in session runner")
@@ -626,12 +571,12 @@ func (r *SessionRunner) ParseStepVariables(stepVariables map[string]interface{})
// step variables > session variables (extracted variables from previous steps) // step variables > session variables (extracted variables from previous steps)
overrideVars := mergeVariables(stepVariables, r.sessionVariables) overrideVars := mergeVariables(stepVariables, r.sessionVariables)
// step variables > testcase config variables // step variables > testcase config variables
overrideVars = mergeVariables(overrideVars, r.caseRunner.parsedConfig.Variables) overrideVars = mergeVariables(overrideVars, r.caseRunner.Config.Variables)
// parse step variables // parse step variables
parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars) parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars)
if err != nil { if err != nil {
log.Error().Interface("variables", r.caseRunner.parsedConfig.Variables). log.Error().Interface("variables", r.caseRunner.Config.Variables).
Err(err).Msg("parse step variables failed") Err(err).Msg("parse step variables failed")
return nil, errors.Wrap(err, "parse step variables failed") return nil, errors.Wrap(err, "parse step variables failed")
} }
@@ -670,36 +615,37 @@ func (r *SessionRunner) InitWithParameters(parameters map[string]interface{}) {
func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) {
caseSummary := r.summary caseSummary := r.summary
caseSummary.Name = r.caseRunner.parsedConfig.Name caseSummary.Name = r.caseRunner.Config.Name
caseSummary.Time.StartAt = r.startTime caseSummary.Time.StartAt = r.startTime
caseSummary.Time.Duration = time.Since(r.startTime).Seconds() caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
exportVars := make(map[string]interface{}) exportVars := make(map[string]interface{})
for _, value := range r.caseRunner.parsedConfig.Export { for _, value := range r.caseRunner.Config.Export {
exportVars[value] = r.sessionVariables[value] exportVars[value] = r.sessionVariables[value]
} }
caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ExportVars = exportVars
caseSummary.InOut.ConfigVars = r.caseRunner.parsedConfig.Variables caseSummary.InOut.ConfigVars = r.caseRunner.Config.Variables
for uuid, client := range r.caseRunner.uiClients { // TODO: move to mobile ui step
// add WDA/UIA logs to summary // for uuid, client := range r.caseRunner.uiClients {
logs := map[string]interface{}{ // // add WDA/UIA logs to summary
"uuid": uuid, // logs := map[string]interface{}{
} // "uuid": uuid,
// }
if client.Device.LogEnabled() { // if client.Device.LogEnabled() {
log, err := client.Driver.StopCaptureLog() // log, err := client.Driver.StopCaptureLog()
if err != nil { // if err != nil {
return caseSummary, err // return caseSummary, err
} // }
logs["content"] = log // logs["content"] = log
} // }
// stop performance monitor // // stop performance monitor
logs["performance"] = client.Device.StopPerf() // logs["performance"] = client.Device.StopPerf()
logs["pcap"] = client.Device.StopPcap() // logs["pcap"] = client.Device.StopPcap()
caseSummary.Logs = append(caseSummary.Logs, logs) // caseSummary.Logs = append(caseSummary.Logs, logs)
} // }
return caseSummary, nil return caseSummary, nil
} }
@@ -739,7 +685,7 @@ func (r *SessionRunner) releaseResources() {
// close websocket connections // close websocket connections
for _, wsConn := range r.wsConnMap { for _, wsConn := range r.wsConnMap {
if wsConn != nil { if wsConn != nil {
log.Info().Str("testcase", r.caseRunner.testCase.Config.Name).Msg("websocket disconnected") log.Info().Str("testcase", r.caseRunner.Config.Name).Msg("websocket disconnected")
err := wsConn.Close() err := wsConn.Close()
if err != nil { if err != nil {
log.Error().Err(err).Msg("websocket disconnection failed") log.Error().Err(err).Msg("websocket disconnection failed")
+1 -1
View File
@@ -44,7 +44,7 @@ func (s *StepRendezvous) Run(r *SessionRunner) (*StepResult, error) {
} }
// pass current rendezvous if already released, activate rendezvous sequentially after spawn done // pass current rendezvous if already released, activate rendezvous sequentially after spawn done
if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.caseRunner.testCase) || !rendezvous.isSpawnDone() { if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, &r.caseRunner.TestCase) || !rendezvous.isSpawnDone() {
return stepResult, nil return stepResult, nil
} }
+1 -1
View File
@@ -305,7 +305,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
sessionData := newSessionData() sessionData := newSessionData()
parser := r.caseRunner.parser parser := r.caseRunner.parser
config := r.caseRunner.parsedConfig config := r.caseRunner.Config
rb := newRequestBuilder(parser, config, step.Request) rb := newRequestBuilder(parser, config, step.Request)
rb.req.Method = strings.ToUpper(string(step.Request.Method)) rb.req.Method = strings.ToUpper(string(step.Request.Method))
+1 -1
View File
@@ -78,7 +78,7 @@ func TestRunRequestPostDataToStruct(t *testing.T) {
} }
func TestRunRequestStatOn(t *testing.T) { func TestRunRequestStatOn(t *testing.T) {
testcase := &TestCase{ testcase := TestCase{
Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), Config: NewConfig("test").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{stepGET, stepPOSTData}, TestSteps: []IStep{stepGET, stepPOSTData},
} }
+1 -1
View File
@@ -85,7 +85,7 @@ func runStepShell(r *SessionRunner, step *TStep) (stepResult *StepResult, err er
ContentSize: 0, ContentSize: 0,
} }
vars := r.caseRunner.parsedConfig.Variables vars := r.caseRunner.Config.Variables
for key, value := range vars { for key, value := range vars {
os.Setenv(key, fmt.Sprintf("%v", value)) os.Setenv(key, fmt.Sprintf("%v", value))
} }
+1 -1
View File
@@ -69,7 +69,7 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe
stepTestCase := s.step.TestCase.(*TestCase) stepTestCase := s.step.TestCase.(*TestCase)
// copy testcase to avoid data racing // copy testcase to avoid data racing
copiedTestCase := &TestCase{} copiedTestCase := TestCase{}
if err := copier.Copy(copiedTestCase, stepTestCase); err != nil { if err := copier.Copy(copiedTestCase, stepTestCase); err != nil {
log.Error().Err(err).Msg("copy step testcase failed") log.Error().Err(err).Msg("copy step testcase failed")
return stepResult, err return stepResult, err
+1 -1
View File
@@ -39,7 +39,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
Success: true, Success: true,
} }
cfg := r.caseRunner.parsedConfig.ThinkTimeSetting cfg := r.caseRunner.Config.ThinkTimeSetting
if cfg == nil { if cfg == nil {
cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
} }
+1 -1
View File
@@ -270,7 +270,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er
sessionData := newSessionData() sessionData := newSessionData()
parser := r.caseRunner.parser parser := r.caseRunner.parser
config := r.caseRunner.parsedConfig config := r.caseRunner.Config
dummyReq := &Request{ dummyReq := &Request{
URL: step.WebSocket.URL, URL: step.WebSocket.URL,