mirror of
https://github.com/httprunner/httprunner.git
synced 2026-08-28 11:36:56 +08:00
refactor: move gwda into uixt
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
giDevice "github.com/electricbubble/gidevice"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
const (
|
||||
// Changes the value of maximum depth for traversing elements source tree.
|
||||
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
|
||||
// but it might restrict the depth of source tree.
|
||||
// A part of elements source tree might be lost if the value was too small. Defaults to 50
|
||||
snapshotMaxDepth = 10
|
||||
// Allows to customize accept/dismiss alert button selector.
|
||||
// It helps you to handle an arbitrary element as accept button in accept alert command.
|
||||
// The selector should be a valid class chain expression, where the search root is the alert element itself.
|
||||
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
|
||||
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`]
|
||||
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
||||
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 8100
|
||||
defaultMjpegPort = 9100
|
||||
)
|
||||
|
||||
func InitWDAClient(device *IOSDevice) (*DriverExt, error) {
|
||||
var deviceOptions []IOSDeviceOption
|
||||
if device.UDID != "" {
|
||||
deviceOptions = append(deviceOptions, WithUDID(device.UDID))
|
||||
}
|
||||
if device.Port != 0 {
|
||||
deviceOptions = append(deviceOptions, WithPort(device.Port))
|
||||
}
|
||||
if device.MjpegPort != 0 {
|
||||
deviceOptions = append(deviceOptions, WithMjpegPort(device.MjpegPort))
|
||||
}
|
||||
|
||||
// init wda device
|
||||
targetDevice, err := NewDevice(deviceOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// switch to iOS springboard before init WDA session
|
||||
// aviod getting stuck when some super app is activate such as douyin or wexin
|
||||
log.Info().Msg("switch to iOS springboard")
|
||||
bundleID := "com.apple.springboard"
|
||||
_, err = targetDevice.GIDevice().AppLaunch(bundleID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "launch springboard failed")
|
||||
}
|
||||
|
||||
// init WDA driver
|
||||
capabilities := NewCapabilities()
|
||||
capabilities.WithDefaultAlertAction(AlertActionAccept)
|
||||
driver, err := NewUSBDriver(capabilities, *targetDevice)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||
}
|
||||
driverExt, err := Extend(driver)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to extend WebDriver")
|
||||
}
|
||||
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
|
||||
"snapshotMaxDepth": snapshotMaxDepth,
|
||||
"acceptAlertButtonSelector": acceptAlertButtonSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set appium WDA settings")
|
||||
}
|
||||
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
||||
|
||||
driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port)
|
||||
if device.LogOn {
|
||||
err = driverExt.StartLogRecording("hrp_wda_log")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return driverExt, nil
|
||||
}
|
||||
|
||||
type Device interface {
|
||||
UUID() string
|
||||
}
|
||||
|
||||
type IOSDevice struct {
|
||||
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
||||
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"`
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
|
||||
d giDevice.Device
|
||||
}
|
||||
|
||||
func (d IOSDevice) UUID() string {
|
||||
return d.UDID
|
||||
}
|
||||
|
||||
func (d IOSDevice) GIDevice() giDevice.Device {
|
||||
return d.d
|
||||
}
|
||||
|
||||
type IOSDeviceOption func(*IOSDevice)
|
||||
|
||||
func WithUDID(udid string) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.UDID = udid
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithMjpegPort(port int) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.MjpegPort = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogOn(logOn bool) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.LogOn = logOn
|
||||
}
|
||||
}
|
||||
|
||||
func NewDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
|
||||
var usbmux giDevice.Usbmux
|
||||
if usbmux, err = giDevice.NewUsbmux(); err != nil {
|
||||
return nil, fmt.Errorf("init usbmux failed: %v", err)
|
||||
}
|
||||
|
||||
var deviceList []giDevice.Device
|
||||
if deviceList, err = usbmux.Devices(); err != nil {
|
||||
return nil, fmt.Errorf("get attached devices failed: %v", err)
|
||||
}
|
||||
|
||||
device = &IOSDevice{
|
||||
Port: defaultPort,
|
||||
MjpegPort: defaultMjpegPort,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(device)
|
||||
}
|
||||
|
||||
serialNumber := device.UDID
|
||||
for _, d := range deviceList {
|
||||
// find device by serial number if specified
|
||||
if serialNumber != "" && d.Properties().SerialNumber != serialNumber {
|
||||
continue
|
||||
}
|
||||
|
||||
device.UDID = d.Properties().SerialNumber
|
||||
device.d = d
|
||||
return device, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("device %s not found", device.UDID)
|
||||
}
|
||||
|
||||
func DeviceList() (devices []IOSDevice, err error) {
|
||||
var usbmux giDevice.Usbmux
|
||||
if usbmux, err = giDevice.NewUsbmux(); err != nil {
|
||||
return nil, fmt.Errorf("usbmuxd: %w", err)
|
||||
}
|
||||
|
||||
var deviceList []giDevice.Device
|
||||
if deviceList, err = usbmux.Devices(); err != nil {
|
||||
return nil, fmt.Errorf("device list: %w", err)
|
||||
}
|
||||
|
||||
devices = make([]IOSDevice, len(deviceList))
|
||||
|
||||
for i := range devices {
|
||||
devices[i].UDID = deviceList[i].Properties().SerialNumber
|
||||
devices[i].Port = defaultPort
|
||||
devices[i].MjpegPort = defaultMjpegPort
|
||||
devices[i].d = deviceList[i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewDriver creates new remote client, this will also start a new session.
|
||||
func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (driver WebDriver, err error) {
|
||||
if len(mjpegPort) == 0 {
|
||||
mjpegPort = []int{defaultMjpegPort}
|
||||
}
|
||||
wd := new(remoteWD)
|
||||
if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sessionInfo SessionInfo
|
||||
if sessionInfo, err = wd.NewSession(capabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wd.sessionId = sessionInfo.SessionId
|
||||
|
||||
if wd.mjpegConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), mjpegPort[0])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wd.mjpegClient = convertToHTTPClient(wd.mjpegConn)
|
||||
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
|
||||
func NewUSBDriver(capabilities Capabilities, device ...IOSDevice) (driver WebDriver, err error) {
|
||||
if len(device) == 0 {
|
||||
if device, err = DeviceList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(device) == 0 {
|
||||
return nil, errors.New("no device")
|
||||
}
|
||||
}
|
||||
dev := device[0]
|
||||
|
||||
wd := &remoteWD{
|
||||
usbCli: &struct {
|
||||
httpCli *http.Client
|
||||
defaultConn, mjpegConn giDevice.InnerConn
|
||||
sync.Mutex
|
||||
}{},
|
||||
}
|
||||
if wd.usbCli.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil {
|
||||
return nil, fmt.Errorf("create connection: %w", err)
|
||||
}
|
||||
wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn())
|
||||
|
||||
if wd.usbCli.mjpegConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
|
||||
return nil, fmt.Errorf("create connection MJPEG: %w", err)
|
||||
}
|
||||
wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn())
|
||||
|
||||
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = wd.NewSession(capabilities)
|
||||
|
||||
go func() {
|
||||
if DefaultKeepAliveInterval <= 0 {
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(DefaultKeepAliveInterval)
|
||||
for {
|
||||
<-ticker.C
|
||||
if healthy, err := wd.IsHealthy(); err != nil || !healthy {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return wd, err
|
||||
}
|
||||
|
||||
func newRequest(method string, url string, rawBody []byte) (request *http.Request, err error) {
|
||||
header := map[string]string{
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if request, err = http.NewRequest(method, url, bytes.NewBuffer(rawBody)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range header {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertToHTTPClient(_conn net.Conn) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return _conn, nil
|
||||
},
|
||||
},
|
||||
Timeout: 0,
|
||||
}
|
||||
}
|
||||
|
||||
type wdaResponse struct {
|
||||
Value string `json:"value"`
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) StartLogRecording(identifier string) error {
|
||||
log.Info().Msg("start WDA log recording")
|
||||
data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier}
|
||||
_, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start WDA log recording")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) GetLogs() (string, error) {
|
||||
log.Info().Msg("stop WDA log recording")
|
||||
data := map[string]interface{}{"action": "stop"}
|
||||
reply, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get WDA logs")
|
||||
}
|
||||
|
||||
return reply.Value, nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) {
|
||||
// [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)]
|
||||
postJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/gtf/automation/log", dExt.host)
|
||||
log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log")
|
||||
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
rawResp, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reply := new(wdaResponse)
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
type rawResponse []byte
|
||||
|
||||
func (r rawResponse) checkErr() (err error) {
|
||||
reply := new(struct {
|
||||
Value struct {
|
||||
Err string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Traceback string `json:"traceback"`
|
||||
}
|
||||
})
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
if reply.Value.Err != "" {
|
||||
errText := reply.Value.Message
|
||||
re := regexp.MustCompile(`{.+?=(.+?)}`)
|
||||
if re.MatchString(reply.Value.Message) {
|
||||
subMatch := re.FindStringSubmatch(reply.Value.Message)
|
||||
errText = subMatch[len(subMatch)-1]
|
||||
}
|
||||
return fmt.Errorf("%s: %s", reply.Value.Err, errText)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToString() (s string, err error) {
|
||||
reply := new(struct{ Value string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToBool() (b bool, err error) {
|
||||
reply := new(struct{ Value bool })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return false, err
|
||||
}
|
||||
b = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) {
|
||||
reply := new(struct{ Value struct{ SessionInfo } })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
sessionInfo = reply.Value.SessionInfo
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) {
|
||||
reply := new(struct{ Value builtinJSON.RawMessage })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) {
|
||||
var str string
|
||||
if str, err = r.valueConvertToString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var decodeString []byte
|
||||
if decodeString, err = base64.StdEncoding.DecodeString(str); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = bytes.NewBuffer(decodeString)
|
||||
return
|
||||
}
|
||||
|
||||
var errNoSuchElement = errors.New("no such element")
|
||||
|
||||
func (r rawResponse) valueConvertToElementID() (id string, err error) {
|
||||
reply := new(struct{ Value map[string]string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(reply.Value) == 0 {
|
||||
return "", errNoSuchElement
|
||||
}
|
||||
if id = elementIDFromValue(reply.Value); id == "" {
|
||||
return "", fmt.Errorf("invalid element returned: %+v", reply)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) {
|
||||
reply := new(struct{ Value []map[string]string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reply.Value) == 0 {
|
||||
return nil, errNoSuchElement
|
||||
}
|
||||
IDs = make([]string, len(reply.Value))
|
||||
for i, elem := range reply.Value {
|
||||
var id string
|
||||
if id = elementIDFromValue(elem); id == "" {
|
||||
return nil, fmt.Errorf("invalid element returned: %+v", reply)
|
||||
}
|
||||
IDs[i] = id
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user