mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-05 23:51:25 +08:00
Merge 'fix-init-llm-service' into 'master'
Fix init llm service See merge request: !157
This commit is contained in:
@@ -116,6 +116,10 @@ The framework supports both Go and Python plugins:
|
|||||||
- Internal utilities in `internal/`
|
- Internal utilities in `internal/`
|
||||||
- Examples in `examples/`
|
- Examples in `examples/`
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
- All code comments must be written in English
|
||||||
|
- All documentation must be written in Chinese
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
- Go 1.23+ required
|
- Go 1.23+ required
|
||||||
- Uses Cobra for CLI
|
- Uses Cobra for CLI
|
||||||
@@ -126,7 +130,3 @@ The framework supports both Go and Python plugins:
|
|||||||
- Static linking for deployment
|
- Static linking for deployment
|
||||||
- Version info embedded via ldflags
|
- Version info embedded via ldflags
|
||||||
- Cross-platform builds supported
|
- Cross-platform builds supported
|
||||||
|
|
||||||
### Code Standards
|
|
||||||
- All code comments must be written in English
|
|
||||||
- All documentation must be written in Chinese
|
|
||||||
|
|||||||
+38
-19
@@ -35,24 +35,31 @@ type UIXTRunner struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UIXTConfig struct {
|
type UIXTConfig struct {
|
||||||
uixt.DriverCacheConfig
|
uixt.DriverCacheConfig // includes Platform, Serial, AIOptions
|
||||||
|
|
||||||
Ctx context.Context
|
// Runtime context
|
||||||
Cancel context.CancelFunc
|
Ctx context.Context
|
||||||
JSONCase ITestCase
|
Cancel context.CancelFunc `json:"-"`
|
||||||
UIA2 bool // UIAutomator2(Android)
|
|
||||||
LogOn bool // 开启打点日志
|
// Test case configuration
|
||||||
|
JSONCase ITestCase
|
||||||
|
|
||||||
|
// Device specific options
|
||||||
|
UIA2 bool // UIAutomator2(Android)
|
||||||
|
LogOn bool // 开启打点日志
|
||||||
|
WDAPort int // iOS WebDriverAgent port
|
||||||
|
WDAMjpegPort int // iOS WebDriverAgent MJPEG port
|
||||||
|
|
||||||
|
// Agent behavior configuration
|
||||||
Timeout int // seconds
|
Timeout int // seconds
|
||||||
AbortErrors []error // abort errors
|
AbortErrors []error // abort errors
|
||||||
MaxRestartAppCount int // max app restart count
|
MaxRestartAppCount int // max app restart count
|
||||||
MaxRetryCount int // max retry count
|
MaxRetryCount int // max retry count
|
||||||
|
|
||||||
WDAPort int
|
// Backward compatibility fields - legacy API support
|
||||||
WDAMjpegPort int
|
OSType string // deprecated: use Platform from DriverCacheConfig
|
||||||
|
Serial string // deprecated: use Serial from DriverCacheConfig
|
||||||
OSType string // platform
|
LLMService option.LLMServiceType // deprecated: use AIOptions from DriverCacheConfig
|
||||||
Serial string
|
|
||||||
LLMService option.LLMServiceType // LLM 服务类型
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -83,7 +90,7 @@ func NewUIXTRunner(configs *UIXTConfig) (runner *UIXTRunner, err error) {
|
|||||||
}
|
}
|
||||||
config.SetAIOptions(configs.AIOptions...)
|
config.SetAIOptions(configs.AIOptions...)
|
||||||
|
|
||||||
switch configs.OSType {
|
switch configs.Platform {
|
||||||
case "ios":
|
case "ios":
|
||||||
port, err := configs.getWDALocalPort(configs.Serial)
|
port, err := configs.getWDALocalPort(configs.Serial)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,7 +130,7 @@ func NewUIXTRunner(configs *UIXTConfig) (runner *UIXTRunner, err error) {
|
|||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
// default to android
|
// default to android
|
||||||
configs.OSType = "android"
|
configs.Platform = "android"
|
||||||
config.SetAndroid(
|
config.SetAndroid(
|
||||||
option.WithSerialNumber(configs.Serial),
|
option.WithSerialNumber(configs.Serial),
|
||||||
option.WithUIA2(configs.UIA2),
|
option.WithUIA2(configs.UIA2),
|
||||||
@@ -144,11 +151,10 @@ func NewUIXTRunner(configs *UIXTConfig) (runner *UIXTRunner, err error) {
|
|||||||
}
|
}
|
||||||
sessionRunner := caseRunner.NewSession()
|
sessionRunner := caseRunner.NewSession()
|
||||||
|
|
||||||
driverCacheConfig := uixt.DriverCacheConfig{
|
// Use configs directly as it inherits DriverCacheConfig
|
||||||
Platform: configs.OSType,
|
driverCacheConfig := configs.DriverCacheConfig
|
||||||
Serial: configs.Serial,
|
driverCacheConfig.AIOptions = config.AIOptions.Options()
|
||||||
AIOptions: config.AIOptions.Options(),
|
|
||||||
}
|
|
||||||
dExt, err := uixt.GetOrCreateXTDriver(driverCacheConfig)
|
dExt, err := uixt.GetOrCreateXTDriver(driverCacheConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "get driver failed")
|
return nil, errors.Wrap(err, "get driver failed")
|
||||||
@@ -181,6 +187,19 @@ func NewUIXTRunner(configs *UIXTConfig) (runner *UIXTRunner, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (configs *UIXTConfig) addDefault() {
|
func (configs *UIXTConfig) addDefault() {
|
||||||
|
// Handle backward compatibility - sync legacy fields to embedded DriverCacheConfig
|
||||||
|
if configs.OSType != "" && configs.Platform == "" {
|
||||||
|
configs.Platform = configs.OSType
|
||||||
|
}
|
||||||
|
if configs.Serial != "" && configs.DriverCacheConfig.Serial == "" {
|
||||||
|
configs.DriverCacheConfig.Serial = configs.Serial
|
||||||
|
}
|
||||||
|
if configs.LLMService != "" && len(configs.AIOptions) == 0 {
|
||||||
|
configs.AIOptions = []option.AIServiceOption{
|
||||||
|
option.WithLLMService(configs.LLMService),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if configs.Ctx == nil {
|
if configs.Ctx == nil {
|
||||||
configs.Ctx = context.Background()
|
configs.Ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,7 +472,10 @@ func (w *WingsService) callWingsAPI(ctx context.Context, request WingsActionRequ
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
logID := resp.Header.Get("X-Tt-Logid")
|
logID := resp.Header.Get("X-Tt-Logid")
|
||||||
log.Info().Str("step_text", request.StepText).Str("image_url", request.DeviceInfos[0].NowImageUrl).Str("log_id", logID).Str("biz_id", request.BizId).Str("url", w.apiURL).Msg("call wings api")
|
log.Info().Str("step_text", request.StepText).
|
||||||
|
Str("image_url", request.DeviceInfos[0].NowImageUrl).
|
||||||
|
Str("log_id", logID).Str("biz_id", request.BizId).
|
||||||
|
Str("url", w.apiURL).Msg("call wings api")
|
||||||
|
|
||||||
// Read response body
|
// Read response body
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
|||||||
@@ -1851,3 +1851,149 @@ func TestNewMCPErrorResponse(t *testing.T) {
|
|||||||
result := NewMCPErrorResponse("Test error message")
|
result := NewMCPErrorResponse("Test error message")
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseActionOptions tests core functionality of parseActionOptions function
|
||||||
|
func TestParseActionOptions(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
arguments map[string]any
|
||||||
|
expectErr bool
|
||||||
|
validate func(t *testing.T, opts *option.ActionOptions)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty_arguments",
|
||||||
|
arguments: map[string]any{},
|
||||||
|
expectErr: false,
|
||||||
|
validate: func(t *testing.T, opts *option.ActionOptions) {
|
||||||
|
assert.Equal(t, "", opts.Platform)
|
||||||
|
assert.Equal(t, "", opts.Serial)
|
||||||
|
assert.Equal(t, 0.0, opts.X)
|
||||||
|
assert.Equal(t, 0.0, opts.Y)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic_fields",
|
||||||
|
arguments: map[string]any{
|
||||||
|
"platform": "android",
|
||||||
|
"serial": "device123",
|
||||||
|
"x": 100.5,
|
||||||
|
"y": 200.7,
|
||||||
|
"text": "Hello World",
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validate: func(t *testing.T, opts *option.ActionOptions) {
|
||||||
|
assert.Equal(t, "android", opts.Platform)
|
||||||
|
assert.Equal(t, "device123", opts.Serial)
|
||||||
|
assert.Equal(t, 100.5, opts.X)
|
||||||
|
assert.Equal(t, 200.7, opts.Y)
|
||||||
|
assert.Equal(t, "Hello World", opts.Text)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complete_nested_fields",
|
||||||
|
arguments: map[string]any{
|
||||||
|
"platform": "ios",
|
||||||
|
"serial": "ios_device",
|
||||||
|
"screenshot_with_ocr": true,
|
||||||
|
"screenshot_with_upload": true,
|
||||||
|
"screenshot_with_live_type": true,
|
||||||
|
"screenshot_with_live_popularity": true,
|
||||||
|
"screenshot_with_base64": true,
|
||||||
|
"screenshot_with_ui_types": []string{"button", "input", "text"},
|
||||||
|
"screenshot_with_close_popups": true,
|
||||||
|
"screenshot_with_ocr_cluster": "test_cluster",
|
||||||
|
"screenshot_file_name": "test.png",
|
||||||
|
"screenrecord_duration": 30.5,
|
||||||
|
"screenrecord_with_audio": true,
|
||||||
|
"screenrecord_with_scrcpy": true,
|
||||||
|
"screenrecord_path": "/tmp/record.mp4",
|
||||||
|
"scope": []float64{0.1, 0.2, 0.9, 0.8},
|
||||||
|
"abs_scope": []int{100, 200, 900, 800},
|
||||||
|
"regex": true,
|
||||||
|
"offset": []int{5, 10},
|
||||||
|
"tap_random_rect": true,
|
||||||
|
"swipe_offset": []int{1, 2, 3, 4},
|
||||||
|
"offset_random_range": []int{-5, 5},
|
||||||
|
"index": 2,
|
||||||
|
"match_one": true,
|
||||||
|
"ignore_NotFoundError": true,
|
||||||
|
"pre_mark_operation": true,
|
||||||
|
"post_mark_operation": false,
|
||||||
|
"max_retry_times": 5,
|
||||||
|
"timeout": 30,
|
||||||
|
"custom": map[string]any{
|
||||||
|
"test_key": "test_value",
|
||||||
|
"nested_data": map[string]any{"key": "value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validate: func(t *testing.T, opts *option.ActionOptions) {
|
||||||
|
assert.Equal(t, "ios", opts.Platform)
|
||||||
|
assert.Equal(t, "ios_device", opts.Serial)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithOCR)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithUpload)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithLiveType)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithLivePopularity)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithBase64)
|
||||||
|
assert.Equal(t, []string{"button", "input", "text"}, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithUITypes)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenShotOptions.ScreenShotWithClosePopups)
|
||||||
|
assert.Equal(t, "test_cluster", opts.ScreenOptions.ScreenShotOptions.ScreenShotWithOCRCluster)
|
||||||
|
assert.Equal(t, "test.png", opts.ScreenOptions.ScreenShotOptions.ScreenShotFileName)
|
||||||
|
assert.Equal(t, 30.5, opts.ScreenOptions.ScreenRecordOptions.ScreenRecordDuration)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenRecordOptions.ScreenRecordWithAudio)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenRecordOptions.ScreenRecordWithScrcpy)
|
||||||
|
assert.Equal(t, "/tmp/record.mp4", opts.ScreenOptions.ScreenRecordOptions.ScreenRecordPath)
|
||||||
|
assert.Equal(t, []float64{0.1, 0.2, 0.9, 0.8}, []float64(opts.ScreenOptions.ScreenFilterOptions.Scope))
|
||||||
|
assert.Equal(t, []int{100, 200, 900, 800}, []int(opts.ScreenOptions.ScreenFilterOptions.AbsScope))
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenFilterOptions.Regex)
|
||||||
|
assert.Equal(t, []int{5, 10}, opts.ScreenOptions.ScreenFilterOptions.TapOffset)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenFilterOptions.TapRandomRect)
|
||||||
|
assert.Equal(t, []int{1, 2, 3, 4}, opts.ScreenOptions.ScreenFilterOptions.SwipeOffset)
|
||||||
|
assert.Equal(t, []int{-5, 5}, opts.ScreenOptions.ScreenFilterOptions.OffsetRandomRange)
|
||||||
|
assert.Equal(t, 2, opts.ScreenOptions.ScreenFilterOptions.Index)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenFilterOptions.MatchOne)
|
||||||
|
assert.True(t, opts.ScreenOptions.ScreenFilterOptions.IgnoreNotFoundError)
|
||||||
|
assert.True(t, opts.ScreenOptions.MarkOperationOptions.PreMarkOperation)
|
||||||
|
assert.False(t, opts.ScreenOptions.MarkOperationOptions.PostMarkOperation)
|
||||||
|
assert.Equal(t, 5, opts.MaxRetryTimes)
|
||||||
|
assert.Equal(t, 30, opts.Timeout)
|
||||||
|
assert.Equal(t, "test_value", opts.Custom["test_key"])
|
||||||
|
nestedData, ok := opts.Custom["nested_data"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "value", nestedData["key"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error_case_non_serializable",
|
||||||
|
arguments: map[string]any{
|
||||||
|
"platform": "android",
|
||||||
|
"invalid": make(chan int),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error_case_invalid_type",
|
||||||
|
arguments: map[string]any{
|
||||||
|
"x": "not_a_number",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := parseActionOptions(tc.arguments)
|
||||||
|
|
||||||
|
if tc.expectErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
if tc.validate != nil {
|
||||||
|
tc.validate(t, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-5
@@ -24,6 +24,7 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
services: services,
|
services: services,
|
||||||
loadedMCPClients: make(map[string]client.MCPClient),
|
loadedMCPClients: make(map[string]client.MCPClient),
|
||||||
}
|
}
|
||||||
|
log.Info().Interface("services", services).Msg("init XTDriver with AI services")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -32,17 +33,21 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
// Use advanced LLM service configuration if provided
|
// Use advanced LLM service configuration if provided
|
||||||
driverExt.LLMService, err = ai.NewLLMServiceWithOptionConfig(services.LLMConfig)
|
driverExt.LLMService, err = ai.NewLLMServiceWithOptionConfig(services.LLMConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("init llm service with config failed")
|
log.Warn().Err(err).Interface("service", services.LLMConfig).
|
||||||
|
Msg("init llm service with advanced config failed")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("LLM service initialized with advanced config")
|
log.Info().Interface("service", services.LLMConfig).
|
||||||
|
Msg("LLM service initialized with advanced config")
|
||||||
}
|
}
|
||||||
} else if services.LLMService != "" {
|
} else if services.LLMService != "" {
|
||||||
// Use simple LLM service configuration if provided
|
// Use simple LLM service configuration if provided
|
||||||
driverExt.LLMService, err = ai.NewLLMService(services.LLMService)
|
driverExt.LLMService, err = ai.NewLLMService(services.LLMService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("init llm service failed")
|
log.Warn().Err(err).Str("service", string(services.LLMService)).
|
||||||
|
Msg("init llm service with simple config failed")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("LLM service initialized with simple config")
|
log.Info().Str("service", string(services.LLMService)).
|
||||||
|
Msg("LLM service initialized with simple config")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use Wings service as fallback
|
// Use Wings service as fallback
|
||||||
@@ -50,7 +55,7 @@ func NewXTDriver(driver IDriver, opts ...option.AIServiceOption) (*XTDriver, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("init Wings service failed")
|
log.Warn().Err(err).Msg("init Wings service failed")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("Wings service initialized")
|
log.Info().Msg("Wings service initialized as fallback")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user