mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-11 18:11:21 +08:00
774 lines
22 KiB
Go
774 lines
22 KiB
Go
package uixt
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
builtinLog "log"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/mitchellh/mapstructure"
|
||
"github.com/pkg/errors"
|
||
"github.com/rs/zerolog/log"
|
||
|
||
"github.com/httprunner/httprunner/v4/hrp/code"
|
||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
|
||
)
|
||
|
||
const (
|
||
defaultWDAPort = 8100
|
||
defaultMjpegPort = 9100
|
||
)
|
||
|
||
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 {'不允许','暂不'}`]"
|
||
)
|
||
|
||
type IOSPerfOption = gidevice.PerfOption
|
||
|
||
var (
|
||
WithIOSPerfSystemCPU = gidevice.WithPerfSystemCPU
|
||
WithIOSPerfSystemMem = gidevice.WithPerfSystemMem
|
||
WithIOSPerfSystemDisk = gidevice.WithPerfSystemDisk
|
||
WithIOSPerfSystemNetwork = gidevice.WithPerfSystemNetwork
|
||
WithIOSPerfGPU = gidevice.WithPerfGPU
|
||
WithIOSPerfFPS = gidevice.WithPerfFPS
|
||
WithIOSPerfNetwork = gidevice.WithPerfNetwork
|
||
WithIOSPerfBundleID = gidevice.WithPerfBundleID
|
||
WithIOSPerfPID = gidevice.WithPerfPID
|
||
WithIOSPerfOutputInterval = gidevice.WithPerfOutputInterval
|
||
WithIOSPerfProcessAttributes = gidevice.WithPerfProcessAttributes
|
||
WithIOSPerfSystemAttributes = gidevice.WithPerfSystemAttributes
|
||
)
|
||
|
||
type IOSPcapOption = gidevice.PcapOption
|
||
|
||
var (
|
||
WithIOSPcapAll = gidevice.WithPcapAll
|
||
WithIOSPcapPID = gidevice.WithPcapPID
|
||
WithIOSPcapProcName = gidevice.WithPcapProcName
|
||
WithIOSPcapBundleID = gidevice.WithPcapBundleID
|
||
)
|
||
|
||
type IOSDeviceOption func(*IOSDevice)
|
||
|
||
func WithUDID(udid string) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.UDID = udid
|
||
}
|
||
}
|
||
|
||
func WithWDAPort(port int) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.Port = port
|
||
}
|
||
}
|
||
|
||
func WithWDAMjpegPort(port int) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.MjpegPort = port
|
||
}
|
||
}
|
||
|
||
func WithWDALogOn(logOn bool) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.LogOn = logOn
|
||
}
|
||
}
|
||
|
||
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.ResetHomeOnStartup = reset
|
||
}
|
||
}
|
||
|
||
func WithSnapshotMaxDepth(depth int) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.SnapshotMaxDepth = depth
|
||
}
|
||
}
|
||
|
||
func WithAcceptAlertButtonSelector(selector string) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.AcceptAlertButtonSelector = selector
|
||
}
|
||
}
|
||
|
||
func WithDismissAlertButtonSelector(selector string) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.DismissAlertButtonSelector = selector
|
||
}
|
||
}
|
||
|
||
func WithXCTest(bundleID string) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.XCTestBundleID = bundleID
|
||
}
|
||
}
|
||
|
||
func WithIOSPerfOptions(options ...gidevice.PerfOption) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.PerfOptions = &gidevice.PerfOptions{}
|
||
for _, option := range options {
|
||
option(device.PerfOptions)
|
||
}
|
||
}
|
||
}
|
||
|
||
func WithIOSPcapOptions(options ...gidevice.PcapOption) IOSDeviceOption {
|
||
return func(device *IOSDevice) {
|
||
device.PcapOptions = &gidevice.PcapOptions{}
|
||
for _, option := range options {
|
||
option(device.PcapOptions)
|
||
}
|
||
}
|
||
}
|
||
|
||
func GetIOSDevices(udid ...string) (devices []gidevice.Device, err error) {
|
||
var usbmux gidevice.Usbmux
|
||
if usbmux, err = gidevice.NewUsbmux(); err != nil {
|
||
return nil, errors.Wrap(code.DeviceConnectionError,
|
||
fmt.Sprintf("init usbmux failed: %v", err))
|
||
}
|
||
|
||
if devices, err = usbmux.Devices(); err != nil {
|
||
return nil, errors.Wrap(code.DeviceConnectionError,
|
||
fmt.Sprintf("list ios devices failed: %v", err))
|
||
}
|
||
|
||
// filter by udid
|
||
var deviceList []gidevice.Device
|
||
for _, d := range devices {
|
||
for _, u := range udid {
|
||
if u != "" && u != d.Properties().SerialNumber {
|
||
continue
|
||
}
|
||
// filter non-usb ios devices
|
||
if d.Properties().ConnectionType != "USB" {
|
||
continue
|
||
}
|
||
deviceList = append(deviceList, d)
|
||
}
|
||
}
|
||
|
||
if len(deviceList) == 0 {
|
||
var err error
|
||
if udid == nil || (len(udid) == 1 && udid[0] == "") {
|
||
err = fmt.Errorf("no ios device found")
|
||
} else {
|
||
err = fmt.Errorf("no ios device found for udid %v", udid)
|
||
}
|
||
return nil, err
|
||
}
|
||
return deviceList, nil
|
||
}
|
||
|
||
func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) {
|
||
if dev.UDID != "" {
|
||
deviceOptions = append(deviceOptions, WithUDID(dev.UDID))
|
||
}
|
||
if dev.Port != 0 {
|
||
deviceOptions = append(deviceOptions, WithWDAPort(dev.Port))
|
||
}
|
||
if dev.MjpegPort != 0 {
|
||
deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort))
|
||
}
|
||
if dev.LogOn {
|
||
deviceOptions = append(deviceOptions, WithWDALogOn(true))
|
||
}
|
||
if dev.PerfOptions != nil {
|
||
deviceOptions = append(deviceOptions, WithIOSPerfOptions(dev.perfOpitons()...))
|
||
}
|
||
if dev.PcapOptions != nil {
|
||
deviceOptions = append(deviceOptions, WithIOSPcapOptions(dev.pcapOpitons()...))
|
||
}
|
||
if dev.XCTestBundleID != "" {
|
||
deviceOptions = append(deviceOptions, WithXCTest(dev.XCTestBundleID))
|
||
}
|
||
if dev.ResetHomeOnStartup {
|
||
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
|
||
}
|
||
if dev.SnapshotMaxDepth != 0 {
|
||
deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth))
|
||
}
|
||
if dev.AcceptAlertButtonSelector != "" {
|
||
deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector))
|
||
}
|
||
if dev.DismissAlertButtonSelector != "" {
|
||
deviceOptions = append(deviceOptions, WithDismissAlertButtonSelector(dev.DismissAlertButtonSelector))
|
||
}
|
||
return
|
||
}
|
||
|
||
func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
|
||
device = &IOSDevice{
|
||
Port: defaultWDAPort,
|
||
MjpegPort: defaultMjpegPort,
|
||
SnapshotMaxDepth: snapshotMaxDepth,
|
||
AcceptAlertButtonSelector: acceptAlertButtonSelector,
|
||
DismissAlertButtonSelector: dismissAlertButtonSelector,
|
||
// switch to iOS springboard before init WDA session
|
||
// avoid getting stuck when some super app is active such as douyin or wexin
|
||
ResetHomeOnStartup: true,
|
||
}
|
||
for _, option := range options {
|
||
option(device)
|
||
}
|
||
|
||
deviceList, err := GetIOSDevices(device.UDID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
|
||
}
|
||
|
||
if device.UDID == "" && len(deviceList) > 1 {
|
||
return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the udid")
|
||
}
|
||
|
||
dev := deviceList[0]
|
||
udid := dev.Properties().SerialNumber
|
||
|
||
if device.UDID == "" {
|
||
device.UDID = udid
|
||
log.Warn().
|
||
Str("udid", udid).
|
||
Msg("ios UDID is not specified, select the first one")
|
||
}
|
||
|
||
device.d = dev
|
||
|
||
// run xctest if XCTestBundleID is set
|
||
if device.XCTestBundleID != "" {
|
||
_, err = device.RunXCTest(device.XCTestBundleID)
|
||
if err != nil {
|
||
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
|
||
return
|
||
}
|
||
}
|
||
|
||
log.Info().Str("udid", device.UDID).Msg("init ios device")
|
||
return device, nil
|
||
}
|
||
|
||
type IOSDevice struct {
|
||
d gidevice.Device
|
||
PerfOptions *gidevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"`
|
||
PcapOptions *gidevice.PcapOptions `json:"pcap_options,omitempty" yaml:"pcap_options,omitempty"`
|
||
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
||
Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
|
||
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
|
||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||
XCTestBundleID string `json:"xctest_bundle_id,omitempty" yaml:"xctest_bundle_id,omitempty"`
|
||
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
||
|
||
// switch to iOS springboard before init WDA session
|
||
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
|
||
|
||
// config appium settings
|
||
SnapshotMaxDepth int `json:"snapshot_max_depth,omitempty" yaml:"snapshot_max_depth,omitempty"`
|
||
AcceptAlertButtonSelector string `json:"accept_alert_button_selector,omitempty" yaml:"accept_alert_button_selector,omitempty"`
|
||
DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"`
|
||
|
||
// performance monitor
|
||
perfStop chan struct{} // stop performance monitor
|
||
perfFile string // saved perf file path
|
||
|
||
// pcap monitor
|
||
pcapStop chan struct{} // stop pcap monitor
|
||
pcapFile string // saved pcap file path
|
||
}
|
||
|
||
func (dev *IOSDevice) Init() error {
|
||
return nil
|
||
}
|
||
|
||
func (dev *IOSDevice) UUID() string {
|
||
return dev.UDID
|
||
}
|
||
|
||
func (dev *IOSDevice) LogEnabled() bool {
|
||
return dev.LogOn
|
||
}
|
||
|
||
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
|
||
driverOptions := NewDriverOptions()
|
||
for _, option := range options {
|
||
option(driverOptions)
|
||
}
|
||
|
||
// init WDA driver
|
||
capabilities := driverOptions.capabilities
|
||
if capabilities == nil {
|
||
capabilities = NewCapabilities()
|
||
capabilities.WithDefaultAlertAction(AlertActionAccept)
|
||
}
|
||
|
||
var driver IWebDriver
|
||
if os.Getenv("WDA_USB_DRIVER") == "" {
|
||
// default use http driver
|
||
driver, err = dev.NewHTTPDriver(capabilities)
|
||
} else {
|
||
driver, err = dev.NewUSBDriver(capabilities)
|
||
}
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||
}
|
||
|
||
if dev.ResetHomeOnStartup {
|
||
log.Info().Msg("go back to home screen")
|
||
if err = driver.Homescreen(); err != nil {
|
||
return nil, errors.Wrap(code.MobileUIDriverError,
|
||
fmt.Sprintf("go back to home screen failed: %v", err))
|
||
}
|
||
}
|
||
|
||
driverExt, err = newDriverExt(dev, driver, options...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
|
||
"snapshotMaxDepth": dev.SnapshotMaxDepth,
|
||
"acceptAlertButtonSelector": dev.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")
|
||
|
||
if dev.LogOn {
|
||
err = driverExt.Driver.StartCaptureLog("hrp_wda_log")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if dev.PerfOptions != nil {
|
||
if err := dev.StartPerf(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if dev.PcapOptions != nil {
|
||
if err := dev.StartPcap(); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return driverExt, nil
|
||
}
|
||
|
||
func (dev *IOSDevice) StartPerf() error {
|
||
log.Info().Msg("start performance monitor")
|
||
data, err := dev.d.PerfStart(dev.perfOpitons()...)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
dev.perfFile = filepath.Join(env.ResultsPath, "perf.data")
|
||
file, err := os.OpenFile(dev.perfFile,
|
||
os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o755)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
dev.perfStop = make(chan struct{})
|
||
// start performance monitor
|
||
go func() {
|
||
for {
|
||
select {
|
||
case <-dev.perfStop:
|
||
file.Close()
|
||
dev.d.PerfStop()
|
||
return
|
||
case d := <-data:
|
||
_, err = file.WriteString(string(d) + "\n")
|
||
if err != nil {
|
||
log.Error().Err(err).
|
||
Str("line", string(d)).
|
||
Msg("write perf data failed")
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
return nil
|
||
}
|
||
|
||
func (dev *IOSDevice) StopPerf() string {
|
||
if dev.perfStop == nil {
|
||
return ""
|
||
}
|
||
close(dev.perfStop)
|
||
log.Info().Str("perfFile", dev.perfFile).Msg("stop performance monitor")
|
||
return dev.perfFile
|
||
}
|
||
|
||
func (dev *IOSDevice) StartPcap() error {
|
||
log.Info().Msg("start packet capture")
|
||
packets, err := dev.d.PcapStart(dev.pcapOpitons()...)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
dev.pcapFile = filepath.Join(env.ResultsPath, "dump.pcap")
|
||
file, err := os.OpenFile(dev.pcapFile,
|
||
os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o755)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// pcap magic number
|
||
// https://www.ietf.org/archive/id/draft-gharris-opsawg-pcap-01.html
|
||
_, _ = file.Write([]byte{
|
||
0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00,
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
0xff, 0xff, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||
})
|
||
|
||
dev.pcapStop = make(chan struct{})
|
||
// start pcap monitor
|
||
go func() {
|
||
for {
|
||
select {
|
||
case <-dev.pcapStop:
|
||
file.Close()
|
||
dev.d.PcapStop()
|
||
return
|
||
case d := <-packets:
|
||
_, err = file.Write(d)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("write pcap data failed")
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
return nil
|
||
}
|
||
|
||
// StopPcap stops pcap monitor and returns the saved pcap file path
|
||
func (dev *IOSDevice) StopPcap() string {
|
||
if dev.pcapStop == nil {
|
||
return ""
|
||
}
|
||
close(dev.pcapStop)
|
||
log.Info().Str("pcapFile", dev.pcapFile).Msg("stop packet capture")
|
||
return dev.pcapFile
|
||
}
|
||
|
||
func (dev *IOSDevice) Install(appPath string, opts *InstallOptions) (err error) {
|
||
for i := 0; i <= opts.RetryTime; i++ {
|
||
err = builtin.RunCommand("go-ios", "install", "--path="+appPath, "--udid="+dev.UDID)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
func (dev *IOSDevice) Uninstall(bundleId string) error {
|
||
return builtin.RunCommand("go-ios", "uninstall", bundleId, "--udid="+dev.UDID)
|
||
}
|
||
|
||
func (dev *IOSDevice) forward(localPort, remotePort int) error {
|
||
log.Info().Int("localPort", localPort).Int("remotePort", remotePort).
|
||
Str("udid", dev.UDID).Msg("forward tcp port")
|
||
|
||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("listen tcp error")
|
||
return err
|
||
}
|
||
|
||
go func(listener net.Listener, device gidevice.Device) {
|
||
for {
|
||
accept, err := listener.Accept()
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("accept error")
|
||
continue
|
||
}
|
||
|
||
rInnerConn, err := device.NewConnect(remotePort)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("connect to ios device failed")
|
||
os.Exit(code.GetErrorCode(code.DeviceConnectionError))
|
||
}
|
||
|
||
rConn := rInnerConn.RawConn()
|
||
_ = rConn.SetDeadline(time.Time{})
|
||
|
||
go func(lConn net.Conn) {
|
||
go func(lConn, rConn net.Conn) {
|
||
if _, err := io.Copy(lConn, rConn); err != nil {
|
||
log.Error().Err(err).Msg("copy local -> remote")
|
||
rConn.Close()
|
||
accept.Close()
|
||
}
|
||
}(lConn, rConn)
|
||
go func(lConn, rConn net.Conn) {
|
||
if _, err := io.Copy(rConn, lConn); err != nil {
|
||
log.Error().Err(err).Msg("copy local <- remote")
|
||
rConn.Close()
|
||
accept.Close()
|
||
}
|
||
}(lConn, rConn)
|
||
}(accept)
|
||
}
|
||
}(listener, dev.d)
|
||
|
||
return nil
|
||
}
|
||
|
||
func (dev *IOSDevice) perfOpitons() (perfOptions []gidevice.PerfOption) {
|
||
if dev.PerfOptions == nil {
|
||
return
|
||
}
|
||
|
||
// system
|
||
if dev.PerfOptions.SysCPU {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfSystemCPU(true))
|
||
}
|
||
if dev.PerfOptions.SysMem {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfSystemMem(true))
|
||
}
|
||
if dev.PerfOptions.SysDisk {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfSystemDisk(true))
|
||
}
|
||
if dev.PerfOptions.SysNetwork {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfSystemNetwork(true))
|
||
}
|
||
if dev.PerfOptions.FPS {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfFPS(true))
|
||
}
|
||
if dev.PerfOptions.Network {
|
||
perfOptions = append(perfOptions, gidevice.WithPerfNetwork(true))
|
||
}
|
||
|
||
// process
|
||
if dev.PerfOptions.BundleID != "" {
|
||
perfOptions = append(perfOptions,
|
||
gidevice.WithPerfBundleID(dev.PerfOptions.BundleID))
|
||
}
|
||
if dev.PerfOptions.Pid != 0 {
|
||
perfOptions = append(perfOptions,
|
||
gidevice.WithPerfPID(dev.PerfOptions.Pid))
|
||
}
|
||
|
||
// config
|
||
if dev.PerfOptions.OutputInterval != 0 {
|
||
perfOptions = append(perfOptions,
|
||
gidevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval))
|
||
}
|
||
if dev.PerfOptions.SystemAttributes != nil {
|
||
perfOptions = append(perfOptions,
|
||
gidevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...))
|
||
}
|
||
if dev.PerfOptions.ProcessAttributes != nil {
|
||
perfOptions = append(perfOptions,
|
||
gidevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...))
|
||
}
|
||
return
|
||
}
|
||
|
||
func (dev *IOSDevice) pcapOpitons() (pcapOptions []gidevice.PcapOption) {
|
||
if dev.PcapOptions == nil {
|
||
return
|
||
}
|
||
|
||
if dev.PcapOptions.All {
|
||
pcapOptions = append(pcapOptions, gidevice.WithPcapAll(true))
|
||
}
|
||
if dev.PcapOptions.Pid > 0 {
|
||
pcapOptions = append(pcapOptions, gidevice.WithPcapPID(dev.PcapOptions.Pid))
|
||
}
|
||
if dev.PcapOptions.ProcName != "" {
|
||
pcapOptions = append(pcapOptions, gidevice.WithPcapProcName(dev.PcapOptions.ProcName))
|
||
}
|
||
if dev.PcapOptions.BundleID != "" {
|
||
pcapOptions = append(pcapOptions, gidevice.WithPcapBundleID(dev.PcapOptions.BundleID))
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
|
||
func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver IWebDriver, err error) {
|
||
var localPort int
|
||
localPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_PORT"))
|
||
if err != nil {
|
||
localPort, err = builtin.GetFreePort()
|
||
if err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError,
|
||
fmt.Sprintf("get free port failed: %v", err))
|
||
}
|
||
|
||
if err = dev.forward(localPort, dev.Port); err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError,
|
||
fmt.Sprintf("forward tcp port failed: %v", err))
|
||
}
|
||
} else {
|
||
log.Info().Int("WDA_LOCAL_PORT", localPort).Msg("reuse WDA local port")
|
||
}
|
||
|
||
var localMjpegPort int
|
||
localMjpegPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_MJPEG_PORT"))
|
||
if err != nil {
|
||
localMjpegPort, err = builtin.GetFreePort()
|
||
if err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError,
|
||
fmt.Sprintf("get free port failed: %v", err))
|
||
}
|
||
if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError,
|
||
fmt.Sprintf("forward tcp port failed: %v", err))
|
||
}
|
||
} else {
|
||
log.Info().Int("WDA_LOCAL_MJPEG_PORT", localMjpegPort).
|
||
Msg("reuse WDA local mjpeg port")
|
||
}
|
||
|
||
log.Info().Interface("capabilities", capabilities).
|
||
Int("localPort", localPort).Int("localMjpegPort", localMjpegPort).
|
||
Msg("init WDA HTTP driver")
|
||
|
||
wd := new(wdaDriver)
|
||
wd.client = http.DefaultClient
|
||
|
||
host := "localhost"
|
||
if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||
}
|
||
if _, err = wd.NewSession(capabilities); err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||
}
|
||
|
||
if wd.mjpegHTTPConn, err = net.Dial(
|
||
"tcp",
|
||
fmt.Sprintf("%s:%d", host, localMjpegPort),
|
||
); err != nil {
|
||
return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error())
|
||
}
|
||
wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn)
|
||
|
||
// init WDA scale
|
||
if wd.scale, err = wd.Scale(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return wd, nil
|
||
}
|
||
|
||
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
|
||
func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver IWebDriver, err error) {
|
||
log.Info().Interface("capabilities", capabilities).
|
||
Str("udid", dev.UDID).Msg("init WDA USB driver")
|
||
|
||
wd := new(wdaDriver)
|
||
if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil {
|
||
return nil, errors.Wrap(code.DeviceUSBDriverError,
|
||
fmt.Sprintf("connect port %d failed: %v", dev.Port, err))
|
||
}
|
||
wd.client = convertToHTTPClient(wd.defaultConn.RawConn())
|
||
|
||
if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
|
||
return nil, errors.Wrap(code.DeviceUSBDriverError,
|
||
fmt.Sprintf("connect MJPEG port %d failed: %v", dev.MjpegPort, err))
|
||
}
|
||
wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn())
|
||
|
||
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
|
||
return nil, errors.Wrap(code.DeviceUSBDriverError, err.Error())
|
||
}
|
||
if _, err = wd.NewSession(capabilities); err != nil {
|
||
return nil, errors.Wrap(code.DeviceUSBDriverError, err.Error())
|
||
}
|
||
|
||
// init WDA scale
|
||
if wd.scale, err = wd.Scale(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return wd, nil
|
||
}
|
||
|
||
func (dev *IOSDevice) RunXCTest(bundleID string) (cancel context.CancelFunc, err error) {
|
||
log.Info().Str("bundleID", bundleID).Msg("run xctest")
|
||
out, cancel, err := dev.d.XCTest(bundleID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "run xctest failed")
|
||
}
|
||
// wait for xctest to start
|
||
time.Sleep(5 * time.Second)
|
||
|
||
f, err := os.OpenFile(fmt.Sprintf("xctest_%s.log", dev.UDID),
|
||
os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer builtinLog.SetOutput(f)
|
||
|
||
// print xctest running logs
|
||
go func() {
|
||
for s := range out {
|
||
builtinLog.Print(s)
|
||
}
|
||
f.Close()
|
||
}()
|
||
|
||
return cancel, nil
|
||
}
|
||
|
||
type Application struct {
|
||
CFBundleVersion string `json:"version"`
|
||
CFBundleDisplayName string `json:"name"`
|
||
CFBundleIdentifier string `json:"bundleId"`
|
||
}
|
||
|
||
func (dev *IOSDevice) GetPackageInfo(packageName string) (AppInfo, error) {
|
||
appInfo := AppInfo{
|
||
Name: packageName,
|
||
}
|
||
applicationType := gidevice.ApplicationTypeAny
|
||
result, err := dev.d.InstallationProxyBrowse(
|
||
gidevice.WithApplicationType(applicationType),
|
||
gidevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier"))
|
||
if err != nil {
|
||
return appInfo, errors.Wrap(code.DeviceShellExecError,
|
||
fmt.Sprintf("get app list failed %v", err))
|
||
}
|
||
|
||
for _, app := range result {
|
||
a := Application{}
|
||
mapstructure.Decode(app, &a)
|
||
|
||
if a.CFBundleIdentifier != packageName {
|
||
continue
|
||
}
|
||
appInfo.AppBaseInfo = AppBaseInfo{
|
||
BundleId: a.CFBundleIdentifier,
|
||
VersionName: a.CFBundleVersion,
|
||
AppName: a.CFBundleDisplayName,
|
||
}
|
||
log.Info().Interface("appInfo", appInfo).Msg("get app info")
|
||
return appInfo, nil
|
||
}
|
||
return appInfo, errors.New("failed to get package version")
|
||
}
|