fix: load testcase panic caused by config options

This commit is contained in:
lilong.129
2025-04-27 11:50:50 +08:00
parent 5385556b66
commit d2976844fc
7 changed files with 134 additions and 222 deletions

View File

@@ -4,7 +4,6 @@ import (
"reflect"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
)
@@ -23,26 +22,26 @@ func NewConfig(name string) *TConfig {
// define struct for testcase config
type TConfig struct {
Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` // deprecated in v4.1, moved to env
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // public request headers
Environs map[string]string `json:"environs,omitempty" yaml:"environs,omitempty"` // environment variables
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"`
IOS []*uixt.IOSDevice `json:"ios,omitempty" yaml:"ios,omitempty"`
Android []*uixt.AndroidDevice `json:"android,omitempty" yaml:"android,omitempty"`
Harmony []*uixt.HarmonyDevice `json:"harmony,omitempty" yaml:"harmony,omitempty"`
RequestTimeout float32 `json:"request_timeout,omitempty" yaml:"request_timeout,omitempty"` // request timeout in seconds
CaseTimeout float32 `json:"case_timeout,omitempty" yaml:"case_timeout,omitempty"` // testcase timeout in seconds
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
Name string `json:"name" yaml:"name"` // required
Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"`
BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty"` // deprecated in v4.1, moved to env
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // public request headers
Environs map[string]string `json:"environs,omitempty" yaml:"environs,omitempty"` // environment variables
Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` // global variables
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"`
IOS []*option.IOSDeviceOptions `json:"ios,omitempty" yaml:"ios,omitempty"`
Android []*option.AndroidDeviceOptions `json:"android,omitempty" yaml:"android,omitempty"`
Harmony []*option.HarmonyDeviceOptions `json:"harmony,omitempty" yaml:"harmony,omitempty"`
RequestTimeout float32 `json:"request_timeout,omitempty" yaml:"request_timeout,omitempty"` // request timeout in seconds
CaseTimeout float32 `json:"case_timeout,omitempty" yaml:"case_timeout,omitempty"` // testcase timeout in seconds
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` // testcase file path
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
}
func (c *TConfig) Get() *TConfig {
@@ -120,63 +119,54 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig {
func (c *TConfig) SetIOS(opts ...option.IOSDeviceOption) *TConfig {
iosOptions := option.NewIOSDeviceOptions(opts...)
device := &uixt.IOSDevice{
Options: iosOptions,
}
// each device can have its own settings
if iosOptions.UDID != "" {
c.IOS = append(c.IOS, device)
c.IOS = append(c.IOS, iosOptions)
return c
}
// device UDID is not specified, settings will be shared
if len(c.IOS) == 0 {
c.IOS = append(c.IOS, device)
c.IOS = append(c.IOS, iosOptions)
} else {
c.IOS[0] = device
c.IOS[0] = iosOptions
}
return c
}
func (c *TConfig) SetHarmony(opts ...option.HarmonyDeviceOption) *TConfig {
harmonyOptions := option.NewHarmonyDeviceOptions(opts...)
device := &uixt.HarmonyDevice{
Options: harmonyOptions,
}
// each device can have its own settings
if harmonyOptions.ConnectKey != "" {
c.Harmony = append(c.Harmony, device)
c.Harmony = append(c.Harmony, harmonyOptions)
return c
}
// device UDID is not specified, settings will be shared
if len(c.Harmony) == 0 {
c.Harmony = append(c.Harmony, device)
c.Harmony = append(c.Harmony, harmonyOptions)
} else {
c.Harmony[0] = device
c.Harmony[0] = harmonyOptions
}
return c
}
func (c *TConfig) SetAndroid(opts ...option.AndroidDeviceOption) *TConfig {
uiaOptions := option.NewAndroidDeviceOptions(opts...)
device := &uixt.AndroidDevice{
Options: uiaOptions,
}
// each device can have its own settings
if uiaOptions.SerialNumber != "" {
c.Android = append(c.Android, device)
c.Android = append(c.Android, uiaOptions)
return c
}
// device UDID is not specified, settings will be shared
if len(c.Android) == 0 {
c.Android = append(c.Android, device)
c.Android = append(c.Android, uiaOptions)
} else {
c.Android[0] = device
c.Android[0] = uiaOptions
}
return c
}

View File

@@ -1,6 +1,6 @@
{
"config": {
"name": "播_抖极_feed卡片_android",
"name": "播_抖音_滑动场景_随机间隔_android",
"variables": {
"device": "${ENV(SerialNumber)}"
},
@@ -8,91 +8,32 @@
{
"serial": "$device",
"log_on": true,
"close_popup": true
"adb_server_host": "localhost",
"adb_server_port": 5037,
"uia2_ip": "localhost",
"uia2_port": 6790,
"uia2_server_package_name": "io.appium.uiautomator2.server",
"uia2_server_test_package_name": "io.appium.uiautomator2.server.test"
}
]
},
"teststeps": [
{
"name": "清理android无关进程",
"name": "启动抖音",
"android": {
"os_type": "android",
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme"
},
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme.lite"
},
{
"method": "app_terminate",
"params": "com.smile.gifmaker"
},
{
"method": "app_terminate",
"params": "com.kuaishou.nebula"
},
{
"method": "app_terminate",
"params": "com.tencent.mm"
},
{
"method": "app_terminate",
"params": "com.duowan.kiwi"
},
{
"method": "app_terminate",
"params": "air.tv.douyu.android"
},
{
"method": "app_terminate",
"params": "com.xingin.xhs"
},
{
"method": "app_terminate",
"params": "com.taobao.taobao"
},
{
"method": "app_terminate",
"params": "tv.danmaku.bili"
},
{
"method": "app_terminate",
"params": "com.cmcc.cmvideo"
},
{
"method": "app_terminate",
"params": "com.xunmeng.pinduoduo"
},
{
"method": "app_terminate",
"params": "com.cctv.yangshipin.app.androidp"
}
]
}
},
{
"name": "启动抖音极速版",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme.lite"
},
{
"method": "app_launch",
"params": "com.ss.android.ugc.aweme.lite"
"params": "com.ss.android.ugc.aweme"
},
{
"method": "sleep",
"params": 10
},
{
"method": "close_popups",
"options": {
"max_retry_times": 2
}
}
]
},
@@ -100,139 +41,119 @@
{
"check": "ui_foreground_app",
"assert": "equal",
"expect": "com.ss.android.ugc.aweme.lite",
"msg": "app [com.ss.android.ugc.aweme.lite] should be in foreground"
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
}
]
},
{
"name": "处理通讯录弹窗",
"android": {
"actions": [
{
"method": "tap_ocr",
"params": "拒绝",
"ignore_NotFoundError": true
}
]
}
},
{
"name": "处理青少年弹窗",
"android": {
"os_type": "android",
"actions": [
{
"method": "sleep",
"params": 5
},
{
"method": "tap_ocr",
"params": "我知道了",
"ignore_NotFoundError": true
"options": {
"max_retry_times": 1,
"ignore_NotFoundError": true
}
}
]
}
},
{
"name": "点击直播标签,进入直播间",
"android": {
"actions": [
{
"method": "swipe_to_tap_texts",
"params": [
"看直播开宝箱",
"最高领",
"点击进入直播间"
],
"identifier": "click_live_new",
"max_retry_times": 40,
"wait_time": 2,
"direction": [
0.5,
0.8,
0.5,
0.2
],
"scope": [
0.1,
0.5,
0.9,
0.9
],
"offset": [
0,
-100
]
}
]
}
},
{
"name": "等待30s",
"android": {
"actions": [
{
"method": "sleep",
"params": 30
}
]
}
},
{
"name": "下滑进入下一个直播间",
"name": "滑动 Feed 3 次,随机间隔 0-5s",
"loops": 3,
"android": {
"os_type": "android",
"actions": [
{
"method": "swipe",
"params": [
0.5,
0.7,
0.5,
0.1
],
"identifier": "slide_in_live_new"
"params": "up",
"options": {
"max_retry_times": 1
}
},
{
"method": "sleep",
"params": 30
"method": "sleep_random",
"params": [
0,
5
]
}
]
}
},
{
"name": "返回主界面,并打开本地时间戳",
"name": "滑动 Feed 1 次,随机间隔 5-10s",
"loops": 1,
"android": {
"os_type": "android",
"actions": [
{
"method": "swipe",
"params": "up",
"options": {
"max_retry_times": 1
}
},
{
"method": "sleep_random",
"params": [
5,
10
]
}
]
}
},
{
"name": "滑动 Feed 10 次70% 随机间隔 0-5s30% 随机间隔 5-10s",
"loops": 10,
"android": {
"os_type": "android",
"actions": [
{
"method": "swipe",
"params": "up",
"options": {
"max_retry_times": 1
}
},
{
"method": "sleep_random",
"params": [
0,
5,
0.7,
5,
10,
0.3
]
}
]
}
},
{
"name": "exit",
"android": {
"os_type": "android",
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme.lite"
},
{
"method": "home"
},
{
"method": "swipe_to_tap_app",
"params": "local",
"max_retry_times": 5,
"offset": [
0,
-50
]
},
{
"method": "sleep",
"params": 10
"params": "com.ss.android.ugc.aweme"
}
]
},
"validate": [
{
"check": "ui_ocr",
"assert": "exists",
"expect": "17",
"msg": "打开本地时间戳失败"
"check": "ui_foreground_app",
"assert": "not_equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
}
]
}
]
}
}

View File

@@ -15,7 +15,8 @@ func TestAndroidDouyinFeedTest(t *testing.T) {
WithVariables(map[string]interface{}{
"device": "${ENV(SerialNumber)}",
}).
SetAndroid(option.WithSerialNumber("$device")),
SetAndroid(option.WithSerialNumber("$device"),
option.WithAdbLogOn(true)),
TestSteps: []hrp.IStep{
hrp.NewStep("启动抖音").
Android().

View File

@@ -1 +1 @@
v5.0.0-beta-2504251755
v5.0.0-beta-2504271150

View File

@@ -419,14 +419,14 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
r.parametersIterator = parametersIterator
// parse android devices config
for _, androidDevice := range parsedConfig.Android {
err := r.parseDeviceConfig(androidDevice, parsedConfig.Variables)
for _, androidDeviceOptions := range parsedConfig.Android {
err := r.parseDeviceConfig(androidDeviceOptions, parsedConfig.Variables)
if err != nil {
return nil, errors.Wrap(code.InvalidCaseError,
fmt.Sprintf("parse android config failed: %v", err))
}
device, err := uixt.NewAndroidDevice(androidDevice.Options.Options()...)
device, err := uixt.NewAndroidDevice(androidDeviceOptions.Options()...)
if err != nil {
return nil, errors.Wrap(err, "init android device failed")
}
@@ -436,17 +436,17 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
}
driverExt := uixt.NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM))
r.uixtDrivers[androidDevice.Options.SerialNumber] = driverExt
r.uixtDrivers[androidDeviceOptions.SerialNumber] = driverExt
}
// parse iOS devices config
for _, iosDevice := range parsedConfig.IOS {
err := r.parseDeviceConfig(iosDevice, parsedConfig.Variables)
for _, iosDeviceOptions := range parsedConfig.IOS {
err := r.parseDeviceConfig(iosDeviceOptions, parsedConfig.Variables)
if err != nil {
return nil, errors.Wrap(code.InvalidCaseError,
fmt.Sprintf("parse ios config failed: %v", err))
}
device, err := uixt.NewIOSDevice(iosDevice.Options.Options()...)
device, err := uixt.NewIOSDevice(iosDeviceOptions.Options()...)
if err != nil {
return nil, errors.Wrap(err, "init ios device failed")
}
@@ -456,17 +456,17 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
}
driverExt := uixt.NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM))
r.uixtDrivers[iosDevice.Options.UDID] = driverExt
r.uixtDrivers[iosDeviceOptions.UDID] = driverExt
}
// parse harmony devices config
for _, harmonyDevice := range parsedConfig.Harmony {
err := r.parseDeviceConfig(harmonyDevice, parsedConfig.Variables)
for _, harmonyDeviceOptions := range parsedConfig.Harmony {
err := r.parseDeviceConfig(harmonyDeviceOptions, parsedConfig.Variables)
if err != nil {
return nil, errors.Wrap(code.InvalidCaseError,
fmt.Sprintf("parse harmony config failed: %v", err))
}
device, err := uixt.NewHarmonyDevice(harmonyDevice.Options.Options()...)
device, err := uixt.NewHarmonyDevice(harmonyDeviceOptions.Options()...)
if err != nil {
return nil, errors.Wrap(err, "init harmony device failed")
}
@@ -476,7 +476,7 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
}
driverExt := uixt.NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM))
r.uixtDrivers[harmonyDevice.Options.ConnectKey] = driverExt
r.uixtDrivers[harmonyDeviceOptions.ConnectKey] = driverExt
}
return parsedConfig, nil

View File

@@ -15,8 +15,8 @@ import (
)
type MobileUI struct {
OSType string `json:"os_type,omitempty" yaml:"os_type,omitempty"` // ios or harmony or android
Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` // android serial or ios udid
OSType string `json:"os_type,omitempty" yaml:"os_type,omitempty"` // mobile device os type
Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` // mobile device serial number
uixt.MobileAction `yaml:",inline"`
Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"`
}

View File

@@ -8,7 +8,7 @@ import (
)
type ActionOptions struct {
Context context.Context
Context context.Context `json:"-" yaml:"-"`
// log
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log