diff --git a/config.go b/config.go index 6302f3d7..f360a82a 100644 --- a/config.go +++ b/config.go @@ -119,9 +119,9 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { } func (c *TConfig) SetIOS(opts ...option.IOSDeviceOption) *TConfig { - iosOptions := option.NewIOSDeviceConfig(opts...) + iosOptions := option.NewIOSDeviceOptions(opts...) device := &uixt.IOSDevice{ - IOSDeviceConfig: iosOptions, + IOSDeviceOptions: iosOptions, } // each device can have its own settings @@ -140,9 +140,9 @@ func (c *TConfig) SetIOS(opts ...option.IOSDeviceOption) *TConfig { } func (c *TConfig) SetHarmony(opts ...option.HarmonyDeviceOption) *TConfig { - harmonyOptions := option.NewHarmonyDeviceConfig(opts...) + harmonyOptions := option.NewHarmonyDeviceOptions(opts...) device := &uixt.HarmonyDevice{ - HarmonyDeviceConfig: harmonyOptions, + HarmonyDeviceOptions: harmonyOptions, } // each device can have its own settings @@ -161,9 +161,9 @@ func (c *TConfig) SetHarmony(opts ...option.HarmonyDeviceOption) *TConfig { } func (c *TConfig) SetAndroid(opts ...option.AndroidDeviceOption) *TConfig { - uiaOptions := option.NewAndroidDeviceConfig(opts...) + uiaOptions := option.NewAndroidDeviceOptions(opts...) device := &uixt.AndroidDevice{ - AndroidDeviceConfig: uiaOptions, + AndroidDeviceOptions: uiaOptions, } // each device can have its own settings diff --git a/internal/version/VERSION b/internal/version/VERSION index deb4d166..12ee2523 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2502062237 +v5.0.0+2502071516 diff --git a/pkg/uixt/android_device.go b/pkg/uixt/android_device.go index 2eec404e..f6eaf353 100644 --- a/pkg/uixt/android_device.go +++ b/pkg/uixt/android_device.go @@ -27,19 +27,11 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -var ( - DouyinServerPort = 32316 - +const ( // adb server AdbServerHost = "localhost" AdbServerPort = gadb.AdbServerPort // 5037 - // uiautomator2 server - UIA2ServerHost = "localhost" - UIA2ServerPort = 6790 - UIA2ServerPackageName = "io.appium.uiautomator2.server" - UIA2ServerTestPackageName = "io.appium.uiautomator2.server.test" - EvalInstallerPackageName = "sogou.mobile.explorer" InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d" ) @@ -48,13 +40,8 @@ var ( var evalite embed.FS func NewAndroidDevice(opts ...option.AndroidDeviceOption) (device *AndroidDevice, err error) { - androidOptions := &option.AndroidDeviceConfig{ - UIA2IP: UIA2ServerHost, - UIA2Port: UIA2ServerPort, - } - for _, option := range opts { - option(androidOptions) - } + androidOptions := option.NewAndroidDeviceOptions(opts...) + deviceList, err := GetAndroidDevices(androidOptions.SerialNumber) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, err.Error()) @@ -75,9 +62,9 @@ func NewAndroidDevice(opts ...option.AndroidDeviceOption) (device *AndroidDevice } device = &AndroidDevice{ - AndroidDeviceConfig: androidOptions, - d: dev, - logcat: NewAdbLogcat(device.SerialNumber), + Device: dev, + AndroidDeviceOptions: androidOptions, + Logcat: NewAdbLogcat(androidOptions.SerialNumber), } evalToolRaw, err := evalite.ReadFile("evalite") @@ -126,30 +113,30 @@ func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) { } type AndroidDevice struct { - *option.AndroidDeviceConfig - d *gadb.Device - logcat *AdbLogcat + *gadb.Device + *option.AndroidDeviceOptions + Logcat *AdbLogcat } -func (dev *AndroidDevice) Init() error { - dev.d.RunShellCommand("ime", "enable", UnicodeImePackageName) - dev.d.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath) +func (dev *AndroidDevice) Setup() error { + dev.RunShellCommand("ime", "enable", UnicodeImePackageName) + dev.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath) if dev.UIA2 { // uiautomator2 server must be started before // check uiautomator server package installed - if !dev.d.IsPackageInstalled(UIA2ServerPackageName) { + if !dev.IsPackageInstalled(dev.UIA2ServerPackageName) { return errors.Wrapf(code.MobileUIDriverAppNotInstalled, - "%s not installed", UIA2ServerPackageName) + "%s not installed", dev.UIA2ServerPackageName) } - if !dev.d.IsPackageInstalled(UIA2ServerTestPackageName) { + if !dev.IsPackageInstalled(dev.UIA2ServerTestPackageName) { return errors.Wrapf(code.MobileUIDriverAppNotInstalled, - "%s not installed", UIA2ServerTestPackageName) + "%s not installed", dev.UIA2ServerTestPackageName) } // TODO: check uiautomator server package running - // if dev.d.IsPackageRunning(UIA2ServerPackageName) { + // if dev.IsPackageRunning(UIA2ServerPackageName) { // return nil // } @@ -164,6 +151,10 @@ func (dev *AndroidDevice) Init() error { return nil } +func (dev *AndroidDevice) Teardown() error { + return nil +} + func (dev *AndroidDevice) UUID() string { return dev.SerialNumber } @@ -173,13 +164,13 @@ func (dev *AndroidDevice) LogEnabled() bool { } func (dev *AndroidDevice) NewDriver(opts ...option.DriverOption) (driverExt *DriverExt, err error) { - var driver IWebDriver + var driver IDriver if dev.UIA2 || dev.LogOn { - driver, err = NewUIA2Driver(dev, opts...) + driver, err = NewUIA2Driver(dev) } else if dev.STUB { - driver, err = NewStubDriver(dev, opts...) + driver, err = NewStubDriver(dev) } else { - driver, err = NewADBDriver(dev, opts...) + driver, err = NewADBDriver(dev) } if err != nil { return nil, errors.Wrap(err, "failed to init UIA driver") @@ -221,13 +212,13 @@ func (dev *AndroidDevice) StopPcap() string { } func (dev *AndroidDevice) Uninstall(packageName string) error { - _, err := dev.d.RunShellCommand("uninstall", packageName) + _, err := dev.RunShellCommand("uninstall", packageName) return err } func (dev *AndroidDevice) Install(apkPath string, opts ...option.InstallOption) error { installOpts := option.NewInstallOptions(opts...) - brand, err := dev.d.Brand() + brand, err := dev.Brand() if err != nil { return err } @@ -245,7 +236,7 @@ func (dev *AndroidDevice) Install(apkPath string, opts ...option.InstallOption) case "vivo": return dev.installVivoSilent(apkPath, args...) case "oppo", "realme", "oneplus": - if dev.d.IsPackageInstalled(EvalInstallerPackageName) { + if dev.IsPackageInstalled(EvalInstallerPackageName) { return dev.installViaInstaller(apkPath, args...) } log.Warn().Msg("oppo not install eval installer") @@ -263,13 +254,13 @@ func (dev *AndroidDevice) installVivoSilent(apkPath string, args ...string) erro verifyCode = verifyCode[:8] verifyCode = "-V" + verifyCode args = append([]string{verifyCode}, args...) - _, err := dev.d.InstallAPK(apkPath, args...) + _, err := dev.InstallAPK(apkPath, args...) return err } func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) error { appRemotePath := "/data/local/tmp/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".apk" - err := dev.d.PushFile(apkPath, appRemotePath, time.Now()) + err := dev.PushFile(apkPath, appRemotePath, time.Now()) if err != nil { return err } @@ -277,7 +268,7 @@ func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) er defer func() { close(done) }() - logcat := NewAdbLogcatWithCallback(dev.d.Serial(), func(line string) { + logcat := NewAdbLogcatWithCallback(dev.Serial(), func(line string) { re := regexp.MustCompile(`\{.*?}`) match := re.FindString(line) if match == "" { @@ -307,7 +298,7 @@ func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) er // 需要监听是否完成安装 command := strings.Split(InstallViaInstallerCommand, " ") args = append(command, appRemotePath) - _, err = dev.d.RunShellCommand("am", args[1:]...) + _, err = dev.RunShellCommand("am", args[1:]...) if err != nil { return err } @@ -322,13 +313,13 @@ func (dev *AndroidDevice) installViaInstaller(apkPath string, args ...string) er } func (dev *AndroidDevice) installCommon(apkPath string, args ...string) error { - _, err := dev.d.InstallAPK(apkPath, args...) + _, err := dev.InstallAPK(apkPath, args...) return err } func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) { // adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp' - output, err := dev.d.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'") + output, err := dev.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'") if err != nil { return WindowInfo{}, errors.Wrap(err, "get current window failed") } @@ -354,7 +345,7 @@ func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) } // adb shell dumpsys activity activities | grep mResumedActivity - output, err = dev.d.RunShellCommand("dumpsys", "activity", "activities", "|", "grep", "mResumedActivity") + output, err = dev.RunShellCommand("dumpsys", "activity", "activities", "|", "grep", "mResumedActivity") if err != nil { return WindowInfo{}, errors.Wrap(err, "get current activity failed") } @@ -408,7 +399,7 @@ func (dev *AndroidDevice) GetPackageInfo(packageName string) (AppInfo, error) { } func (dev *AndroidDevice) getPackageVersion(packageName string) (string, error) { - output, err := dev.d.RunShellCommand("dumpsys", "package", packageName, "|", "grep", "versionName") + output, err := dev.RunShellCommand("dumpsys", "package", packageName, "|", "grep", "versionName") if err != nil { return "", errors.Wrap(err, "get package version failed") } @@ -423,7 +414,7 @@ func (dev *AndroidDevice) getPackageVersion(packageName string) (string, error) } func (dev *AndroidDevice) getPackagePath(packageName string) (string, error) { - output, err := dev.d.RunShellCommand("pm", "path", packageName) + output, err := dev.RunShellCommand("pm", "path", packageName) if err != nil { return "", errors.Wrap(err, "get package path failed") } @@ -436,7 +427,7 @@ func (dev *AndroidDevice) getPackagePath(packageName string) (string, error) { } func (dev *AndroidDevice) getPackageMD5(packagePath string) (string, error) { - output, err := dev.d.RunShellCommand("md5sum", packagePath) + output, err := dev.RunShellCommand("md5sum", packagePath) if err != nil { return "", errors.Wrap(err, "get package md5 failed") } @@ -450,12 +441,12 @@ func (dev *AndroidDevice) getPackageMD5(packagePath string) (string, error) { func (dev *AndroidDevice) startUIA2Server() error { const maxRetries = 3 for attempt := 1; attempt <= maxRetries; attempt++ { - log.Info().Str("package", UIA2ServerTestPackageName). + log.Info().Str("package", dev.UIA2ServerTestPackageName). Int("attempt", attempt).Msg("start uiautomator server") // $ adb shell am instrument -w $UIA2ServerTestPackageName // -w: wait for instrumentation to finish before returning. // Required for test runners. - out, err := dev.d.RunShellCommand("am", "instrument", "-w", UIA2ServerTestPackageName) + out, err := dev.RunShellCommand("am", "instrument", "-w", dev.UIA2ServerTestPackageName) if err != nil { return errors.Wrap(err, "start uiautomator server failed") } @@ -469,7 +460,7 @@ func (dev *AndroidDevice) startUIA2Server() error { } func (dev *AndroidDevice) stopUIA2Server() error { - _, err := dev.d.RunShellCommand("am", "force-stop", UIA2ServerPackageName) + _, err := dev.RunShellCommand("am", "force-stop", dev.UIA2ServerPackageName) return err } diff --git a/pkg/uixt/android_driver_adb.go b/pkg/uixt/android_driver_adb.go index 5f9ca039..357f8698 100644 --- a/pkg/uixt/android_driver_adb.go +++ b/pkg/uixt/android_driver_adb.go @@ -23,7 +23,6 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/utf7" - "github.com/httprunner/httprunner/v5/pkg/gadb" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -32,20 +31,18 @@ const ( UnicodeImePackageName = "io.appium.settings/.UnicodeIME" ) -func NewADBDriver(device *AndroidDevice, opts ...option.DriverOption) (*ADBDriver, error) { +func NewADBDriver(device *AndroidDevice) (*ADBDriver, error) { log.Info().Interface("device", device).Msg("init android adb driver") driver := &ADBDriver{} driver.NewSession(nil) - driver.adbClient = device.d - driver.logcat = device.logcat + driver.Device = device.Device + driver.Logcat = device.Logcat return driver, nil } type ADBDriver struct { - DriverClient - - adbClient *gadb.Device - logcat *AdbLogcat + *AndroidDevice + *DriverClient } func (ad *ADBDriver) runShellCommand(cmd string, args ...string) (output string, err error) { @@ -68,7 +65,7 @@ func (ad *ADBDriver) runShellCommand(cmd string, args ...string) (output string, // adb shell screencap -p if cmd == "screencap" { - resp, err := ad.adbClient.ScreenCap() + resp, err := ad.Device.ScreenCap() if err == nil { driverResult.ResponseBody = "OMITTED" return string(resp), nil @@ -76,7 +73,7 @@ func (ad *ADBDriver) runShellCommand(cmd string, args ...string) (output string, return "", errors.Wrap(err, "adb screencap failed") } - output, err = ad.adbClient.RunShellCommand(cmd, args...) + output, err = ad.Device.RunShellCommand(cmd, args...) driverResult.ResponseBody = strings.TrimSpace(output) return output, err } @@ -792,7 +789,7 @@ func (ad *ADBDriver) IsHealthy() (healthy bool, err error) { func (ad *ADBDriver) StartCaptureLog(identifier ...string) (err error) { log.Info().Msg("start adb log recording") // start logcat - err = ad.logcat.CatchLogcat("iesqaMonitor:V") + err = ad.Logcat.CatchLogcat("iesqaMonitor:V") if err != nil { err = errors.Wrap(code.DeviceCaptureLogError, fmt.Sprintf("start adb log recording failed: %v", err)) @@ -804,7 +801,7 @@ func (ad *ADBDriver) StartCaptureLog(identifier ...string) (err error) { func (ad *ADBDriver) StopCaptureLog() (result interface{}, err error) { defer func() { log.Info().Msg("stop adb log recording") - err = ad.logcat.Stop() + err = ad.Logcat.Stop() if err != nil { log.Error().Err(err).Msg("failed to get adb log recording") } @@ -812,14 +809,14 @@ func (ad *ADBDriver) StopCaptureLog() (result interface{}, err error) { if err != nil { log.Error().Err(err).Msg("failed to close adb log writer") } - pointRes := ConvertPoints(ad.logcat.logs) + pointRes := ConvertPoints(ad.Logcat.logs) // 没有解析到打点日志,走兜底逻辑 if len(pointRes) == 0 { log.Info().Msg("action log is null, use action file >>>") logFilePathPrefix := fmt.Sprintf("%v/data", config.ActionLogFilePath) files := []string{} - ad.adbClient.RunShellCommand("pull", config.DeviceActionLogFilePath, config.ActionLogFilePath) + ad.Device.RunShellCommand("pull", config.DeviceActionLogFilePath, config.ActionLogFilePath) err = filepath.Walk(config.ActionLogFilePath, func(path string, info fs.FileInfo, err error) error { // 只是需要日志文件 if ok := strings.Contains(path, logFilePathPrefix); ok { @@ -898,7 +895,7 @@ func (ad *ADBDriver) SetIme(imeRegx string) error { if ime == "" { return fmt.Errorf("failed to set ime by %s, ime list: %v", imeRegx, imeList) } - brand, _ := ad.adbClient.Brand() + brand, _ := ad.Device.Brand() packageName := strings.Split(ime, "/")[0] res, err := ad.runShellCommand("ime", "set", ime) log.Info().Str("funcName", "SetIme").Interface("ime", ime). @@ -1061,7 +1058,7 @@ func (ad *ADBDriver) RecordScreen(folderPath string, duration time.Duration) (vi // scrcpy -s 7d21bb91 --record=file.mp4 -N cmd := exec.Command( "scrcpy", - "-s", ad.adbClient.Serial(), + "-s", ad.Device.Serial(), fmt.Sprintf("--record=%s", fileName), "-N", ) diff --git a/pkg/uixt/android_driver_stub.go b/pkg/uixt/android_driver_stub.go index d821cb0e..33d45fac 100644 --- a/pkg/uixt/android_driver_stub.go +++ b/pkg/uixt/android_driver_stub.go @@ -17,17 +17,20 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -const StubSocketName = "com.bytest.device" +const ( + StubSocketName = "com.bytest.device" + DouyinServerPort = 32316 +) -func NewStubDriver(device *AndroidDevice, opts ...option.DriverOption) (driver *StubAndroidDriver, err error) { - socketLocalPort, err := device.d.Forward(StubSocketName) +func NewStubDriver(device *AndroidDevice) (driver *StubAndroidDriver, err error) { + socketLocalPort, err := device.Forward(StubSocketName) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, fmt.Sprintf("forward port %d->%s failed: %v", socketLocalPort, StubSocketName, err)) } - serverLocalPort, err := device.d.Forward(DouyinServerPort) + serverLocalPort, err := device.Forward(DouyinServerPort) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, fmt.Sprintf("forward port %d->%d failed: %v", @@ -53,16 +56,16 @@ func NewStubDriver(device *AndroidDevice, opts ...option.DriverOption) (driver * } driver.NewSession(nil) - driver.adbClient = device.d - driver.logcat = device.logcat + driver.Device = device.Device + driver.Logcat = device.Logcat return driver, nil } type StubAndroidDriver struct { + *ADBDriver socket net.Conn seq int timeout time.Duration - ADBDriver } type AppLoginInfo struct { @@ -85,7 +88,7 @@ func (sad *StubAndroidDriver) httpGET(pathElem ...string) (rawResp rawResponse, if err != nil { return nil, fmt.Errorf("adb forward: %w", err) } - sad.client = convertToHTTPClient(conn) + sad.Client = convertToHTTPClient(conn) return sad.Request(http.MethodGet, sad.concatURL(nil, pathElem...), nil) } @@ -103,7 +106,7 @@ func (sad *StubAndroidDriver) httpPOST(data interface{}, pathElem ...string) (ra if err != nil { return nil, fmt.Errorf("adb forward: %w", err) } - sad.client = convertToHTTPClient(conn) + sad.Client = convertToHTTPClient(conn) var bsJSON []byte = nil if data != nil { @@ -137,7 +140,7 @@ func (sad *StubAndroidDriver) sendCommand(packageName string, cmdType string, pa return nil, err } - res, err := sad.adbClient.RunStubCommand(append(data, '\n'), packageName) + res, err := sad.Device.RunStubCommand(append(data, '\n'), packageName) if err != nil { return nil, err } @@ -266,7 +269,7 @@ func (sad *StubAndroidDriver) LoginNoneUIDynamic(packageName, phoneNumber string } func (sad *StubAndroidDriver) SetHDTStatus(status bool) error { - _, err := sad.adbClient.RunShellCommand("settings", "put", "global", "feedbacker_sso_bypass_token", "default_sso_bypass_token") + _, err := sad.Device.RunShellCommand("settings", "put", "global", "feedbacker_sso_bypass_token", "default_sso_bypass_token") if err != nil { log.Warn().Msg(fmt.Sprintf("failed to disable sso, error: %v", err)) } diff --git a/pkg/uixt/android_driver_uia2.go b/pkg/uixt/android_driver_uia2.go index d0d64004..2115ceca 100644 --- a/pkg/uixt/android_driver_uia2.go +++ b/pkg/uixt/android_driver_uia2.go @@ -23,9 +23,9 @@ var errDriverNotImplemented = errors.New("driver method not implemented") const forwardToPrefix = "forward-to-" -func NewUIA2Driver(device *AndroidDevice, opts ...option.DriverOption) (*UIA2Driver, error) { +func NewUIA2Driver(device *AndroidDevice) (*UIA2Driver, error) { log.Info().Interface("device", device).Msg("init android UIA2 driver") - localPort, err := device.d.Forward(device.UIA2Port) + localPort, err := device.Forward(device.UIA2Port) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, fmt.Sprintf("forward port %d->%d failed: %v", @@ -36,9 +36,9 @@ func NewUIA2Driver(device *AndroidDevice, opts ...option.DriverOption) (*UIA2Dri return nil, fmt.Errorf("adb forward: %w", err) } driver := new(UIA2Driver) - driver.client = convertToHTTPClient(conn) - driver.adbClient = device.d - driver.logcat = device.logcat + driver.Client = convertToHTTPClient(conn) + driver.Device = device.Device + driver.Logcat = device.Logcat _, err = driver.NewSession(nil) if err != nil { @@ -48,35 +48,7 @@ func NewUIA2Driver(device *AndroidDevice, opts ...option.DriverOption) (*UIA2Dri } type UIA2Driver struct { - ADBDriver -} - -type BatteryStatus int - -const ( - _ = iota - BatteryStatusUnknown BatteryStatus = iota - BatteryStatusCharging - BatteryStatusDischarging - BatteryStatusNotCharging - BatteryStatusFull -) - -func (bs BatteryStatus) String() string { - switch bs { - case BatteryStatusUnknown: - return "unknown" - case BatteryStatusCharging: - return "charging" - case BatteryStatusDischarging: - return "discharging" - case BatteryStatusNotCharging: - return "not charging" - case BatteryStatusFull: - return "full" - default: - return fmt.Sprintf("unknown status code (%d)", bs) - } + *ADBDriver } func (ud *UIA2Driver) resetDriver() error { diff --git a/pkg/uixt/device.go b/pkg/uixt/device.go index 09136b96..44bfe610 100644 --- a/pkg/uixt/device.go +++ b/pkg/uixt/device.go @@ -6,11 +6,13 @@ import ( // current implemeted device: IOSDevice, AndroidDevice, HarmonyDevice type IDevice interface { - Init() error // init android device + Setup() error + Teardown() error + UUID() string // ios udid or android serial LogEnabled() bool - // TODO: add ctx to NewDriver + // TODO: remove NewDriver(...option.DriverOption) (driverExt *DriverExt, err error) Install(appPath string, opts ...option.InstallOption) error @@ -18,10 +20,4 @@ type IDevice interface { GetPackageInfo(packageName string) (AppInfo, error) GetCurrentWindow() (windowInfo WindowInfo, err error) - - // Teardown() error -} - -func NewDriver(device IDevice, opts ...option.DriverOption) (driver IWebDriver, err error) { - return } diff --git a/pkg/uixt/driver.go b/pkg/uixt/driver.go index 8720f11b..cc4b9ce0 100644 --- a/pkg/uixt/driver.go +++ b/pkg/uixt/driver.go @@ -25,8 +25,8 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -// IWebDriver defines methods supported by IWebDriver drivers. -type IWebDriver interface { +// current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver +type IDriver interface { // NewSession starts a new session and returns the SessionInfo. NewSession(capabilities option.Capabilities) (SessionInfo, error) @@ -238,7 +238,7 @@ type DriverResult struct { type DriverClient struct { urlPrefix *url.URL - client *http.Client + Client *http.Client // cache to avoid repeated query scale float64 @@ -323,7 +323,7 @@ func (wd *DriverClient) Request(method string, rawURL string, rawBody []byte) (r driverResult.RequestTime = time.Now() var resp *http.Response - if resp, err = wd.client.Do(req); err != nil { + if resp, err = wd.Client.Do(req); err != nil { return nil, err } defer func() { @@ -371,14 +371,14 @@ func convertToHTTPClient(conn net.Conn) *http.Client { type DriverExt struct { Ctx context.Context Device IDevice - Driver IWebDriver + Driver IDriver ImageService IImageService // used to extract image data // funplugin plugin funplugin.IPlugin } -func newDriverExt(device IDevice, driver IWebDriver, opts ...option.DriverOption) (dExt *DriverExt, err error) { +func newDriverExt(device IDevice, driver IDriver, opts ...option.DriverOption) (dExt *DriverExt, err error) { options := option.NewDriverOptions(opts...) dExt = &DriverExt{ @@ -404,7 +404,7 @@ func newDriverExt(device IDevice, driver IWebDriver, opts ...option.DriverOption return dExt, nil } -func (dExt *DriverExt) Init() error { +func (dExt *DriverExt) Setup() error { // unlock device screen err := dExt.Driver.Unlock() if err != nil { diff --git a/pkg/uixt/ext/android_stub.go b/pkg/uixt/ext/android_stub.go new file mode 100644 index 00000000..6546c91b --- /dev/null +++ b/pkg/uixt/ext/android_stub.go @@ -0,0 +1,318 @@ +package uixt_ext + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/internal/json" + "github.com/httprunner/httprunner/v5/pkg/uixt" + "github.com/httprunner/httprunner/v5/pkg/uixt/option" +) + +const ( + StubSocketName = "com.bytest.device" + DouyinServerPort = 32316 +) + +func NewStubDriver(device *uixt.AndroidDevice, opts ...option.DriverOption) (driver *StubAndroidDriver, err error) { + socketLocalPort, err := device.Forward(StubSocketName) + if err != nil { + return nil, errors.Wrap(code.DeviceConnectionError, + fmt.Sprintf("forward port %d->%s failed: %v", + socketLocalPort, StubSocketName, err)) + } + + serverLocalPort, err := device.Forward(DouyinServerPort) + if err != nil { + return nil, errors.Wrap(code.DeviceConnectionError, + fmt.Sprintf("forward port %d->%d failed: %v", + serverLocalPort, DouyinServerPort, err)) + } + + address := fmt.Sprintf("127.0.0.1:%d", socketLocalPort) + conn, err := net.Dial("tcp", address) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("failed to connect %s", address)) + return nil, err + } + + driver = &StubAndroidDriver{ + socket: conn, + timeout: 10 * time.Second, + } + + rawURL := fmt.Sprintf("http://forward-to-%d:%d", + serverLocalPort, DouyinServerPort) + if driver.urlPrefix, err = url.Parse(rawURL); err != nil { + return nil, err + } + + driver.Device = device.Device + driver.Logcat = device.Logcat + return driver, nil +} + +type StubAndroidDriver struct { + uixt.ADBDriver + socket net.Conn + seq int + timeout time.Duration +} + +type AppLoginInfo struct { + Did string `json:"did,omitempty" yaml:"did,omitempty"` + Uid string `json:"uid,omitempty" yaml:"uid,omitempty"` + IsLogin bool `json:"is_login,omitempty" yaml:"is_login,omitempty"` +} + +func (sad *StubAndroidDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + var localPort int + { + tmpURL, _ := url.Parse(sad.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) + } + } + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + sad.Client = convertToHTTPClient(conn) + return sad.Request(http.MethodGet, sad.concatURL(nil, pathElem...), nil) +} + +func (sad *StubAndroidDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var localPort int + { + tmpURL, _ := url.Parse(sad.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) + } + } + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + sad.Client = convertToHTTPClient(conn) + + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return sad.Request(http.MethodPost, sad.concatURL(nil, pathElem...), bsJSON) +} + +func (sad *StubAndroidDriver) sendCommand(packageName string, cmdType string, params map[string]interface{}, readTimeout ...time.Duration) (interface{}, error) { + sad.seq++ + packet := map[string]interface{}{ + "Seq": sad.seq, + "Cmd": cmdType, + "v": "", + } + for key, value := range params { + if key == "Cmd" || key == "Seq" { + return "", errors.New("params cannot be Cmd or Seq") + } + packet[key] = value + } + data, err := json.Marshal(packet) + if err != nil { + return nil, err + } + + res, err := sad.Device.RunStubCommand(append(data, '\n'), packageName) + if err != nil { + return nil, err + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(res), &resultMap); err != nil { + return nil, err + } + if resultMap["Error"] != nil { + return nil, fmt.Errorf("failed to call stub command: %s", resultMap["Error"].(string)) + } + + return resultMap["Result"], nil +} + +func (sad *StubAndroidDriver) DeleteSession() error { + return sad.close() +} + +func (sad *StubAndroidDriver) close() error { + if sad.socket != nil { + return sad.socket.Close() + } + return nil +} + +func (sad *StubAndroidDriver) Status() (uixt.DeviceStatus, error) { + app, err := sad.GetForegroundApp() + if err != nil { + return uixt.DeviceStatus{}, err + } + res, err := sad.sendCommand(app.PackageName, "Hello", nil) + if err != nil { + return uixt.DeviceStatus{}, err + } + log.Info().Msg(fmt.Sprintf("ping stub result :%v", res)) + return uixt.DeviceStatus{}, nil +} + +func (sad *StubAndroidDriver) Source(srcOpt ...option.SourceOption) (source string, err error) { + app, err := sad.GetForegroundApp() + if err != nil { + return "", err + } + params := map[string]interface{}{ + "ClassName": "com.bytedance.byteinsight.MockOperator", + "Method": "getLayout", + "RetType": "", + "Args": []string{}, + } + res, err := sad.sendCommand(app.PackageName, "CallStaticMethod", params) + if err != nil { + return "", err + } + return res.(string), nil +} + +func (sad *StubAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { + params := map[string]interface{}{ + "phone": phoneNumber, + } + if captcha != "" { + params["captcha"] = captcha + } else if password != "" { + params["password"] = password + } else { + return info, fmt.Errorf("password and capcha is empty") + } + resp, err := sad.httpPOST(params, "/host", "/login", "account") + if err != nil { + return info, err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return info, err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to login %s", res["data"]) + log.Err(err).Msgf("%v", res) + return info, err + } + time.Sleep(20 * time.Second) + info, err = sad.getLoginAppInfo(packageName) + if err != nil || !info.IsLogin { + return info, fmt.Errorf("falied to login %v", info) + } + return info, nil +} + +func (sad *StubAndroidDriver) LogoutNoneUI(packageName string) error { + resp, err := sad.httpGET("/host", "/logout") + if err != nil { + return err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to logout %s", res["data"]) + log.Err(err).Msgf("%v", res) + return err + } + fmt.Printf("%v", resp) + if err != nil { + return err + } + time.Sleep(3 * time.Second) + return nil +} + +func (sad *StubAndroidDriver) LoginNoneUIDynamic(packageName, phoneNumber string, captcha string) error { + params := map[string]interface{}{ + "ClassName": "qe.python.test.LoginUtil", + "Method": "loginSync", + "RetType": "", + "Args": []string{phoneNumber, captcha}, + } + res, err := sad.sendCommand(packageName, "CallStaticMethod", params) + if err != nil { + return err + } + log.Info().Msg(res.(string)) + return nil +} + +func (sad *StubAndroidDriver) SetHDTStatus(status bool) error { + _, err := sad.Device.RunShellCommand("settings", "put", "global", "feedbacker_sso_bypass_token", "default_sso_bypass_token") + if err != nil { + log.Warn().Msg(fmt.Sprintf("failed to disable sso, error: %v", err)) + } + params := map[string]interface{}{ + "ClassName": "com.bytedance.ies.stark.framework.HybridDevTool", + "Method": "setEnabled", + "RetType": "", + "Args": []bool{status}, + } + res, err := sad.sendCommand("com.ss.android.ugc.aweme", "CallStaticMethod", params) + if err != nil { + return fmt.Errorf("failed to set hds status %v, error: %v", status, err) + } + log.Info().Msg(fmt.Sprintf("set hdt status result: %s", res)) + return nil +} + +func (sad *StubAndroidDriver) getLoginAppInfo(packageName string) (info AppLoginInfo, err error) { + resp, err := sad.httpGET("/host", "/app", "/info") + if err != nil { + return info, err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return info, err + } + if res["isSuccess"] != true { + err = fmt.Errorf("falied to get app info %s", res["data"]) + log.Err(err).Msgf("%v", res) + return info, err + } + + err = json.Unmarshal([]byte(res["data"].(string)), &info) + if err != nil { + err = fmt.Errorf("falied to parse app info %s", res["data"]) + return + } + return info, nil +} + +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: 30 * time.Second, + } +} diff --git a/pkg/uixt/harmony_device.go b/pkg/uixt/harmony_device.go index c084551c..eb95ce76 100644 --- a/pkg/uixt/harmony_device.go +++ b/pkg/uixt/harmony_device.go @@ -17,13 +17,12 @@ var ( ) type HarmonyDevice struct { - *option.HarmonyDeviceConfig - d *ghdc.Device + *ghdc.Device + *option.HarmonyDeviceOptions } func NewHarmonyDevice(opts ...option.HarmonyDeviceOption) (device *HarmonyDevice, err error) { - deviceConfig := option.NewHarmonyDeviceConfig(opts...) - + deviceConfig := option.NewHarmonyDeviceOptions(opts...) deviceList, err := GetHarmonyDevices(deviceConfig.ConnectKey) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, err.Error()) @@ -43,8 +42,8 @@ func NewHarmonyDevice(opts ...option.HarmonyDeviceOption) (device *HarmonyDevice } device = &HarmonyDevice{ - HarmonyDeviceConfig: deviceConfig, - d: dev, + HarmonyDeviceOptions: deviceConfig, + Device: dev, } log.Info().Str("connectKey", device.ConnectKey).Msg("init harmony device") return device, nil @@ -83,7 +82,11 @@ func GetHarmonyDevices(serial ...string) (devices []*ghdc.Device, err error) { return devices, nil } -func (dev *HarmonyDevice) Init() error { +func (dev *HarmonyDevice) Setup() error { + return nil +} + +func (dev *HarmonyDevice) Teardown() error { return nil } @@ -96,7 +99,7 @@ func (dev *HarmonyDevice) LogEnabled() bool { } func (dev *HarmonyDevice) NewDriver(opts ...option.DriverOption) (driverExt *DriverExt, err error) { - driver, err := newHarmonyDriver(dev.d) + driver, err := newHarmonyDriver(dev.Device) if err != nil { log.Error().Err(err).Msg("failed to new harmony driver") return nil, err @@ -110,8 +113,8 @@ func (dev *HarmonyDevice) NewDriver(opts ...option.DriverOption) (driverExt *Dri return driverExt, nil } -func (dev *HarmonyDevice) NewUSBDriver(opts ...option.DriverOption) (driver IWebDriver, err error) { - harmonyDriver, err := newHarmonyDriver(dev.d) +func (dev *HarmonyDevice) NewUSBDriver(opts ...option.DriverOption) (driver IDriver, err error) { + harmonyDriver, err := newHarmonyDriver(dev.Device) if err != nil { log.Error().Err(err).Msg("failed to new harmony driver") return nil, err diff --git a/pkg/uixt/harmony_driver_hdc.go b/pkg/uixt/harmony_driver_hdc.go index 8aaec303..f50158cc 100644 --- a/pkg/uixt/harmony_driver_hdc.go +++ b/pkg/uixt/harmony_driver_hdc.go @@ -14,9 +14,9 @@ import ( ) type hdcDriver struct { - points []ExportPoint - DriverClient - device *ghdc.Device + *HarmonyDevice + *DriverClient + points []ExportPoint uiDriver *ghdc.UIDriver } @@ -30,7 +30,7 @@ const ( func newHarmonyDriver(device *ghdc.Device) (driver *hdcDriver, err error) { driver = new(hdcDriver) - driver.device = device + driver.Device = device uiDriver, err := ghdc.NewUIDriver(*device) if err != nil { log.Error().Err(err).Msg("failed to new harmony ui driver") @@ -100,7 +100,7 @@ func (hd *hdcDriver) Homescreen() error { func (hd *hdcDriver) Unlock() (err error) { // Todo 检查是否锁屏 hdc shell hidumper -s RenderService -a screen - screenInfo, err := hd.device.RunShellCommand("hidumper", "-s", "RenderService", "-a", "screen") + screenInfo, err := hd.RunShellCommand("hidumper", "-s", "RenderService", "-a", "screen") if err != nil { return err } @@ -126,7 +126,7 @@ func (hd *hdcDriver) AppLaunch(packageName string) error { } func (hd *hdcDriver) AppTerminate(packageName string) (bool, error) { - _, err := hd.device.RunShellCommand("aa", "force-stop", packageName) + _, err := hd.RunShellCommand("aa", "force-stop", packageName) if err != nil { log.Error().Err(err).Msg("failed to terminal app") return false, err diff --git a/pkg/uixt/ios_device.go b/pkg/uixt/ios_device.go index 6deb08ab..70c501dd 100644 --- a/pkg/uixt/ios_device.go +++ b/pkg/uixt/ios_device.go @@ -31,28 +31,6 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -const ( - defaultWDAPort = 8100 - defaultMjpegPort = 9100 - defaultBightInsightPort = 8000 - defaultDouyinServerPort = 32921 -) - -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 {'不允许','暂不'}`]" -) - var tunnelManager *tunnel.TunnelManager = nil func GetIOSDevices(udid ...string) (deviceList []ios.DeviceEntry, err error) { @@ -126,20 +104,7 @@ func RebootTunnel() (err error) { } func NewIOSDevice(opts ...option.IOSDeviceOption) (device *IOSDevice, err error) { - deviceOptions := &option.IOSDeviceConfig{ - 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 opts { - option(deviceOptions) - } - + deviceOptions := option.NewIOSDeviceOptions(opts...) deviceList, err := GetIOSDevices(deviceOptions.UDID) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, err.Error()) @@ -160,12 +125,12 @@ func NewIOSDevice(opts ...option.IOSDeviceOption) (device *IOSDevice, err error) } device = &IOSDevice{ - IOSDeviceConfig: deviceOptions, - listeners: make(map[int]*forward.ConnListener), - d: dev, + IOSDeviceOptions: deviceOptions, + listeners: make(map[int]*forward.ConnListener), + d: dev, } log.Info().Str("udid", device.UDID).Msg("init ios device") - err = device.Init() + err = device.Setup() if err != nil { _ = device.Teardown() return nil, err @@ -174,7 +139,7 @@ func NewIOSDevice(opts ...option.IOSDeviceOption) (device *IOSDevice, err error) } type IOSDevice struct { - *option.IOSDeviceConfig + *option.IOSDeviceOptions d ios.DeviceEntry listeners map[int]*forward.ConnListener } @@ -207,7 +172,7 @@ const ( ApplicationTypeAny ApplicationType = "Any" ) -func (dev *IOSDevice) Init() error { +func (dev *IOSDevice) Setup() error { images, err := dev.ListImages() if err != nil { return err @@ -270,7 +235,7 @@ func (dev *IOSDevice) NewDriver(opts ...option.DriverOption) (driverExt *DriverE capabilities.WithDefaultAlertAction(option.AlertActionAccept) } - var driver IWebDriver + var driver IDriver if dev.STUB { driver, err = dev.NewStubDriver() if err != nil { @@ -561,7 +526,7 @@ func (dev *IOSDevice) Reboot() error { } // NewHTTPDriver creates new remote HTTP client, this will also start a new session. -func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IWebDriver, err error) { +func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IDriver, err error) { var localPort int localPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_PORT")) if err != nil { @@ -570,7 +535,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IW return nil, errors.Wrap(code.DeviceHTTPDriverError, fmt.Sprintf("get free port failed: %v", err)) } - if err = dev.forward(localPort, dev.Port); err != nil { + if err = dev.forward(localPort, dev.WDAPort); err != nil { return nil, errors.Wrap(code.DeviceHTTPDriverError, fmt.Sprintf("forward tcp port failed: %v", err)) } @@ -586,7 +551,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IW return nil, errors.Wrap(code.DeviceHTTPDriverError, fmt.Sprintf("get free port failed: %v", err)) } - if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil { + if err = dev.forward(localMjpegPort, dev.WDAMjpegPort); err != nil { return nil, errors.Wrap(code.DeviceHTTPDriverError, fmt.Sprintf("forward tcp port failed: %v", err)) } @@ -600,9 +565,9 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IW Msg("init WDA HTTP driver") wd := new(wdaDriver) - wd.device = dev + wd.IOSDevice = dev wd.udid = dev.UDID - wd.client = &http.Client{ + wd.Client = &http.Client{ Timeout: time.Second * 10, // 设置超时时间为 10 秒 } @@ -634,7 +599,12 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver IW return wd, nil } -func (dev *IOSDevice) NewStubDriver() (driver IWebDriver, err error) { +const ( + defaultBightInsightPort = 8000 + defaultDouyinServerPort = 32921 +) + +func (dev *IOSDevice) NewStubDriver() (driver IDriver, err error) { localStubPort, err := builtin.GetFreePort() if err != nil { return nil, errors.Wrap(code.DeviceHTTPDriverError, diff --git a/pkg/uixt/ios_driver_stub.go b/pkg/uixt/ios_driver_stub.go index 327933b9..612bac98 100644 --- a/pkg/uixt/ios_driver_stub.go +++ b/pkg/uixt/ios_driver_stub.go @@ -14,7 +14,6 @@ import ( type stubIOSDriver struct { *wdaDriver - DriverClient bightInsightPrefix string serverPrefix string @@ -32,7 +31,7 @@ func newStubIOSDriver(bightInsightAddr, serverAddr string, dev *IOSDevice, readT driver.bightInsightPrefix = bightInsightAddr driver.serverPrefix = serverAddr driver.timeout = timeout - driver.DriverClient.client = &http.Client{ + driver.DriverClient.Client = &http.Client{ Timeout: time.Second * 10, // 设置超时时间为 10 秒 } return driver, nil @@ -516,7 +515,7 @@ func (s *stubIOSDriver) LogoutNoneUI(packageName string) error { } func (s *stubIOSDriver) TearDown() error { - s.DriverClient.client.CloseIdleConnections() + s.DriverClient.Client.CloseIdleConnections() return nil } diff --git a/pkg/uixt/ios_driver_stub_test.go b/pkg/uixt/ios_driver_stub_test.go index 8b5530e3..72b05432 100644 --- a/pkg/uixt/ios_driver_stub_test.go +++ b/pkg/uixt/ios_driver_stub_test.go @@ -11,7 +11,7 @@ import ( ) var ( - iOSStubDriver IWebDriver + iOSStubDriver IDriver iOSDevice *IOSDevice ) diff --git a/pkg/uixt/ios_driver_wda.go b/pkg/uixt/ios_driver_wda.go index b664ab84..d4a6bb53 100644 --- a/pkg/uixt/ios_driver_wda.go +++ b/pkg/uixt/ios_driver_wda.go @@ -28,9 +28,9 @@ import ( ) type wdaDriver struct { - DriverClient + *IOSDevice + *DriverClient udid string - device *IOSDevice mjpegHTTPConn net.Conn // via HTTP mjpegClient *http.Client mjpegUrl string @@ -467,7 +467,7 @@ func (wd *wdaDriver) GetForegroundApp() (appInfo AppInfo, err error) { if err != nil { return appInfo, err } - apps, err := wd.device.ListApps(ApplicationTypeAny) + apps, err := wd.ListApps(ApplicationTypeAny) if err != nil { return appInfo, err } @@ -1021,7 +1021,7 @@ func (wd *wdaDriver) GetDriverResults() []*DriverResult { func (wd *wdaDriver) TearDown() error { wd.mjpegClient.CloseIdleConnections() - wd.client.CloseIdleConnections() + wd.Client.CloseIdleConnections() return nil } diff --git a/pkg/uixt/ios_test.go b/pkg/uixt/ios_test.go index 808814f7..922aa31a 100644 --- a/pkg/uixt/ios_test.go +++ b/pkg/uixt/ios_test.go @@ -15,7 +15,7 @@ import ( var ( bundleId = "com.apple.Preferences" - driver IWebDriver + driver IDriver iOSDriverExt *DriverExt ) diff --git a/pkg/uixt/option/android.go b/pkg/uixt/option/android.go index 8df387db..e55da8e8 100644 --- a/pkg/uixt/option/android.go +++ b/pkg/uixt/option/android.go @@ -1,18 +1,31 @@ package option -type AndroidDeviceConfig struct { +import "github.com/httprunner/httprunner/v5/pkg/gadb" + +type AndroidDeviceOptions struct { SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` - STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use stub - UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2 - UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip - UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port + STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use stub LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + + // adb + AdbServerHost string `json:"adb_server_host,omitempty" yaml:"adb_server_host,omitempty"` + AdbServerPort int `json:"adb_server_port,omitempty" yaml:"adb_server_port,omitempty"` + + // uiautomator2 + UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2 + UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip + UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port + UIA2ServerPackageName string `json:"uia2_server_package_name,omitempty" yaml:"uia2_server_package_name,omitempty"` + UIA2ServerTestPackageName string `json:"uia2_server_test_package_name,omitempty" yaml:"uia2_server_test_package_name,omitempty"` } -func (dev *AndroidDeviceConfig) Options() (deviceOptions []AndroidDeviceOption) { +func (dev *AndroidDeviceOptions) Options() (deviceOptions []AndroidDeviceOption) { if dev.SerialNumber != "" { deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) } + if dev.STUB { + deviceOptions = append(deviceOptions, WithStub(true)) + } if dev.UIA2 { deviceOptions = append(deviceOptions, WithUIA2(true)) } @@ -28,54 +41,87 @@ func (dev *AndroidDeviceConfig) Options() (deviceOptions []AndroidDeviceOption) return } -func NewAndroidDeviceConfig(opts ...AndroidDeviceOption) *AndroidDeviceConfig { - config := &AndroidDeviceConfig{} +const ( + // adb server + defaultAdbServerHost = "localhost" + defaultAdbServerPort = gadb.AdbServerPort // 5037 + + // uiautomator2 server + defaultUIA2ServerHost = "localhost" + defaultUIA2ServerPort = 6790 + defaultUIA2ServerPackageName = "io.appium.uiautomator2.server" + defaultUIA2ServerTestPackageName = "io.appium.uiautomator2.server.test" +) + +func NewAndroidDeviceOptions(opts ...AndroidDeviceOption) *AndroidDeviceOptions { + config := &AndroidDeviceOptions{} for _, opt := range opts { opt(config) } + + // set default + if config.AdbServerHost == "" { + config.AdbServerHost = defaultAdbServerHost + } + if config.AdbServerPort == 0 { + config.AdbServerPort = defaultAdbServerPort + } + + if config.UIA2 { + if config.UIA2IP == "" && config.UIA2Port == 0 { + config.UIA2IP = defaultUIA2ServerHost + config.UIA2Port = defaultUIA2ServerPort + } + if config.UIA2ServerPackageName == "" { + config.UIA2ServerPackageName = defaultUIA2ServerPackageName + } + if config.UIA2ServerTestPackageName == "" { + config.UIA2ServerTestPackageName = defaultUIA2ServerTestPackageName + } + } return config } -type AndroidDeviceOption func(*AndroidDeviceConfig) +type AndroidDeviceOption func(*AndroidDeviceOptions) func WithDriverTypeADB() AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.STUB = false } } func WithSerialNumber(serial string) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.SerialNumber = serial } } func WithUIA2(uia2On bool) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.UIA2 = uia2On } } func WithStub(stubOn bool) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.STUB = stubOn } } func WithUIA2IP(ip string) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.UIA2IP = ip } } func WithUIA2Port(port int) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.UIA2Port = port } } func WithAdbLogOn(logOn bool) AndroidDeviceOption { - return func(device *AndroidDeviceConfig) { + return func(device *AndroidDeviceOptions) { device.LogOn = logOn } } diff --git a/pkg/uixt/option/harmony.go b/pkg/uixt/option/harmony.go index ead187d6..0998a41f 100644 --- a/pkg/uixt/option/harmony.go +++ b/pkg/uixt/option/harmony.go @@ -1,11 +1,11 @@ package option -type HarmonyDeviceConfig struct { +type HarmonyDeviceOptions struct { ConnectKey string `json:"connect_key,omitempty" yaml:"connect_key,omitempty"` LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } -func (dev *HarmonyDeviceConfig) Options() (deviceOptions []HarmonyDeviceOption) { +func (dev *HarmonyDeviceOptions) Options() (deviceOptions []HarmonyDeviceOption) { if dev.ConnectKey != "" { deviceOptions = append(deviceOptions, WithConnectKey(dev.ConnectKey)) } @@ -15,24 +15,24 @@ func (dev *HarmonyDeviceConfig) Options() (deviceOptions []HarmonyDeviceOption) return } -func NewHarmonyDeviceConfig(opts ...HarmonyDeviceOption) (device *HarmonyDeviceConfig) { - device = &HarmonyDeviceConfig{} +func NewHarmonyDeviceOptions(opts ...HarmonyDeviceOption) (device *HarmonyDeviceOptions) { + device = &HarmonyDeviceOptions{} for _, option := range opts { option(device) } return } -type HarmonyDeviceOption func(*HarmonyDeviceConfig) +type HarmonyDeviceOption func(*HarmonyDeviceOptions) func WithConnectKey(connectKey string) HarmonyDeviceOption { - return func(device *HarmonyDeviceConfig) { + return func(device *HarmonyDeviceOptions) { device.ConnectKey = connectKey } } func WithLogOn(logOn bool) HarmonyDeviceOption { - return func(device *HarmonyDeviceConfig) { + return func(device *HarmonyDeviceOptions) { device.LogOn = logOn } } diff --git a/pkg/uixt/option/ios.go b/pkg/uixt/option/ios.go index 0a998a92..9ba7f546 100644 --- a/pkg/uixt/option/ios.go +++ b/pkg/uixt/option/ios.go @@ -1,11 +1,11 @@ package option -type IOSDeviceConfig struct { - 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 - STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use stub - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +type IOSDeviceOptions struct { + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + 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 + STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use stub + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` // switch to iOS springboard before init WDA session ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` @@ -16,15 +16,15 @@ type IOSDeviceConfig struct { DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"` } -func (dev *IOSDeviceConfig) Options() (deviceOptions []IOSDeviceOption) { +func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) { if dev.UDID != "" { deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) } - if dev.Port != 0 { - deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) + if dev.WDAPort != 0 { + deviceOptions = append(deviceOptions, WithWDAPort(dev.WDAPort)) } - if dev.MjpegPort != 0 { - deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) + if dev.WDAMjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.WDAMjpegPort)) } if dev.STUB { deviceOptions = append(deviceOptions, WithIOSStub(true)) @@ -47,66 +47,108 @@ func (dev *IOSDeviceConfig) Options() (deviceOptions []IOSDeviceOption) { return } -func NewIOSDeviceConfig(opts ...IOSDeviceOption) *IOSDeviceConfig { - config := &IOSDeviceConfig{} +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 + defaultSnapshotMaxDepth = 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 {'不允许','暂不'}`]" +) + +func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions { + config := &IOSDeviceOptions{} for _, opt := range opts { opt(config) } + + if config.WDAPort == 0 { + config.WDAPort = defaultWDAPort + } + if config.WDAMjpegPort == 0 { + config.WDAMjpegPort = defaultMjpegPort + } + + if config.SnapshotMaxDepth == 0 { + config.SnapshotMaxDepth = defaultSnapshotMaxDepth + } + if config.AcceptAlertButtonSelector == "" { + config.AcceptAlertButtonSelector = acceptAlertButtonSelector + } + if config.DismissAlertButtonSelector == "" { + config.DismissAlertButtonSelector = dismissAlertButtonSelector + } + + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is active such as douyin or wexin + config.ResetHomeOnStartup = true + return config } -type IOSDeviceOption func(*IOSDeviceConfig) +type IOSDeviceOption func(*IOSDeviceOptions) func WithUDID(udid string) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.UDID = udid } } func WithWDAPort(port int) IOSDeviceOption { - return func(device *IOSDeviceConfig) { - device.Port = port + return func(device *IOSDeviceOptions) { + device.WDAPort = port } } func WithWDAMjpegPort(port int) IOSDeviceOption { - return func(device *IOSDeviceConfig) { - device.MjpegPort = port + return func(device *IOSDeviceOptions) { + device.WDAMjpegPort = port } } func WithWDALogOn(logOn bool) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.LogOn = logOn } } func WithIOSStub(stub bool) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.STUB = stub } } func WithResetHomeOnStartup(reset bool) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.ResetHomeOnStartup = reset } } func WithSnapshotMaxDepth(depth int) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.SnapshotMaxDepth = depth } } func WithAcceptAlertButtonSelector(selector string) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.AcceptAlertButtonSelector = selector } } func WithDismissAlertButtonSelector(selector string) IOSDeviceOption { - return func(device *IOSDeviceConfig) { + return func(device *IOSDeviceOptions) { device.DismissAlertButtonSelector = selector } } diff --git a/pkg/uixt/types.go b/pkg/uixt/types.go index 4db580dd..ca32df8f 100644 --- a/pkg/uixt/types.go +++ b/pkg/uixt/types.go @@ -1,6 +1,9 @@ package uixt -import "math" +import ( + "fmt" + "math" +) type DeviceStatus struct { Message string `json:"message"` @@ -122,6 +125,34 @@ func (v BatteryState) String() string { } } +type BatteryStatus int + +const ( + _ = iota + BatteryStatusUnknown BatteryStatus = iota + BatteryStatusCharging + BatteryStatusDischarging + BatteryStatusNotCharging + BatteryStatusFull +) + +func (bs BatteryStatus) String() string { + switch bs { + case BatteryStatusUnknown: + return "unknown" + case BatteryStatusCharging: + return "charging" + case BatteryStatusDischarging: + return "discharging" + case BatteryStatusNotCharging: + return "not charging" + case BatteryStatusFull: + return "full" + default: + return fmt.Sprintf("unknown status code (%d)", bs) + } +} + type Size struct { Width int `json:"width"` Height int `json:"height"` @@ -233,7 +264,7 @@ type Rotation struct { Z int `json:"z"` } -type Condition func(wd IWebDriver) (bool, error) +type Condition func(wd IDriver) (bool, error) type Direction string diff --git a/runner.go b/runner.go index ec4ec429..2df5a19e 100644 --- a/runner.go +++ b/runner.go @@ -427,14 +427,14 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { if err != nil { return nil, errors.Wrap(err, "init android device failed") } - if err := device.Init(); err != nil { + if err := device.Setup(); err != nil { return nil, err } driver, err := device.NewDriver() if err != nil { return nil, err } - if err := driver.Init(); err != nil { + if err := driver.Setup(); err != nil { return nil, err } r.uixtDrivers[device.SerialNumber] = driver @@ -450,14 +450,14 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { if err != nil { return nil, errors.Wrap(err, "init ios device failed") } - if err := device.Init(); err != nil { + if err := device.Setup(); err != nil { return nil, err } driver, err := device.NewDriver() if err != nil { return nil, err } - if err := driver.Init(); err != nil { + if err := driver.Setup(); err != nil { return nil, err } r.uixtDrivers[device.UDID] = driver @@ -473,14 +473,14 @@ func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) { if err != nil { return nil, errors.Wrap(err, "init harmony device failed") } - if err := device.Init(); err != nil { + if err := device.Setup(); err != nil { return nil, err } driver, err := device.NewDriver() if err != nil { return nil, err } - if err := driver.Init(); err != nil { + if err := driver.Setup(); err != nil { return nil, err } r.uixtDrivers[device.ConnectKey] = driver diff --git a/server/context.go b/server/context.go index 67f32118..26697dfc 100644 --- a/server/context.go +++ b/server/context.go @@ -52,7 +52,7 @@ func handleDeviceContext() gin.HandlerFunc { c.Abort() return } - device.Init() + device.Setup() driver, err := device.NewDriver( option.WithDriverImageService(true), diff --git a/step_request.go b/step_request.go index 0208042b..041615fa 100644 --- a/step_request.go +++ b/step_request.go @@ -760,7 +760,7 @@ func (s *StepRequest) MobileUI() *StepMobile { // Android creates a new android step session func (s *StepRequest) Android(opts ...option.AndroidDeviceOption) *StepMobile { - androidOptions := option.NewAndroidDeviceConfig(opts...) + androidOptions := option.NewAndroidDeviceOptions(opts...) return &StepMobile{ StepConfig: s.StepConfig, Android: &MobileUI{ @@ -771,7 +771,7 @@ func (s *StepRequest) Android(opts ...option.AndroidDeviceOption) *StepMobile { // IOS creates a new ios step session func (s *StepRequest) IOS(opts ...option.IOSDeviceOption) *StepMobile { - iosOptions := option.NewIOSDeviceConfig(opts...) + iosOptions := option.NewIOSDeviceOptions(opts...) return &StepMobile{ StepConfig: s.StepConfig, IOS: &MobileUI{ @@ -782,7 +782,7 @@ func (s *StepRequest) IOS(opts ...option.IOSDeviceOption) *StepMobile { // Harmony creates a new harmony step session func (s *StepRequest) Harmony(opts ...option.HarmonyDeviceOption) *StepMobile { - harmonyOptions := option.NewHarmonyDeviceConfig(opts...) + harmonyOptions := option.NewHarmonyDeviceOptions(opts...) return &StepMobile{ StepConfig: s.StepConfig, Harmony: &MobileUI{