mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-02 14:29:44 +08:00
merge master
This commit is contained in:
@@ -17,6 +17,7 @@ type ActionMethod string
|
||||
const (
|
||||
ACTION_AppInstall ActionMethod = "install"
|
||||
ACTION_AppUninstall ActionMethod = "uninstall"
|
||||
ACTION_AppClear ActionMethod = "app_clear"
|
||||
ACTION_AppStart ActionMethod = "app_start"
|
||||
ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成
|
||||
ACTION_AppTerminate ActionMethod = "app_terminate"
|
||||
@@ -26,6 +27,10 @@ const (
|
||||
ACTION_SleepRandom ActionMethod = "sleep_random"
|
||||
ACTION_StartCamera ActionMethod = "camera_start" // alias for app_launch camera
|
||||
ACTION_StopCamera ActionMethod = "camera_stop" // alias for app_terminate camera
|
||||
ACTION_SetClipboard ActionMethod = "set_clipboard"
|
||||
ACTION_GetClipboard ActionMethod = "get_clipboard"
|
||||
ACTION_SetIme ActionMethod = "set_ime"
|
||||
ACTION_GetSource ActionMethod = "get_source"
|
||||
|
||||
// UI validation
|
||||
// selectors
|
||||
@@ -60,6 +65,9 @@ const (
|
||||
ACTION_VideoCrawler ActionMethod = "video_crawler"
|
||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
||||
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
||||
ACTION_InstallApp ActionMethod = "install_app"
|
||||
ACTION_UninstallApp ActionMethod = "uninstall_app"
|
||||
ACTION_DownloadApp ActionMethod = "download_app"
|
||||
)
|
||||
|
||||
type MobileAction struct {
|
||||
@@ -554,8 +562,23 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
|
||||
|
||||
switch action.Method {
|
||||
case ACTION_AppInstall:
|
||||
// TODO
|
||||
return errActionNotImplemented
|
||||
if appUrl, ok := action.Params.(string); ok {
|
||||
if err = dExt.InstallByUrl(appUrl, NewInstallOptions(WithRetryTime(action.MaxRetryTimes))); err != nil {
|
||||
return errors.Wrap(err, "failed to install app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppUninstall:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.Uninstall(packageName, action.GetOptions()...); err != nil {
|
||||
return errors.Wrap(err, "failed to uninstall app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppClear:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
if err = dExt.Driver.Clear(packageName); err != nil {
|
||||
return errors.Wrap(err, "failed to clear app")
|
||||
}
|
||||
}
|
||||
case ACTION_AppLaunch:
|
||||
if bundleId, ok := action.Params.(string); ok {
|
||||
return dExt.Driver.AppLaunch(bundleId)
|
||||
@@ -594,8 +617,34 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
|
||||
case ACTION_SetClipboard:
|
||||
if text, ok := action.Params.(string); ok {
|
||||
err := dExt.Driver.SetPasteboard(PasteboardTypePlaintext, text)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set clipboard")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("set_clioboard params should be text(string), got %v", action.Params)
|
||||
case ACTION_Home:
|
||||
return dExt.Driver.Homescreen()
|
||||
case ACTION_SetIme:
|
||||
if ime, ok := action.Params.(string); ok {
|
||||
err = dExt.Driver.SetIme(ime)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_GetSource:
|
||||
if packageName, ok := action.Params.(string); ok {
|
||||
source := NewSourceOption().WithProcessName(packageName)
|
||||
_, err = dExt.Driver.Source(source)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ime")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case ACTION_TapXY:
|
||||
if location, ok := action.Params.([]interface{}); ok {
|
||||
// relative x,y of window size: [0.5, 0.5]
|
||||
|
||||
@@ -3,6 +3,7 @@ package uixt
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -218,10 +219,18 @@ func (ad *adbDriver) Orientation() (orientation Orientation, err error) {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Homescreen() (err error) {
|
||||
return ad.PressKeyCode(KCHome, KMEmpty)
|
||||
return ad.PressKeyCodes(KCHome, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error) {
|
||||
func (ad *adbDriver) Unlock() (err error) {
|
||||
return ad.PressKeyCodes(KCMenu, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return ad.PressKeyCodes(keyCode, KMEmpty)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta) (err error) {
|
||||
// adb shell input keyevent <keyCode>
|
||||
_, err = ad.adbClient.RunShellCommand(
|
||||
"input", "keyevent", fmt.Sprintf("%d", keyCode))
|
||||
@@ -421,6 +430,14 @@ func (ad *adbDriver) IsUnicodeIMEInstalled() bool {
|
||||
return strings.Contains(output, UnicodeImePackageName)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) ListIme() []string {
|
||||
output, err := ad.adbClient.RunShellCommand("ime", "list", "-s")
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(output, "\n")
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) {
|
||||
defer func() {
|
||||
// Reset to default, don't care which keyboard was chosen before switch:
|
||||
@@ -458,6 +475,15 @@ func (ad *adbDriver) Input(text string, options ...ActionOption) (err error) {
|
||||
return ad.SendKeys(text, options...)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Clear(packageName string) error {
|
||||
if _, err := ad.adbClient.RunShellCommand("pm", "clear", packageName); err != nil {
|
||||
log.Error().Str("packageName", packageName).Err(err).Msg("failed to clear package cache")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) PressButton(devBtn DeviceButton) (err error) {
|
||||
err = errDriverNotImplemented
|
||||
return
|
||||
@@ -499,6 +525,14 @@ func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (ad *adbDriver) LogoutNoneUI(packageName string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||
source, err := ad.Source()
|
||||
if err != nil {
|
||||
@@ -680,46 +714,57 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
return pointRes, nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetDriverResults() []*DriverResult {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
||||
// adb shell dumpsys activity activities
|
||||
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
|
||||
packageInfo, err := ad.adbClient.RunShellCommand("CLASSPATH=/data/local/tmp/evalite", "app_process", "/", "com.bytedance.iesqa.eval_process.PackageService")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to dumpsys activities")
|
||||
return AppInfo{}, errors.Wrap(err, "dumpsys activities failed")
|
||||
return app, err
|
||||
}
|
||||
log.Info().Msg(packageInfo)
|
||||
err = json.Unmarshal([]byte(strings.TrimSpace(packageInfo)), &app)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SetIme(imeRegx string) error {
|
||||
imeList := ad.ListIme()
|
||||
ime := ""
|
||||
for _, imeName := range imeList {
|
||||
if regexp.MustCompile(imeRegx).MatchString(imeName) {
|
||||
ime = imeName
|
||||
break
|
||||
}
|
||||
}
|
||||
if ime == "" {
|
||||
return fmt.Errorf("failed to set ime by %s, ime list: %v", imeRegx, imeList)
|
||||
}
|
||||
brand, _ := ad.adbClient.Brand()
|
||||
packageName := strings.Split(ime, "/")[0]
|
||||
res, err := ad.adbClient.RunShellCommand("ime", "set", ime)
|
||||
log.Info().Str("funcName", "SetIme").Interface("ime", ime).
|
||||
Interface("output", res).Msg("set ime")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
// grep mResumedActivity|ResumedActivity
|
||||
if strings.HasPrefix(trimmedLine, "mResumedActivity:") || strings.HasPrefix(trimmedLine, "ResumedActivity:") {
|
||||
// mResumedActivity: ActivityRecord{9656d74 u0 com.android.settings/.Settings t407}
|
||||
// ResumedActivity: ActivityRecord{8265c25 u0 com.android.settings/.Settings t73}
|
||||
strs := strings.Split(trimmedLine, " ")
|
||||
for _, str := range strs {
|
||||
if strings.Contains(str, "/") {
|
||||
// com.android.settings/.Settings
|
||||
s := strings.Split(str, "/")
|
||||
app := AppInfo{
|
||||
AppBaseInfo: AppBaseInfo{
|
||||
PackageName: s[0],
|
||||
Activity: s[1],
|
||||
},
|
||||
}
|
||||
return app, nil
|
||||
if strings.ToLower(brand) == "oppo" {
|
||||
time.Sleep(1 * time.Second)
|
||||
pid, _ := ad.adbClient.RunShellCommand("pidof", packageName)
|
||||
if strings.TrimSpace(pid) == "" {
|
||||
appInfo, err := ad.GetForegroundApp()
|
||||
_ = ad.AppLaunch(packageName)
|
||||
if err == nil && packageName != UnicodeImePackageName {
|
||||
time.Sleep(10 * time.Second)
|
||||
nextAppInfo, err := ad.GetForegroundApp()
|
||||
log.Info().Str("beforeFocusedPackage", appInfo.PackageName).Str("afterFocusedPackage", nextAppInfo.PackageName).Msg("")
|
||||
if err == nil && nextAppInfo.PackageName != appInfo.PackageName {
|
||||
_ = ad.PressKeyCodes(KCBack, KMEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SetIme(ime string) error {
|
||||
_, err := ad.adbClient.RunShellCommand("ime", "set", ime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// even if the shell command has returned,
|
||||
// as there might be a situation where the input method has not been completely switched yet
|
||||
// Listen to the following message.
|
||||
|
||||
@@ -4,26 +4,43 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||
)
|
||||
|
||||
var (
|
||||
AdbServerHost = "localhost"
|
||||
AdbServerPort = gadb.AdbServerPort // 5037
|
||||
UIA2ServerHost = "localhost"
|
||||
UIA2ServerPort = 6790
|
||||
DouyinServerPort = 32316
|
||||
AdbServerHost = "localhost"
|
||||
AdbServerPort = gadb.AdbServerPort // 5037
|
||||
UIA2ServerHost = "localhost"
|
||||
UIA2ServerPort = 6790
|
||||
EvalInstallerPackageName = "sogou.mobile.explorer"
|
||||
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
|
||||
)
|
||||
|
||||
//go:embed evalite
|
||||
var evalite embed.FS
|
||||
|
||||
const forwardToPrefix = "forward-to-"
|
||||
|
||||
type AndroidDeviceOption func(*AndroidDevice)
|
||||
@@ -40,6 +57,12 @@ func WithUIA2(uia2On bool) AndroidDeviceOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithStub(stubOn bool) AndroidDeviceOption {
|
||||
return func(device *AndroidDevice) {
|
||||
device.STUB = stubOn
|
||||
}
|
||||
}
|
||||
|
||||
func WithUIA2IP(ip string) AndroidDeviceOption {
|
||||
return func(device *AndroidDevice) {
|
||||
device.UIA2IP = ip
|
||||
@@ -108,7 +131,14 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
|
||||
|
||||
device.d = dev
|
||||
device.logcat = NewAdbLogcat(device.SerialNumber)
|
||||
|
||||
evalToolRaw, err := evalite.ReadFile("evalite")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidShellExecError, err.Error())
|
||||
}
|
||||
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
|
||||
return device, nil
|
||||
}
|
||||
@@ -151,9 +181,18 @@ type AndroidDevice struct {
|
||||
logcat *AdbLogcat
|
||||
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
|
||||
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
|
||||
STUB bool `json:"stub,omitempty" yaml:"stub,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
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"`
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Init() error {
|
||||
myexec.RunCommand("adb", "-s", dev.SerialNumber, "shell",
|
||||
"ime", "enable", "io.appium.settings/.UnicodeIME")
|
||||
myexec.RunCommand("adb", "-s", dev.SerialNumber, "shell", "rm", "-r", env.DeviceActionLogFilePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) System() string {
|
||||
@@ -169,7 +208,7 @@ func (dev *AndroidDevice) LogEnabled() bool {
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
|
||||
driverOptions := &DriverOptions{}
|
||||
driverOptions := NewDriverOptions()
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
@@ -177,6 +216,8 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
|
||||
var driver WebDriver
|
||||
if dev.UIA2 || dev.LogOn {
|
||||
driver, err = dev.NewUSBDriver(driverOptions.capabilities)
|
||||
} else if dev.STUB {
|
||||
driver, err = dev.NewStubDriver(driverOptions.capabilities)
|
||||
} else {
|
||||
driver, err = dev.NewAdbDriver()
|
||||
}
|
||||
@@ -184,7 +225,7 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
|
||||
return nil, errors.Wrap(err, "failed to init UIA driver")
|
||||
}
|
||||
|
||||
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
|
||||
driverExt, err = newDriverExt(dev, driver, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -221,6 +262,36 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDri
|
||||
return uiaDriver, nil
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) NewStubDriver(capabilities Capabilities) (driver *stubAndroidDriver, err error) {
|
||||
socketLocalPort, err := dev.d.Forward(StubSocketName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
fmt.Sprintf("forward port %d->%s failed: %v",
|
||||
socketLocalPort, StubSocketName, err))
|
||||
}
|
||||
|
||||
serverLocalPort, err := dev.d.Forward(DouyinServerPort)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||
fmt.Sprintf("forward port %d->%d failed: %v",
|
||||
serverLocalPort, DouyinServerPort, err))
|
||||
}
|
||||
|
||||
rawURL := fmt.Sprintf("http://%s%d:%d",
|
||||
forwardToPrefix, serverLocalPort, DouyinServerPort)
|
||||
|
||||
stubDriver, err := newStubAndroidDriver(fmt.Sprintf("127.0.0.1:%d", socketLocalPort), rawURL)
|
||||
if err != nil {
|
||||
_ = dev.d.ForwardKill(socketLocalPort)
|
||||
_ = dev.d.ForwardKill(serverLocalPort)
|
||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
|
||||
}
|
||||
stubDriver.adbClient = dev.d
|
||||
stubDriver.logcat = dev.logcat
|
||||
|
||||
return stubDriver, nil
|
||||
}
|
||||
|
||||
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
|
||||
func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) {
|
||||
rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.UIA2IP, dev.UIA2Port)
|
||||
@@ -261,6 +332,116 @@ func (dev *AndroidDevice) StopPcap() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Uninstall(packageName string) error {
|
||||
return myexec.RunCommand("adb", "-s", dev.SerialNumber, "uninstall", packageName)
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Install(appPath string, opts *InstallOptions) error {
|
||||
app, err := os.Open(appPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("install %s open file failed", appPath))
|
||||
}
|
||||
|
||||
defer app.Close()
|
||||
brand, err := dev.d.Brand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := []string{}
|
||||
if opts.Reinstall {
|
||||
args = append(args, "-r")
|
||||
}
|
||||
if opts.GrantPermission {
|
||||
args = append(args, "-g")
|
||||
}
|
||||
if opts.Downgrade {
|
||||
args = append(args, "-d")
|
||||
}
|
||||
switch strings.ToLower(brand) {
|
||||
case "vivo":
|
||||
return dev.installVivoSilent(app, args...)
|
||||
case "oppo", "realme", "oneplus":
|
||||
if dev.d.IsPackagesInstalled(EvalInstallerPackageName) {
|
||||
return dev.installViaInstaller(app, args...)
|
||||
}
|
||||
log.Warn().Msg("oppo not install eval installer")
|
||||
return dev.installCommon(app, args...)
|
||||
default:
|
||||
return dev.installCommon(app, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) installVivoSilent(app io.ReadSeeker, args ...string) error {
|
||||
currentTime := builtin.GetCurrentDay()
|
||||
md5HashInBytes := md5.Sum([]byte(currentTime))
|
||||
verifyCode := hex.EncodeToString(md5HashInBytes[:])
|
||||
verifyCode = base64.StdEncoding.EncodeToString([]byte(verifyCode))
|
||||
verifyCode = verifyCode[:8]
|
||||
verifyCode = "-V" + verifyCode
|
||||
args = append([]string{verifyCode}, args...)
|
||||
_, err := dev.d.InstallAPK(app, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) installViaInstaller(app io.ReadSeeker, args ...string) error {
|
||||
appRemotePath := "/data/local/tmp/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".apk"
|
||||
err := dev.d.Push(app, appRemotePath, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := make(chan error)
|
||||
defer func() {
|
||||
close(done)
|
||||
}()
|
||||
logcat := NewAdbLogcatWithCallback(dev.d.Serial(), func(line string) {
|
||||
re := regexp.MustCompile(`\{.*?}`)
|
||||
match := re.FindString(line)
|
||||
if match == "" {
|
||||
return
|
||||
}
|
||||
var result InstallResult
|
||||
err := json.Unmarshal([]byte(match), &result)
|
||||
if err != nil {
|
||||
log.Warn().Msg("parse Install msg line error: " + match)
|
||||
return
|
||||
}
|
||||
if result.Result == 0 {
|
||||
// 安装成功
|
||||
done <- nil
|
||||
} else {
|
||||
done <- errors.New(match)
|
||||
}
|
||||
})
|
||||
err = logcat.CatchLogcat("PackageInstallerCallback")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = logcat.Stop()
|
||||
}()
|
||||
|
||||
// 需要监听是否完成安装
|
||||
command := strings.Split(InstallViaInstallerCommand, " ")
|
||||
args = append(command, appRemotePath)
|
||||
_, err = dev.d.RunShellCommand("am", args[1:]...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 等待安装完成或超时
|
||||
timeout := 3 * time.Minute
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf("installation timed out after %v", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) installCommon(app io.ReadSeeker, args ...string) error {
|
||||
_, err := dev.d.InstallAPK(app, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
type LineCallback func(string)
|
||||
|
||||
type AdbLogcat struct {
|
||||
|
||||
273
hrp/pkg/uixt/android_stub_driver.go
Normal file
273
hrp/pkg/uixt/android_stub_driver.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
type stubAndroidDriver struct {
|
||||
socket net.Conn
|
||||
seq int
|
||||
timeout time.Duration
|
||||
adbDriver
|
||||
}
|
||||
|
||||
const StubSocketName = "com.bytest.device"
|
||||
|
||||
// newStubAndroidDriver
|
||||
// 创建stub Driver address为forward后的端口格式127.0.0.1:${port}
|
||||
func newStubAndroidDriver(address string, urlPrefix string, readTimeout ...time.Duration) (*stubAndroidDriver, error) {
|
||||
timeout := 10 * time.Second
|
||||
if len(readTimeout) > 0 {
|
||||
timeout = readTimeout[0]
|
||||
}
|
||||
|
||||
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: timeout,
|
||||
}
|
||||
|
||||
if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
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.httpRequest(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.httpRequest(http.MethodPost, sad.concatURL(nil, pathElem...), bsJSON)
|
||||
}
|
||||
|
||||
func (sad *stubAndroidDriver) NewSession(capabilities Capabilities) (SessionInfo, error) {
|
||||
return SessionInfo{}, errDriverNotImplemented
|
||||
}
|
||||
|
||||
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.adbClient.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() (DeviceStatus, error) {
|
||||
app, err := sad.GetForegroundApp()
|
||||
if err != nil {
|
||||
return DeviceStatus{}, err
|
||||
}
|
||||
res, err := sad.sendCommand(app.PackageName, "Hello", nil)
|
||||
if err != nil {
|
||||
return DeviceStatus{}, err
|
||||
}
|
||||
log.Info().Msg(fmt.Sprintf("ping stub result :%v", res))
|
||||
return DeviceStatus{}, nil
|
||||
}
|
||||
|
||||
func (sad *stubAndroidDriver) Source(srcOpt ...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) LoginNoneUIBak(packageName, phoneNumber, captcha string) error {
|
||||
_, err := sad.adbClient.RunShellCommand("am", "broadcast", "-a", fmt.Sprintf("%s.util.crony.action_login", packageName), "-e", "phone", phoneNumber, "-e", "code", captcha)
|
||||
time.Sleep(10 * time.Second)
|
||||
login, err := sad.isLogin(packageName)
|
||||
if err != nil || !login {
|
||||
log.Err(err).Msg("failed to login")
|
||||
return fmt.Errorf("failed to login")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (sad *stubAndroidDriver) LoginNoneUI(packageName, phoneNumber, captcha string) error {
|
||||
params := map[string]interface{}{
|
||||
"phone": phoneNumber,
|
||||
"code": captcha,
|
||||
}
|
||||
resp, err := sad.httpPOST(params, "/host", "/login", "account")
|
||||
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
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
login, err := sad.isLogin(packageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !login {
|
||||
return fmt.Errorf("failed to login")
|
||||
}
|
||||
return 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
|
||||
}
|
||||
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) isLogin(packageName string) (login bool, err error) {
|
||||
resp, err := sad.httpGET("/host", "/login", "/check")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
res, err := resp.valueConvertToJsonObject()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
log.Info().Msgf("%v", res)
|
||||
if res["isSuccess"] != true {
|
||||
err = fmt.Errorf("falied to get is login %s", res["data"])
|
||||
log.Err(err).Msgf("%v", res)
|
||||
return false, err
|
||||
}
|
||||
fmt.Printf("%v", resp)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
56
hrp/pkg/uixt/android_stub_driver_test.go
Normal file
56
hrp/pkg/uixt/android_stub_driver_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package uixt
|
||||
|
||||
import "testing"
|
||||
|
||||
var androidStubDriver *stubAndroidDriver
|
||||
|
||||
func setupStubDriver(t *testing.T) {
|
||||
device, err := NewAndroidDevice()
|
||||
checkErr(t, err)
|
||||
device.STUB = true
|
||||
androidStubDriver, err = device.NewStubDriver(Capabilities{})
|
||||
checkErr(t, err)
|
||||
}
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
setupStubDriver(t)
|
||||
status, err := androidStubDriver.Status()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(status)
|
||||
}
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
setupStubDriver(t)
|
||||
source, err := androidStubDriver.Source()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(source)
|
||||
}
|
||||
|
||||
func TestIsLogin(t *testing.T) {
|
||||
setupStubDriver(t)
|
||||
res, err := androidStubDriver.isLogin("com.ss.android.ugc.aweme")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(res)
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
setupStubDriver(t)
|
||||
err := androidStubDriver.LoginNoneUI("com.ss.android.ugc.aweme", "12342316231", "8517")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
setupStubDriver(t)
|
||||
err := androidStubDriver.LogoutNoneUI("com.ss.android.ugc.aweme")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -21,8 +20,8 @@ var (
|
||||
func setupAndroid(t *testing.T) {
|
||||
device, err := NewAndroidDevice()
|
||||
checkErr(t, err)
|
||||
device.UIA2 = false
|
||||
device.LogOn = true
|
||||
device.UIA2 = true
|
||||
device.LogOn = false
|
||||
driverExt, err = device.NewDriver()
|
||||
checkErr(t, err)
|
||||
}
|
||||
@@ -124,12 +123,9 @@ func TestDriver_DeviceSize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDriver_Source(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setupAndroid(t)
|
||||
|
||||
source, err := driver.Source()
|
||||
source, err := driverExt.Driver.Source()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -199,33 +195,24 @@ func TestDriver_DeviceInfo(t *testing.T) {
|
||||
func TestDriver_Tap(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
driverExt.Driver.StartCaptureLog("")
|
||||
err := driverExt.Driver.Tap(150, 340, WithIdentifier("test"))
|
||||
err := driverExt.TapXY(0.5, 0.5, WithIdentifier("test"), WithPressDuration(4))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
|
||||
err = driverExt.Driver.TapFloat(60.5, 125.5, WithIdentifier("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
result, _ := driverExt.Driver.StopCaptureLog()
|
||||
t.Log(result)
|
||||
//time.Sleep(time.Second)
|
||||
//
|
||||
//err = driverExt.Driver.TapFloat(60.5, 125.5, WithIdentifier("test"))
|
||||
//if err != nil {
|
||||
// t.Fatal(err)
|
||||
//}
|
||||
//time.Sleep(time.Second)
|
||||
//result, _ := driverExt.Driver.StopCaptureLog()
|
||||
//t.Log(result)
|
||||
}
|
||||
|
||||
func TestDriver_Swipe(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.Swipe(400, 1000, 400, 500, WithPressDuration(2000))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.SwipeFloat(400, 555.5, 400, 1255.5)
|
||||
setupAndroid(t)
|
||||
err := driverExt.Driver.Swipe(400, 1000, 400, 500, WithPressDuration(0.5))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -261,7 +248,7 @@ func TestDriver_Drag(t *testing.T) {
|
||||
func TestDriver_SendKeys(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
|
||||
err := driverExt.Driver.SendKeys("Android\"输入速度测试", WithIdentifier("test"))
|
||||
err := driverExt.Driver.SendKeys("辽宁省沈阳市新民市民族街36-4", WithIdentifier("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -306,6 +293,14 @@ func TestDriver_SetRotation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_GetOrientation(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
_, _ = driverExt.Driver.AppTerminate("com.quark.browser")
|
||||
_ = driverExt.Driver.AppLaunch("com.quark.browser")
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = driverExt.Driver.Homescreen()
|
||||
}
|
||||
|
||||
func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) {
|
||||
uiSelector := NewUiSelectorHelper().Text("a").String()
|
||||
if uiSelector != `new UiSelector().text("a");` {
|
||||
@@ -448,8 +443,6 @@ func TestConvertPoints(t *testing.T) {
|
||||
if len(eps) != 3 {
|
||||
t.Fatal()
|
||||
}
|
||||
jsons, _ := json.Marshal(eps)
|
||||
println(fmt.Sprintf("%v", string(jsons)))
|
||||
}
|
||||
|
||||
func TestDriver_ShellInputUnicode(t *testing.T) {
|
||||
|
||||
@@ -29,6 +29,7 @@ func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDrive
|
||||
log.Info().Msg("init uiautomator2 driver")
|
||||
if capabilities == nil {
|
||||
capabilities = NewCapabilities()
|
||||
capabilities.WithWaitForIdleTimeout(0)
|
||||
}
|
||||
driver = new(uiaDriver)
|
||||
if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil {
|
||||
@@ -141,7 +142,12 @@ func (ud *uiaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err er
|
||||
func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) {
|
||||
// register(postHandler, new NewSession("/wd/hub/session"))
|
||||
var rawResp rawResponse
|
||||
data := map[string]interface{}{"capabilities": capabilities}
|
||||
data := make(map[string]interface{})
|
||||
if len(capabilities) == 0 {
|
||||
data["capabilities"] = make(map[string]interface{})
|
||||
} else {
|
||||
data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities}
|
||||
}
|
||||
if rawResp, err = ud.Driver.httpPOST(data, "/session"); err != nil {
|
||||
return SessionInfo{SessionId: ""}, err
|
||||
}
|
||||
@@ -244,10 +250,14 @@ func (ud *uiaDriver) PressBack(options ...ActionOption) (err error) {
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) Homescreen() (err error) {
|
||||
return ud.PressKeyCode(KCHome, KMEmpty)
|
||||
return ud.PressKeyCodes(KCHome, KMEmpty)
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
|
||||
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return ud.PressKeyCodes(keyCode, KMEmpty)
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
|
||||
// register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode"))
|
||||
data := map[string]interface{}{
|
||||
"keycode": keyCode,
|
||||
@@ -293,7 +303,7 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
|
||||
|
||||
duration := 100.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration
|
||||
duration = actionOptions.PressDuration * 1000
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
@@ -399,7 +409,7 @@ func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...Actio
|
||||
|
||||
duration := 200.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration
|
||||
duration = actionOptions.PressDuration * 1000
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
@@ -618,3 +628,10 @@ func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) GetDriverResults() []*DriverResult {
|
||||
defer func() {
|
||||
ud.Driver.driverResults = nil
|
||||
}()
|
||||
return ud.Driver.driverResults
|
||||
}
|
||||
|
||||
@@ -17,10 +17,18 @@ import (
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
urlPrefix *url.URL
|
||||
sessionId string
|
||||
client *http.Client
|
||||
scale float64
|
||||
urlPrefix *url.URL
|
||||
sessionId string
|
||||
client *http.Client
|
||||
scale float64
|
||||
driverResults []*DriverResult
|
||||
}
|
||||
|
||||
type DriverResult struct {
|
||||
RequestUrl string `json:"request_driver_url"`
|
||||
RequestBody string `json:"request_driver_body,omitempty"`
|
||||
RequestDuration time.Duration `json:"request_driver_duration"`
|
||||
RequestTime time.Time `json:"request_driver_time"`
|
||||
}
|
||||
|
||||
func (wd *Driver) concatURL(u *url.URL, elem ...string) string {
|
||||
@@ -73,7 +81,15 @@ func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (raw
|
||||
}()
|
||||
|
||||
rawResp, err = io.ReadAll(resp.Body)
|
||||
logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String())
|
||||
duration := time.Since(start)
|
||||
driverResult := &DriverResult{
|
||||
RequestUrl: rawURL,
|
||||
RequestBody: string(rawBody),
|
||||
RequestDuration: duration,
|
||||
RequestTime: time.Now(),
|
||||
}
|
||||
wd.driverResults = append(wd.driverResults, driverResult)
|
||||
logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", duration.String())
|
||||
if !strings.HasSuffix(rawURL, "screenshot") {
|
||||
// avoid printing screenshot data
|
||||
logger.Str("response", string(rawResp))
|
||||
|
||||
BIN
hrp/pkg/uixt/evalite
Normal file
BIN
hrp/pkg/uixt/evalite
Normal file
Binary file not shown.
@@ -126,11 +126,16 @@ type DriverExt struct {
|
||||
lastPopup *PopupInfo
|
||||
}
|
||||
|
||||
func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dExt *DriverExt, err error) {
|
||||
func newDriverExt(device Device, driver WebDriver, options ...DriverOption) (dExt *DriverExt, err error) {
|
||||
driverOptions := NewDriverOptions()
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
|
||||
dExt = &DriverExt{
|
||||
Device: device,
|
||||
Driver: driver,
|
||||
plugin: plugin,
|
||||
plugin: driverOptions.plugin,
|
||||
cacheStepData: cacheStepData{},
|
||||
interruptSignal: make(chan os.Signal, 1),
|
||||
}
|
||||
@@ -150,21 +155,86 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get screen resolution failed")
|
||||
}
|
||||
|
||||
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
|
||||
return nil, err
|
||||
if driverOptions.withImageService {
|
||||
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// create results directory
|
||||
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create results directory failed")
|
||||
}
|
||||
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create screenshots directory failed")
|
||||
if driverOptions.withResultFolder {
|
||||
// create results directory
|
||||
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create results directory failed")
|
||||
}
|
||||
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
|
||||
return nil, errors.Wrap(err, "create screenshots directory failed")
|
||||
}
|
||||
}
|
||||
return dExt, nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) InstallByUrl(url string, opts *InstallOptions) error {
|
||||
// 获取当前目录
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将文件保存到当前目录
|
||||
appPath := filepath.Join(cwd, fmt.Sprint(time.Now().UnixNano())) // 替换为你想保存的文件名
|
||||
err = builtin.DownloadFile(appPath, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dExt.Install(appPath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) Uninstall(packageName string, options ...ActionOption) error {
|
||||
actionOptions := NewActionOptions(options...)
|
||||
err := dExt.Device.Uninstall(packageName)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to uninstall")
|
||||
}
|
||||
if actionOptions.IgnoreNotFoundError {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) Install(filePath string, opts *InstallOptions) error {
|
||||
if _, ok := dExt.Device.(*AndroidDevice); ok {
|
||||
stopChan := make(chan struct{})
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
actions := []TapTextAction{
|
||||
{Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||
{Text: "^已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||
}
|
||||
_ = dExt.Driver.TapByTexts(actions...)
|
||||
_ = dExt.TapByOCR("^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", WithRegex(true), WithIgnoreNotFoundError(true))
|
||||
case <-stopChan:
|
||||
fmt.Println("Ticker stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
close(stopChan)
|
||||
}()
|
||||
}
|
||||
|
||||
return dExt.Device.Install(filePath, opts)
|
||||
}
|
||||
|
||||
// takeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
|
||||
func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path string, err error) {
|
||||
// iOS 优先使用 MJPEG 流进行截图,性能最优
|
||||
@@ -267,7 +337,7 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
|
||||
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
||||
cacheData["screen_results"] = dExt.cacheStepData.screenResults
|
||||
cacheData["e2e_results"] = dExt.cacheStepData.e2eDelay
|
||||
|
||||
cacheData["driver_request_results"] = dExt.Driver.GetDriverResults()
|
||||
// clear cache
|
||||
dExt.cacheStepData.reset()
|
||||
return cacheData
|
||||
|
||||
48
hrp/pkg/uixt/install.go
Normal file
48
hrp/pkg/uixt/install.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package uixt
|
||||
|
||||
type InstallOptions struct {
|
||||
Reinstall bool
|
||||
GrantPermission bool
|
||||
Downgrade bool
|
||||
RetryTime int
|
||||
}
|
||||
|
||||
type InstallOption func(o *InstallOptions)
|
||||
|
||||
func NewInstallOptions(options ...InstallOption) *InstallOptions {
|
||||
installOptions := &InstallOptions{}
|
||||
for _, option := range options {
|
||||
option(installOptions)
|
||||
}
|
||||
return installOptions
|
||||
}
|
||||
|
||||
func WithReinstall(reinstall bool) InstallOption {
|
||||
return func(o *InstallOptions) {
|
||||
o.Reinstall = reinstall
|
||||
}
|
||||
}
|
||||
|
||||
func WithGrantPermission(grantPermission bool) InstallOption {
|
||||
return func(o *InstallOptions) {
|
||||
o.GrantPermission = grantPermission
|
||||
}
|
||||
}
|
||||
|
||||
func WithDowngrade(downgrade bool) InstallOption {
|
||||
return func(o *InstallOptions) {
|
||||
o.Downgrade = downgrade
|
||||
}
|
||||
}
|
||||
|
||||
func WithRetryTime(retryTime int) InstallOption {
|
||||
return func(o *InstallOptions) {
|
||||
o.RetryTime = retryTime
|
||||
}
|
||||
}
|
||||
|
||||
type InstallResult struct {
|
||||
Result int `json:"result"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
@@ -250,11 +250,7 @@ type Screen struct {
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
ProcessArguments struct {
|
||||
Env interface{} `json:"env"`
|
||||
Args []interface{} `json:"args"`
|
||||
} `json:"processArguments"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AppBaseInfo
|
||||
}
|
||||
|
||||
@@ -264,6 +260,10 @@ type AppBaseInfo struct {
|
||||
ViewController string `json:"viewController,omitempty"` // ios view controller
|
||||
PackageName string `json:"packageName,omitempty"` // android package name
|
||||
Activity string `json:"activity,omitempty"` // android activity
|
||||
VersionName string `json:"versionName,omitempty"`
|
||||
VersionCode int `json:"versionCode,omitempty"`
|
||||
AppName string `json:"appName,omitempty"`
|
||||
// AppIcon string `json:"appIcon,omitempty"`
|
||||
}
|
||||
|
||||
type AppState int
|
||||
@@ -374,6 +374,11 @@ func (opt SourceOption) WithFormatAsJson() SourceOption {
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt SourceOption) WithProcessName(processName string) SourceOption {
|
||||
opt["processName"] = processName
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithFormatAsXml Application elements tree in form of xml string
|
||||
func (opt SourceOption) WithFormatAsXml() SourceOption {
|
||||
opt["format"] = "xml"
|
||||
@@ -446,8 +451,17 @@ type Rect struct {
|
||||
}
|
||||
|
||||
type DriverOptions struct {
|
||||
capabilities Capabilities
|
||||
plugin funplugin.IPlugin
|
||||
capabilities Capabilities
|
||||
plugin funplugin.IPlugin
|
||||
withImageService bool
|
||||
withResultFolder bool
|
||||
}
|
||||
|
||||
func NewDriverOptions() *DriverOptions {
|
||||
return &DriverOptions{
|
||||
withImageService: true,
|
||||
withResultFolder: true,
|
||||
}
|
||||
}
|
||||
|
||||
type DriverOption func(*DriverOptions)
|
||||
@@ -458,6 +472,18 @@ func WithDriverCapabilities(capabilities Capabilities) DriverOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverImageService(withImageService bool) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.withImageService = withImageService
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverResultFolder(withResultFolder bool) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.withResultFolder = withResultFolder
|
||||
}
|
||||
}
|
||||
|
||||
func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
|
||||
return func(options *DriverOptions) {
|
||||
options.plugin = plugin
|
||||
@@ -466,8 +492,8 @@ func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
|
||||
|
||||
// current implemeted device: IOSDevice, AndroidDevice
|
||||
type Device interface {
|
||||
System() string // ios or android
|
||||
UUID() string // ios udid or android serial
|
||||
Init() error // init android device
|
||||
UUID() string // ios udid or android serial
|
||||
LogEnabled() bool
|
||||
NewDriver(...DriverOption) (driverExt *DriverExt, err error)
|
||||
|
||||
@@ -476,6 +502,10 @@ type Device interface {
|
||||
|
||||
StartPcap() error
|
||||
StopPcap() string
|
||||
|
||||
Uninstall(packageName string) error
|
||||
|
||||
Install(appPath string, opts *InstallOptions) error
|
||||
}
|
||||
|
||||
type ForegroundApp struct {
|
||||
@@ -524,6 +554,8 @@ type WebDriver interface {
|
||||
// Homescreen Forces the device under test to switch to the home screen
|
||||
Homescreen() error
|
||||
|
||||
Unlock() (err error)
|
||||
|
||||
// AppLaunch Launch an application with given bundle identifier in scope of current session.
|
||||
// !This method is only available since Xcode9 SDK
|
||||
AppLaunch(packageName string) error
|
||||
@@ -570,6 +602,8 @@ type WebDriver interface {
|
||||
// It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330
|
||||
GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error)
|
||||
|
||||
SetIme(ime string) error
|
||||
|
||||
// SendKeys Types a string into active element. There must be element with keyboard focus,
|
||||
// otherwise an error is raised.
|
||||
// WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60
|
||||
@@ -578,17 +612,25 @@ type WebDriver interface {
|
||||
// Input works like SendKeys
|
||||
Input(text string, options ...ActionOption) error
|
||||
|
||||
Clear(packageName string) error
|
||||
|
||||
// PressButton Presses the corresponding hardware button on the device
|
||||
PressButton(devBtn DeviceButton) error
|
||||
|
||||
// PressBack Presses the back button
|
||||
PressBack(options ...ActionOption) error
|
||||
|
||||
PressKeyCode(keyCode KeyCode) (err error)
|
||||
|
||||
Screenshot() (*bytes.Buffer, error)
|
||||
|
||||
// Source Return application elements tree
|
||||
Source(srcOpt ...SourceOption) (string, error)
|
||||
|
||||
LoginNoneUI(packageName, phoneNumber string, captcha string) error
|
||||
|
||||
LogoutNoneUI(packageName string) error
|
||||
|
||||
TapByText(text string, options ...ActionOption) error
|
||||
|
||||
TapByTexts(actions ...TapTextAction) error
|
||||
@@ -609,4 +651,5 @@ type WebDriver interface {
|
||||
// triggers the log capture and returns the log entries
|
||||
StartCaptureLog(identifier ...string) (err error)
|
||||
StopCaptureLog() (result interface{}, err error)
|
||||
GetDriverResults() []*DriverResult
|
||||
}
|
||||
|
||||
@@ -276,6 +276,7 @@ type IOSDevice struct {
|
||||
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"`
|
||||
@@ -294,8 +295,8 @@ type IOSDevice struct {
|
||||
pcapFile string // saved pcap file path
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) System() string {
|
||||
return "ios"
|
||||
func (dev *IOSDevice) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) UUID() string {
|
||||
@@ -307,7 +308,7 @@ func (dev *IOSDevice) LogEnabled() bool {
|
||||
}
|
||||
|
||||
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
|
||||
driverOptions := &DriverOptions{}
|
||||
driverOptions := NewDriverOptions()
|
||||
for _, option := range options {
|
||||
option(driverOptions)
|
||||
}
|
||||
@@ -338,7 +339,7 @@ func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt,
|
||||
}
|
||||
}
|
||||
|
||||
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
|
||||
driverExt, err = newDriverExt(dev, driver, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -471,6 +472,20 @@ func (dev *IOSDevice) StopPcap() string {
|
||||
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")
|
||||
@@ -635,7 +650,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver
|
||||
wd := new(wdaDriver)
|
||||
wd.client = http.DefaultClient
|
||||
|
||||
host := "127.0.0.1"
|
||||
host := "localhost"
|
||||
if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil {
|
||||
return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error())
|
||||
}
|
||||
|
||||
@@ -591,6 +591,14 @@ func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) SetIme(ime string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) PressKeyCode(keyCode KeyCode) (err error) {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
||||
actionOptions := NewActionOptions(options...)
|
||||
@@ -607,6 +615,10 @@ func (wd *wdaDriver) Input(text string, options ...ActionOption) (err error) {
|
||||
return wd.SendKeys(text, options...)
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) Clear(packageName string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
// PressBack simulates a short press on the BACK button.
|
||||
func (wd *wdaDriver) PressBack(options ...ActionOption) (err error) {
|
||||
actionOptions := NewActionOptions(options...)
|
||||
@@ -651,6 +663,14 @@ func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) LogoutNoneUI(packageName string) error {
|
||||
return errDriverNotImplemented
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) StartCamera() (err error) {
|
||||
// start camera, alias for app_launch com.apple.camera
|
||||
return wd.AppLaunch("com.apple.camera")
|
||||
@@ -877,6 +897,13 @@ func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
return reply.Value, nil
|
||||
}
|
||||
|
||||
func (wd *wdaDriver) GetDriverResults() []*DriverResult {
|
||||
defer func() {
|
||||
wd.Driver.driverResults = nil
|
||||
}()
|
||||
return wd.Driver.driverResults
|
||||
}
|
||||
|
||||
type rawResponse []byte
|
||||
|
||||
func (r rawResponse) checkErr() (err error) {
|
||||
@@ -939,6 +966,13 @@ func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage,
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToJsonObject() (obj map[string]interface{}, err error) {
|
||||
if err = json.Unmarshal(r, &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) {
|
||||
str, err := r.valueConvertToString()
|
||||
if err != nil {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,7 +28,7 @@ func setup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
iOSDriverExt, err = newDriverExt(device, driver, nil)
|
||||
iOSDriverExt, err = newDriverExt(device, driver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -37,6 +39,15 @@ func TestViaUSB(t *testing.T) {
|
||||
t.Log(driver.Status())
|
||||
}
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
setup(t)
|
||||
err := iOSDriverExt.Install("/Users/bytedance/Downloads/com.yueyou.cyreader_1387717110_7.54.20.ipa", NewInstallOptions(WithRetryTime(5)))
|
||||
log.Error().Err(err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIOSDevice(t *testing.T) {
|
||||
device, _ := NewIOSDevice()
|
||||
if device != nil {
|
||||
|
||||
@@ -62,11 +62,11 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error {
|
||||
// filter ocr texts with time format
|
||||
var liveTimeTexts []string
|
||||
for _, ocrText := range ocrTexts {
|
||||
if len(ocrText.Text) < 10 || strings.Contains(ocrText.Text, ":") {
|
||||
if len(ocrText.Text) < 13 || strings.Contains(ocrText.Text, ":") {
|
||||
continue
|
||||
}
|
||||
// exclude digit(s) recognized as letter(s)
|
||||
_, errParseInt := strconv.ParseInt(ocrText.Text[:10], 10, 64)
|
||||
_, errParseInt := strconv.ParseInt(ocrText.Text[:13], 10, 64)
|
||||
if errParseInt != nil {
|
||||
continue
|
||||
}
|
||||
@@ -81,11 +81,6 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(liveTimeText) < 13 {
|
||||
for (13 - len(liveTimeText)) > 0 {
|
||||
liveTimeText += "0"
|
||||
}
|
||||
}
|
||||
liveTimeInt, err := strconv.Atoi(liveTimeText)
|
||||
if err != nil {
|
||||
liveTimeInt = 0
|
||||
|
||||
Reference in New Issue
Block a user