mirror of
https://github.com/httprunner/httprunner.git
synced 2026-08-01 19:47:11 +08:00
refactor: enhance JSON handling and improve request retry logic in DriverSession
This commit is contained in:
@@ -158,25 +158,42 @@ func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err er
|
||||
|
||||
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
|
||||
rawResp DriverRawResponse, err error) {
|
||||
for count := 1; count <= s.maxRetry; count++ {
|
||||
var lastError error
|
||||
|
||||
for attempt := 1; attempt <= s.maxRetry; attempt++ {
|
||||
// Execute the request
|
||||
rawResp, err = s.Request(method, urlStr, rawBody)
|
||||
if err == nil {
|
||||
return
|
||||
if attempt > 1 {
|
||||
log.Info().Msgf("request succeeded after %d attempts", attempt)
|
||||
}
|
||||
return rawResp, nil
|
||||
}
|
||||
|
||||
lastError = err
|
||||
log.Warn().Err(err).Msgf("request failed, attempt %d/%d", attempt, s.maxRetry)
|
||||
|
||||
// If this was the last attempt, break
|
||||
if attempt == s.maxRetry {
|
||||
log.Error().Err(lastError).Msgf("all %d retry attempts failed, giving up", s.maxRetry)
|
||||
break
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Try to reset the session for the next attempt
|
||||
if s.resetFn != nil {
|
||||
log.Warn().Msg("reset driver session")
|
||||
if err2 := s.resetFn(); err2 != nil {
|
||||
log.Error().Err(err2).Msgf(
|
||||
"failed to reset session, try count %v", count)
|
||||
log.Warn().Msgf("attempting to reset driver session before attempt %d", attempt+1)
|
||||
if resetErr := s.resetFn(); resetErr != nil {
|
||||
log.Error().Err(resetErr).Msgf("failed to reset session, will retry without reset")
|
||||
} else {
|
||||
log.Info().Msgf(
|
||||
"reset session success, try count %v", count)
|
||||
log.Info().Msg("session reset successful")
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
return nil, lastError
|
||||
}
|
||||
|
||||
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
|
||||
|
||||
@@ -36,13 +36,19 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) {
|
||||
Session: NewDriverSession(),
|
||||
}
|
||||
|
||||
if !device.Options.LazySetup {
|
||||
// setup driver
|
||||
if err := driver.Setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// setup driver
|
||||
if err := driver.Setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check WDA status
|
||||
wdaStatus, err := driver.Status()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info().Interface("status", wdaStatus).
|
||||
Msg("check WDA status")
|
||||
|
||||
// register driver session reset handler
|
||||
driver.Session.RegisterResetHandler(driver.Setup)
|
||||
|
||||
@@ -146,13 +152,6 @@ func (wd *WDADriver) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
wdaStatus, err := wd.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info().Interface("status", wdaStatus).
|
||||
Msg("check WDA status")
|
||||
|
||||
// create new session
|
||||
if err := wd.InitSession(nil); err != nil {
|
||||
return errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||||
|
||||
@@ -38,8 +38,7 @@ func TestDevice_IOS_Install(t *testing.T) {
|
||||
func TestDriver_WDA_LazySetup(t *testing.T) {
|
||||
device, err := NewIOSDevice(
|
||||
option.WithWDAPort(8700),
|
||||
option.WithWDAMjpegPort(8800),
|
||||
option.WithLazySetup(true))
|
||||
option.WithWDAMjpegPort(8800))
|
||||
require.Nil(t, err)
|
||||
driver, err := NewWDADriver(device)
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -193,6 +193,8 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
||||
"tap_random_rect": tempOptions.TapRandomRect,
|
||||
"anti_risk": tempOptions.AntiRisk,
|
||||
"pre_mark_operation": tempOptions.PreMarkOperation,
|
||||
"reset_history": tempOptions.ResetHistory,
|
||||
"match_one": tempOptions.MatchOne,
|
||||
}
|
||||
|
||||
// Add boolean options only if they are true
|
||||
@@ -209,6 +211,18 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
||||
if tempOptions.Index != 0 {
|
||||
arguments["index"] = tempOptions.Index
|
||||
}
|
||||
if tempOptions.Interval > 0 {
|
||||
arguments["interval"] = tempOptions.Interval
|
||||
}
|
||||
if tempOptions.Steps > 0 {
|
||||
arguments["steps"] = tempOptions.Steps
|
||||
}
|
||||
if tempOptions.Timeout > 0 {
|
||||
arguments["timeout"] = tempOptions.Timeout
|
||||
}
|
||||
if tempOptions.Frequency > 0 {
|
||||
arguments["frequency"] = tempOptions.Frequency
|
||||
}
|
||||
// Only set duration if it's not already set (to avoid overriding tool-specific conversions)
|
||||
if tempOptions.Duration > 0 {
|
||||
if _, exists := arguments["duration"]; !exists {
|
||||
@@ -288,13 +302,19 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
||||
if tempOptions.Selector != "" {
|
||||
arguments["selector"] = tempOptions.Selector
|
||||
}
|
||||
}
|
||||
|
||||
func getFloat64ValueOrDefault(value float64, defaultValue float64) float64 {
|
||||
if value == 0 {
|
||||
return defaultValue
|
||||
if tempOptions.Identifier != "" {
|
||||
arguments["identifier"] = tempOptions.Identifier
|
||||
}
|
||||
|
||||
// Add direction option (can be string or []float64)
|
||||
if tempOptions.Direction != nil {
|
||||
arguments["direction"] = tempOptions.Direction
|
||||
}
|
||||
|
||||
// Add custom options
|
||||
if len(tempOptions.Custom) > 0 {
|
||||
arguments["custom"] = tempOptions.Custom
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// parseActionOptions converts MCP request arguments to ActionOptions struct
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolStartToGoal implements the start_to_goal tool call.
|
||||
@@ -162,7 +163,7 @@ func (t *ToolAIQuery) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build action options from unified request
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// AI query logic with options
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolSwipe implements the generic swipe tool call.
|
||||
@@ -124,15 +125,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc {
|
||||
swipeDirection, validDirections)
|
||||
}
|
||||
|
||||
opts := []option.ActionOption{
|
||||
option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)),
|
||||
option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)),
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
if unifiedReq.Duration == 0 {
|
||||
opts = append(opts, option.WithDuration(0.5))
|
||||
}
|
||||
if unifiedReq.AntiRisk {
|
||||
opts = append(opts, option.WithAntiRisk(true))
|
||||
}
|
||||
if unifiedReq.PreMarkOperation {
|
||||
opts = append(opts, option.WithPreMarkOperation(true))
|
||||
if unifiedReq.PressDuration == 0 {
|
||||
opts = append(opts, option.WithPressDuration(0.1))
|
||||
}
|
||||
|
||||
// Convert direction to coordinates and perform swipe
|
||||
@@ -240,17 +239,8 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc {
|
||||
|
||||
params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY}
|
||||
|
||||
// Build action options from the unified request
|
||||
opts := []option.ActionOption{}
|
||||
if unifiedReq.Duration > 0 {
|
||||
opts = append(opts, option.WithDuration(unifiedReq.Duration))
|
||||
}
|
||||
if unifiedReq.PressDuration > 0 {
|
||||
opts = append(opts, option.WithPressDuration(unifiedReq.PressDuration))
|
||||
}
|
||||
if unifiedReq.AntiRisk {
|
||||
opts = append(opts, option.WithAntiRisk(true))
|
||||
}
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
swipeAction := prepareSwipeAction(driverExt, params, opts...)
|
||||
err = swipeAction(driverExt)
|
||||
@@ -327,7 +317,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
// Build action options from request structure
|
||||
var opts []option.ActionOption
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Add boolean options
|
||||
if unifiedReq.IgnoreNotFoundError {
|
||||
@@ -400,24 +390,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build action options from request structure
|
||||
var opts []option.ActionOption
|
||||
|
||||
// Add boolean options
|
||||
if unifiedReq.IgnoreNotFoundError {
|
||||
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||
}
|
||||
if unifiedReq.Regex {
|
||||
opts = append(opts, option.WithRegex(true))
|
||||
}
|
||||
|
||||
// Add numeric options
|
||||
if unifiedReq.MaxRetryTimes > 0 {
|
||||
opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
|
||||
}
|
||||
if unifiedReq.Index > 0 {
|
||||
opts = append(opts, option.WithIndex(unifiedReq.Index))
|
||||
}
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Swipe to tap text action logic
|
||||
err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...)
|
||||
@@ -478,24 +452,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build action options from request structure
|
||||
var opts []option.ActionOption
|
||||
|
||||
// Add boolean options
|
||||
if unifiedReq.IgnoreNotFoundError {
|
||||
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
||||
}
|
||||
if unifiedReq.Regex {
|
||||
opts = append(opts, option.WithRegex(true))
|
||||
}
|
||||
|
||||
// Add numeric options
|
||||
if unifiedReq.MaxRetryTimes > 0 {
|
||||
opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
|
||||
}
|
||||
if unifiedReq.Index > 0 {
|
||||
opts = append(opts, option.WithIndex(unifiedReq.Index))
|
||||
}
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Swipe to tap texts action logic
|
||||
err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...)
|
||||
@@ -575,12 +533,10 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("from_x, from_y, to_x, and to_y coordinates are required")
|
||||
}
|
||||
|
||||
opts := []option.ActionOption{}
|
||||
if unifiedReq.Duration > 0 {
|
||||
opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0))
|
||||
}
|
||||
if unifiedReq.AntiRisk {
|
||||
opts = append(opts, option.WithAntiRisk(true))
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
if unifiedReq.Duration == 0 {
|
||||
opts = append(opts, option.WithDuration(0.5))
|
||||
}
|
||||
|
||||
// Drag action logic
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolTapXY implements the tap_xy tool call.
|
||||
@@ -42,14 +43,9 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Add configurable options based on request
|
||||
if unifiedReq.PreMarkOperation {
|
||||
opts = append(opts, option.WithPreMarkOperation(true))
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
||||
return nil, fmt.Errorf("x and y coordinates are required")
|
||||
@@ -123,19 +119,9 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Add configurable options based on request
|
||||
if unifiedReq.PreMarkOperation {
|
||||
opts = append(opts, option.WithPreMarkOperation(true))
|
||||
}
|
||||
|
||||
// Add AntiRisk support
|
||||
if unifiedReq.AntiRisk {
|
||||
opts = append(opts, option.WithAntiRisk(true))
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
||||
return nil, fmt.Errorf("x and y coordinates are required")
|
||||
@@ -208,14 +194,9 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Add configurable options based on request
|
||||
if unifiedReq.PreMarkOperation {
|
||||
opts = append(opts, option.WithPreMarkOperation(true))
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if unifiedReq.Text == "" {
|
||||
return nil, fmt.Errorf("text parameter is required")
|
||||
@@ -277,14 +258,9 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Add configurable options based on request
|
||||
if unifiedReq.PreMarkOperation {
|
||||
opts = append(opts, option.WithPreMarkOperation(true))
|
||||
}
|
||||
|
||||
// For TapByCV, we need to check if there are UI types in the options
|
||||
// In the original DoAction, it requires ScreenShotWithUITypes to be set
|
||||
// We'll add a basic implementation that triggers CV recognition
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolWebLoginNoneUI implements the web_login_none_ui tool call.
|
||||
@@ -170,7 +171,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Hover by selector action logic
|
||||
@@ -228,7 +229,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Tap by selector action logic
|
||||
@@ -286,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get options directly since ActionOptions is now ActionOptions
|
||||
// Build all options from request arguments
|
||||
opts := unifiedReq.Options()
|
||||
|
||||
// Secondary click by selector action logic
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||
)
|
||||
|
||||
type MobileAction struct {
|
||||
@@ -326,6 +327,10 @@ func (o *ActionOptions) Options() []ActionOption {
|
||||
options = append(options, WithAntiRisk(true))
|
||||
}
|
||||
|
||||
if o.PreMarkOperation {
|
||||
options = append(options, WithPreMarkOperation(true))
|
||||
}
|
||||
|
||||
// custom options
|
||||
if o.Custom != nil {
|
||||
for k, v := range o.Custom {
|
||||
|
||||
@@ -152,17 +152,6 @@ func WithDeviceWDAMjpegPort(port int) DeviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDeviceLazySetup(lazySetup bool) DeviceOption {
|
||||
return func(device *DeviceOptions) {
|
||||
if device.IOSDeviceOptions != nil {
|
||||
device.IOSDeviceOptions.LazySetup = lazySetup
|
||||
}
|
||||
if device.Platform == "" {
|
||||
device.Platform = "ios"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithDeviceResetHomeOnStartup(reset bool) DeviceOption {
|
||||
return func(device *DeviceOptions) {
|
||||
if device.IOSDeviceOptions != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ type IOSDeviceOptions struct {
|
||||
WDAPort int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
|
||||
WDAMjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
LazySetup bool `json:"lazy_setup,omitempty" yaml:"lazy_setup,omitempty"` // lazy setup WDA
|
||||
|
||||
// switch to iOS springboard before init WDA session
|
||||
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
|
||||
@@ -33,9 +32,6 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) {
|
||||
if dev.LogOn {
|
||||
deviceOptions = append(deviceOptions, WithWDALogOn(true))
|
||||
}
|
||||
if dev.LazySetup {
|
||||
deviceOptions = append(deviceOptions, WithLazySetup(true))
|
||||
}
|
||||
if dev.ResetHomeOnStartup {
|
||||
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
|
||||
}
|
||||
@@ -104,12 +100,6 @@ func WithWDALogOn(logOn bool) IOSDeviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLazySetup(lazySetup bool) IOSDeviceOption {
|
||||
return func(device *IOSDeviceOptions) {
|
||||
device.LazySetup = lazySetup
|
||||
}
|
||||
}
|
||||
|
||||
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
|
||||
return func(device *IOSDeviceOptions) {
|
||||
device.ResetHomeOnStartup = reset
|
||||
|
||||
Reference in New Issue
Block a user