mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-06 08:19:45 +08:00
feat: add MCP plugin support and optimize AI service configuration
- Add UIXT runner with MCP plugin support - Refactor AI service options handling - Optimize configuration parsing for LLM and CV services - Update dependencies to latest versions
This commit is contained in:
259
runner_uixt.go
Normal file
259
runner_uixt.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package hrp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/code"
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UIXTRunner struct {
|
||||
Configs *UIXTConfig
|
||||
Session *SessionRunner
|
||||
DriverExt *uixt.XTDriver
|
||||
|
||||
RestartCount int // app restart count
|
||||
RetryCount int // retry count
|
||||
}
|
||||
|
||||
type UIXTConfig struct {
|
||||
uixt.DriverCacheConfig
|
||||
|
||||
JSONCase ITestCase
|
||||
UIA2 bool // UIAutomator2(Android)
|
||||
LogOn bool // 开启打点日志
|
||||
Timeout int // seconds
|
||||
AbortErrors []error // abort errors
|
||||
MaxRestartAppCount int // max app restart count
|
||||
MaxRetryCount int // max retry count
|
||||
|
||||
WDAPort int
|
||||
WDAMjpegPort int
|
||||
}
|
||||
|
||||
const (
|
||||
DEFAULT_TIMEOUT = 1200 // 20 minutes
|
||||
DEFAULT_MAX_RESTART_APP_COUNT = 3 // max app restart count
|
||||
DEFAULT_MAX_RETRY_COUNT = 3 // max retry count
|
||||
)
|
||||
|
||||
func NewUIXTRunner(configs *UIXTConfig) (runner *UIXTRunner, err error) {
|
||||
configs.addDefault()
|
||||
log.Info().Str("version", version.GetVersionInfo()).
|
||||
Interface("configs", configs).Msg("init UIXT runner")
|
||||
|
||||
// init testcase config
|
||||
var config *TConfig
|
||||
var testSteps []IStep
|
||||
if configs.JSONCase != nil {
|
||||
// load testcase
|
||||
testCases, err := LoadTestCases(configs.JSONCase)
|
||||
if err != nil || len(testCases) == 0 {
|
||||
return nil, errors.Wrap(err, "load testcase failed")
|
||||
}
|
||||
testCase := testCases[0]
|
||||
config = testCase.Config.Get()
|
||||
testSteps = testCase.TestSteps
|
||||
} else {
|
||||
config = NewConfig("config agent")
|
||||
}
|
||||
config.SetAIOptions(configs.AIOptions...)
|
||||
|
||||
testcase := TestCase{
|
||||
Config: config,
|
||||
TestSteps: testSteps,
|
||||
}
|
||||
|
||||
var caseRunner *CaseRunner
|
||||
|
||||
switch configs.Platform {
|
||||
case "ios":
|
||||
port, err := configs.getWDALocalPort(configs.Serial)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("get ios agent WDA local port failed")
|
||||
} else {
|
||||
log.Info().Str("port", port).Msg("set WDA_LOCAL_PORT env")
|
||||
os.Setenv("WDA_LOCAL_PORT", port)
|
||||
}
|
||||
config.SetIOS(
|
||||
option.WithUDID(configs.Serial),
|
||||
option.WithWDAPort(configs.WDAPort),
|
||||
option.WithWDAMjpegPort(configs.WDAMjpegPort),
|
||||
option.WithWDALogOn(configs.LogOn),
|
||||
)
|
||||
case "harmony":
|
||||
config.SetHarmony(
|
||||
option.WithConnectKey(configs.Serial),
|
||||
)
|
||||
default:
|
||||
// default to android
|
||||
configs.Platform = "android"
|
||||
config.SetAndroid(
|
||||
option.WithSerialNumber(configs.Serial),
|
||||
option.WithUIA2(configs.UIA2),
|
||||
option.WithAdbLogOn(configs.LogOn),
|
||||
)
|
||||
}
|
||||
|
||||
// create runner with HTML report enabled for UIXT
|
||||
hrpRunner := NewRunner(nil).SetSaveTests(true).GenHTMLReport()
|
||||
caseRunner, err = NewCaseRunner(testcase, hrpRunner)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init case runner failed")
|
||||
}
|
||||
sessionRunner := caseRunner.NewSession()
|
||||
|
||||
dExt, err := uixt.GetOrCreateXTDriver(configs.DriverCacheConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get driver failed")
|
||||
}
|
||||
|
||||
// check environment
|
||||
if err := CheckEnv(dExt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runner = &UIXTRunner{
|
||||
Configs: configs,
|
||||
Session: sessionRunner,
|
||||
DriverExt: dExt,
|
||||
}
|
||||
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
func (configs *UIXTConfig) addDefault() {
|
||||
if configs.Timeout == 0 {
|
||||
configs.Timeout = DEFAULT_TIMEOUT
|
||||
}
|
||||
if configs.MaxRestartAppCount == 0 {
|
||||
configs.MaxRestartAppCount = DEFAULT_MAX_RESTART_APP_COUNT
|
||||
}
|
||||
if configs.MaxRetryCount == 0 {
|
||||
configs.MaxRetryCount = DEFAULT_MAX_RETRY_COUNT
|
||||
}
|
||||
if len(configs.AbortErrors) == 0 {
|
||||
configs.AbortErrors = []error{
|
||||
// risk control error, abort
|
||||
code.RiskControlAccountActivation,
|
||||
code.RiskControlSlideVerification,
|
||||
code.RiskControlLogout,
|
||||
// network error, abort
|
||||
code.NetworkError,
|
||||
}
|
||||
}
|
||||
if configs.WDAPort == 0 {
|
||||
configs.WDAPort = 8700
|
||||
}
|
||||
if configs.WDAMjpegPort == 0 {
|
||||
configs.WDAMjpegPort = 8800
|
||||
}
|
||||
}
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: 10 * time.Minute,
|
||||
}
|
||||
|
||||
func (configs *UIXTConfig) getWDALocalPort(udid string) (string, error) {
|
||||
payloadBytes, _ := json.Marshal(map[string]string{
|
||||
"device_id": udid,
|
||||
})
|
||||
req, err := http.NewRequest("POST",
|
||||
fmt.Sprintf("http://127.0.0.1:%d/get_device_port", configs.WDAMjpegPort),
|
||||
bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "create request failed")
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "request ios agent failed")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "read ios agent response body failed")
|
||||
}
|
||||
|
||||
var resp iosAgentResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return "", errors.Wrap(err, "unmarshal ios agent response failed")
|
||||
}
|
||||
|
||||
log.Info().Interface("resp", resp).Msg("get ios agent WDA local port")
|
||||
if resp.Code != 0 {
|
||||
return "", errors.New("ios agent response code != 0")
|
||||
}
|
||||
return resp.Port, nil
|
||||
}
|
||||
|
||||
type iosAgentResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
func CheckEnv(driverExt *uixt.XTDriver) (err error) {
|
||||
log.Info().Msg("check runner environment")
|
||||
|
||||
// 检查设备是否正常
|
||||
if err := CheckDevice(driverExt); err != nil {
|
||||
log.Error().Err(err).Str("screenshot", "").Msg("check device failed")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckDevice(driverExt *uixt.XTDriver) error {
|
||||
// 检测截图功能是否正常
|
||||
bufSource, err := driverExt.ScreenShot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "screenshot abnormal")
|
||||
}
|
||||
|
||||
// 检测设备是否锁屏(截图是否全黑)
|
||||
img, _, err := image.Decode(bufSource)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode screenshot image failed")
|
||||
}
|
||||
|
||||
if isImageBlack(img) {
|
||||
return errors.Wrap(code.DeviceConfigureError,
|
||||
"device screen is locked")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isBlack(c color.Color) bool {
|
||||
r, g, b, _ := c.RGBA()
|
||||
return r == 0 && g == 0 && b == 0
|
||||
}
|
||||
|
||||
// 判断图片是否全黑
|
||||
func isImageBlack(img image.Image) bool {
|
||||
bounds := img.Bounds()
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
if !isBlack(img.At(x, y)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user