Merge branch 'master' into bugfix/huangbin/web_v5

This commit is contained in:
huangbin.beal
2025-07-10 14:03:22 +08:00
19 changed files with 330 additions and 185 deletions

View File

@@ -33,8 +33,9 @@ type AssertOptions struct {
type AssertionResult struct {
Pass bool `json:"pass"`
Thought string `json:"thought"`
ModelName string `json:"model_name"` // model name used for assertion
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics
Content string `json:"content,omitempty"` // raw response content
ModelName string `json:"model_name"` // model name used for assertion
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics
}
// Asserter handles assertion using different AI models
@@ -180,5 +181,6 @@ func parseAssertionResult(content string, modelType option.LLMServiceType) (*Ass
}
result.ModelName = string(modelType)
result.Content = content // Store the original response content
return &result, nil
}

View File

@@ -492,9 +492,10 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) (*
return assertResult, errors.Wrap(err, "AI assertion failed")
}
// For assertion failure, we should still return success but mark the assertion as failed
// This ensures that the AIResult (including screenshot and thought) is properly saved and displayed
if !result.Pass {
assertResult.Error = result.Thought
return assertResult, errors.New(result.Thought)
assertResult.Error = result.Thought // Store the failure reason for reporting
}
return assertResult, nil

View File

@@ -19,6 +19,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/internal/json"
"github.com/httprunner/httprunner/v5/uixt/option"
)
type Attachments map[string]interface{}
@@ -151,32 +152,38 @@ func (s *DriverSession) buildURL(urlStr string) (string, error) {
return baseURL.ResolveReference(relativeURL).String(), nil
}
func (s *DriverSession) GET(urlStr string) (rawResp DriverRawResponse, err error) {
return s.RequestWithRetry(http.MethodGet, urlStr, nil)
func (s *DriverSession) GET(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
return s.RequestWithRetry(http.MethodGet, urlStr, nil, opts...)
}
func (s *DriverSession) POST(data interface{}, urlStr string) (rawResp DriverRawResponse, err error) {
func (s *DriverSession) POST(data interface{}, urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
var bsJSON []byte = nil
if data != nil {
if bsJSON, err = json.Marshal(data); err != nil {
return nil, err
}
}
return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON)
return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON, opts...)
}
func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err error) {
return s.RequestWithRetry(http.MethodDelete, urlStr, nil)
func (s *DriverSession) DELETE(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
return s.RequestWithRetry(http.MethodDelete, urlStr, nil, opts...)
}
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) (
rawResp DriverRawResponse, err error,
) {
var lastError error
for attempt := 1; attempt <= s.maxRetry; attempt++ {
maxRetry := s.maxRetry
options := option.NewActionOptions(opts...)
if options.MaxRetryTimes > 0 {
maxRetry = options.MaxRetryTimes
}
for attempt := 1; attempt <= maxRetry; attempt++ {
// Execute the request
rawResp, err = s.Request(method, urlStr, rawBody)
rawResp, err = s.Request(method, urlStr, rawBody, opts...)
if err == nil {
if attempt > 1 {
log.Info().Msgf("request succeeded after %d attempts", attempt)
@@ -210,9 +217,15 @@ func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody [
return nil, lastError
}
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) (
rawResp DriverRawResponse, err error,
) {
timeout := s.timeout
options := option.NewActionOptions(opts...)
if options.Timeout > 0 {
timeout = time.Duration(options.Timeout) * time.Second
}
// build final URL
rawURL, err := s.buildURL(urlStr)
if err != nil {
@@ -252,7 +265,7 @@ func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
logger.Msg("request uixt driver")
}()
ctx, cancel := context.WithTimeout(s.ctx, s.timeout)
ctx, cancel := context.WithTimeout(s.ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewBuffer(rawBody))

View File

@@ -65,8 +65,8 @@ func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, e
}
func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) (
absFromX, absFromY, absToX, absToY float64, err error) {
absFromX, absFromY, absToX, absToY float64, err error,
) {
// absolute coordinates
if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 {
return fromX, fromY, toX, toY, nil
@@ -190,32 +190,43 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error {
return nil
}
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) {
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (aiResult *AIExecutionResult, err error) {
switch check {
case option.SelectorOCR:
err = dExt.assertOCR(expected, assert)
case option.SelectorAI:
_, err = dExt.AIAssert(expected)
aiResult, err = dExt.AIAssert(expected)
case option.SelectorForegroundApp:
err = dExt.assertForegroundApp(expected, assert)
case option.SelectorSelector:
err = dExt.assertSelector(expected, assert)
default:
return fmt.Errorf("validator %s not implemented", check)
return nil, fmt.Errorf("validator %s not implemented", check)
}
if err != nil {
// Technical error (not assertion failure)
if message == nil {
message = []string{""}
}
log.Error().Err(err).Str("assert", assert).Str("expect", expected).
Str("msg", message[0]).Msg("validate failed")
return err
return nil, err
} else if aiResult != nil {
// Check assertion result instead of relying on error
if !aiResult.AssertionResult.Pass {
return aiResult, errors.New(aiResult.AssertionResult.Thought)
}
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).
Interface("ai_assertion_result", aiResult.AssertionResult).
Msg("ai assertion passed")
return aiResult, nil
} else {
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).Msg("validate success")
return nil, nil
}
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).Msg("validate success")
return nil
}
type SleepConfig struct {

View File

@@ -160,22 +160,22 @@ func (dev *IOSDevice) Setup() error {
if version.GreaterThan(semver.MustParse("17.4.0")) {
info, err := tunnel.TunnelInfoForDevice(dev.DeviceEntry.Properties.SerialNumber, ios.HttpApiHost(), ios.HttpApiPort())
if err != nil {
return err
return errors.Wrap(code.DeviceConnectionError, err.Error())
}
dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort
dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN
rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry)
if err != nil {
return err
return errors.Wrap(code.DeviceConnectionError, err.Error())
}
defer rsdService.Close()
rsdProvider, err := rsdService.Handshake()
if err != nil {
return err
return errors.Wrap(code.DeviceConnectionError, err.Error())
}
device, err := ios.GetDeviceWithAddress(dev.DeviceEntry.Properties.SerialNumber, info.Address, rsdProvider)
if err != nil {
return err
return errors.Wrap(code.DeviceConnectionError, err.Error())
}
device.UserspaceTUN = dev.DeviceEntry.UserspaceTUN
device.UserspaceTUNPort = dev.DeviceEntry.UserspaceTUNPort
@@ -470,7 +470,7 @@ func (dev *IOSDevice) getVersion() (version *semver.Version, err error) {
version, err = ios.GetProductVersion(dev.DeviceEntry)
if err != nil {
log.Error().Err(err).Msg("failed to get version")
return nil, err
return nil, errors.Wrap(code.DeviceGetInfoError, err.Error())
}
log.Info().Str("version", version.String()).Msg("get ios device version")
return version, nil