mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 17:29:56 +08:00
refactor: NewAndroidDevice
This commit is contained in:
@@ -28,10 +28,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// adb server
|
||||
AdbServerHost = "localhost"
|
||||
AdbServerPort = gadb.AdbServerPort // 5037
|
||||
|
||||
EvalInstallerPackageName = "sogou.mobile.explorer"
|
||||
InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d"
|
||||
)
|
||||
@@ -42,76 +38,54 @@ var evalite embed.FS
|
||||
func NewAndroidDevice(opts ...option.AndroidDeviceOption) (device *AndroidDevice, err error) {
|
||||
androidOptions := option.NewAndroidDeviceOptions(opts...)
|
||||
|
||||
deviceList, err := GetAndroidDevices(androidOptions.SerialNumber)
|
||||
// get all attached android devices
|
||||
adbClient, err := gadb.NewClientWith(
|
||||
androidOptions.AdbServerHost, androidOptions.AdbServerPort)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.DeviceConnectionError, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
devices, err := adbClient.DeviceList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return nil, errors.Wrapf(code.DeviceConnectionError,
|
||||
"no attached android devices")
|
||||
}
|
||||
|
||||
if androidOptions.SerialNumber == "" && len(deviceList) > 1 {
|
||||
return nil, errors.Wrap(code.DeviceConnectionError, "more than one device connected, please specify the serial")
|
||||
}
|
||||
|
||||
dev := deviceList[0]
|
||||
|
||||
// filter device by serial
|
||||
var gadbDevice *gadb.Device
|
||||
if androidOptions.SerialNumber == "" {
|
||||
selectSerial := dev.Serial()
|
||||
androidOptions.SerialNumber = selectSerial
|
||||
log.Warn().
|
||||
Str("serial", androidOptions.SerialNumber).
|
||||
Msg("android SerialNumber is not specified, select the first one")
|
||||
if len(devices) > 1 {
|
||||
return nil, errors.Wrap(code.DeviceConnectionError,
|
||||
"more than one device connected, please specify the serial")
|
||||
}
|
||||
gadbDevice = devices[0]
|
||||
androidOptions.SerialNumber = gadbDevice.Serial()
|
||||
log.Warn().Str("serial", androidOptions.SerialNumber).
|
||||
Msg("android SerialNumber is not specified, select the attached one")
|
||||
} else {
|
||||
for _, d := range devices {
|
||||
if d.Serial() == androidOptions.SerialNumber {
|
||||
gadbDevice = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if gadbDevice == nil {
|
||||
return nil, errors.Wrapf(code.DeviceConnectionError,
|
||||
"android device %s not attached", androidOptions.SerialNumber)
|
||||
}
|
||||
}
|
||||
|
||||
device = &AndroidDevice{
|
||||
Device: dev,
|
||||
Device: gadbDevice,
|
||||
AndroidDeviceOptions: androidOptions,
|
||||
Logcat: NewAdbLogcat(androidOptions.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.DeviceShellExecError, err.Error())
|
||||
}
|
||||
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
|
||||
var adbClient gadb.Client
|
||||
if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if devices, err = adbClient.DeviceList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var deviceList []*gadb.Device
|
||||
// filter by serial
|
||||
for _, d := range devices {
|
||||
for _, s := range serial {
|
||||
if s != "" && s != d.Serial() {
|
||||
continue
|
||||
}
|
||||
deviceList = append(deviceList, d)
|
||||
}
|
||||
}
|
||||
|
||||
if len(deviceList) == 0 {
|
||||
var err error
|
||||
if serial == nil || (len(serial) == 1 && serial[0] == "") {
|
||||
err = fmt.Errorf("no android device found")
|
||||
} else {
|
||||
err = fmt.Errorf("no android device found for serial %v", serial)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return deviceList, nil
|
||||
}
|
||||
|
||||
type AndroidDevice struct {
|
||||
*gadb.Device
|
||||
*option.AndroidDeviceOptions
|
||||
@@ -122,6 +96,16 @@ func (dev *AndroidDevice) Setup() error {
|
||||
dev.RunShellCommand("ime", "enable", UnicodeImePackageName)
|
||||
dev.RunShellCommand("rm", "-r", config.DeviceActionLogFilePath)
|
||||
|
||||
// setup evalite
|
||||
evalToolRaw, err := evalite.ReadFile("evalite")
|
||||
if err != nil {
|
||||
return errors.Wrap(code.LoadFileError, err.Error())
|
||||
}
|
||||
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now())
|
||||
if err != nil {
|
||||
return errors.Wrap(code.DeviceShellExecError, err.Error())
|
||||
}
|
||||
|
||||
if dev.UIA2 {
|
||||
// uiautomator2 server must be started before
|
||||
|
||||
@@ -211,11 +195,6 @@ func (dev *AndroidDevice) StopPcap() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Uninstall(packageName string) error {
|
||||
_, err := dev.RunShellCommand("uninstall", packageName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Install(apkPath string, opts ...option.InstallOption) error {
|
||||
installOpts := option.NewInstallOptions(opts...)
|
||||
brand, err := dev.Brand()
|
||||
@@ -317,6 +296,11 @@ func (dev *AndroidDevice) installCommon(apkPath string, args ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) Uninstall(packageName string) error {
|
||||
_, err := dev.RunShellCommand("uninstall", packageName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (dev *AndroidDevice) GetCurrentWindow() (windowInfo WindowInfo, err error) {
|
||||
// adb shell dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'
|
||||
output, err := dev.RunShellCommand("dumpsys", "window", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
|
||||
@@ -618,346 +602,3 @@ func ConvertPoints(lines []string) (eps []ExportPoint) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type UiSelectorHelper struct {
|
||||
value *bytes.Buffer
|
||||
}
|
||||
|
||||
func NewUiSelectorHelper() UiSelectorHelper {
|
||||
return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")}
|
||||
}
|
||||
|
||||
func (s UiSelectorHelper) String() string {
|
||||
return s.value.String() + ";"
|
||||
}
|
||||
|
||||
// Text Set the search criteria to match the visible text displayed
|
||||
// in a widget (for example, the text label to launch an app).
|
||||
//
|
||||
// The text for the element must match exactly with the string in your input
|
||||
// argument. Matching is case-sensitive.
|
||||
func (s UiSelectorHelper) Text(text string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.text("%s")`, text))
|
||||
return s
|
||||
}
|
||||
|
||||
// TextMatches Set the search criteria to match the visible text displayed in a layout
|
||||
// element, using a regular expression.
|
||||
//
|
||||
// The text in the widget must match exactly with the string in your
|
||||
// input argument.
|
||||
func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex))
|
||||
return s
|
||||
}
|
||||
|
||||
// TextStartsWith Set the search criteria to match visible text in a widget that is
|
||||
// prefixed by the text parameter.
|
||||
//
|
||||
// The matching is case-insensitive.
|
||||
func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text))
|
||||
return s
|
||||
}
|
||||
|
||||
// TextContains Set the search criteria to match the visible text in a widget
|
||||
// where the visible text must contain the string in your input argument.
|
||||
//
|
||||
// The matching is case-sensitive.
|
||||
func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text))
|
||||
return s
|
||||
}
|
||||
|
||||
// ClassName Set the search criteria to match the class property
|
||||
// for a widget (for example, "android.widget.Button").
|
||||
func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.className("%s")`, className))
|
||||
return s
|
||||
}
|
||||
|
||||
// ClassNameMatches Set the search criteria to match the class property
|
||||
// for a widget, using a regular expression.
|
||||
func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex))
|
||||
return s
|
||||
}
|
||||
|
||||
// Description Set the search criteria to match the content-description
|
||||
// property for a widget.
|
||||
//
|
||||
// The content-description is typically used
|
||||
// by the Android Accessibility framework to
|
||||
// provide an audio prompt for the widget when
|
||||
// the widget is selected. The content-description
|
||||
// for the widget must match exactly
|
||||
// with the string in your input argument.
|
||||
//
|
||||
// Matching is case-sensitive.
|
||||
func (s UiSelectorHelper) Description(desc string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc))
|
||||
return s
|
||||
}
|
||||
|
||||
// DescriptionMatches Set the search criteria to match the content-description
|
||||
// property for a widget.
|
||||
//
|
||||
// The content-description is typically used
|
||||
// by the Android Accessibility framework to
|
||||
// provide an audio prompt for the widget when
|
||||
// the widget is selected. The content-description
|
||||
// for the widget must match exactly
|
||||
// with the string in your input argument.
|
||||
func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex))
|
||||
return s
|
||||
}
|
||||
|
||||
// DescriptionStartsWith Set the search criteria to match the content-description
|
||||
// property for a widget.
|
||||
//
|
||||
// The content-description is typically used
|
||||
// by the Android Accessibility framework to
|
||||
// provide an audio prompt for the widget when
|
||||
// the widget is selected. The content-description
|
||||
// for the widget must start
|
||||
// with the string in your input argument.
|
||||
//
|
||||
// Matching is case-insensitive.
|
||||
func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc))
|
||||
return s
|
||||
}
|
||||
|
||||
// DescriptionContains Set the search criteria to match the content-description
|
||||
// property for a widget.
|
||||
//
|
||||
// The content-description is typically used
|
||||
// by the Android Accessibility framework to
|
||||
// provide an audio prompt for the widget when
|
||||
// the widget is selected. The content-description
|
||||
// for the widget must contain
|
||||
// the string in your input argument.
|
||||
//
|
||||
// Matching is case-insensitive.
|
||||
func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc))
|
||||
return s
|
||||
}
|
||||
|
||||
// ResourceId Set the search criteria to match the given resource ID.
|
||||
func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id))
|
||||
return s
|
||||
}
|
||||
|
||||
// ResourceIdMatches Set the search criteria to match the resource ID
|
||||
// of the widget, using a regular expression.
|
||||
func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex))
|
||||
return s
|
||||
}
|
||||
|
||||
// Index Set the search criteria to match the widget by its node
|
||||
// index in the layout hierarchy.
|
||||
//
|
||||
// The index value must be 0 or greater.
|
||||
//
|
||||
// Using the index can be unreliable and should only
|
||||
// be used as a last resort for matching. Instead,
|
||||
// consider using the `Instance(int)` method.
|
||||
func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.index(%d)`, index))
|
||||
return s
|
||||
}
|
||||
|
||||
// Instance Set the search criteria to match the
|
||||
// widget by its instance number.
|
||||
//
|
||||
// The instance value must be 0 or greater, where
|
||||
// the first instance is 0.
|
||||
//
|
||||
// For example, to simulate a user click on
|
||||
// the third image that is enabled in a UI screen, you
|
||||
// could specify a search criteria where the instance is
|
||||
// 2, the `className(String)` matches the image
|
||||
// widget class, and `enabled(boolean)` is true.
|
||||
// The code would look like this:
|
||||
//
|
||||
// `new UiSelector().className("android.widget.ImageView")
|
||||
// .enabled(true).instance(2);`
|
||||
func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance))
|
||||
return s
|
||||
}
|
||||
|
||||
// Enabled Set the search criteria to match widgets that are enabled.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Focused Set the search criteria to match widgets that have focus.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Focusable Set the search criteria to match widgets that are focusable.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Scrollable Set the search criteria to match widgets that are scrollable.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Selected Set the search criteria to match widgets that
|
||||
// are currently selected.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Checked Set the search criteria to match widgets that
|
||||
// are currently checked (usually for checkboxes).
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Checkable Set the search criteria to match widgets that are checkable.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// Clickable Set the search criteria to match widgets that are clickable.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// LongClickable Set the search criteria to match widgets that are long-clickable.
|
||||
//
|
||||
// Typically, using this search criteria alone is not useful.
|
||||
// You should also include additional criteria, such as text,
|
||||
// content-description, or the class name for a widget.
|
||||
//
|
||||
// If no other search criteria is specified, and there is more
|
||||
// than one matching widget, the first widget in the tree
|
||||
// is selected.
|
||||
func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b))
|
||||
return s
|
||||
}
|
||||
|
||||
// packageName Set the search criteria to match the package name
|
||||
// of the application that contains the widget.
|
||||
func (s UiSelectorHelper) packageName(name string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name))
|
||||
return s
|
||||
}
|
||||
|
||||
// PackageNameMatches Set the search criteria to match the package name
|
||||
// of the application that contains the widget.
|
||||
func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex))
|
||||
return s
|
||||
}
|
||||
|
||||
// ChildSelector Adds a child UiSelector criteria to this selector.
|
||||
//
|
||||
// Use this selector to narrow the search scope to
|
||||
// child widgets under a specific parent widget.
|
||||
func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String()))
|
||||
return s
|
||||
}
|
||||
|
||||
func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String()))
|
||||
return s
|
||||
}
|
||||
|
||||
func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String()))
|
||||
return s
|
||||
}
|
||||
|
||||
// FromParent Adds a child UiSelector criteria to this selector which is used to
|
||||
// start search from the parent widget.
|
||||
//
|
||||
// Use this selector to narrow the search scope to
|
||||
// sibling widgets as well all child widgets under a parent.
|
||||
func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper {
|
||||
s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String()))
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -236,33 +236,6 @@ func TestDriver_GetOrientation(t *testing.T) {
|
||||
_ = driverExt.Driver.Homescreen()
|
||||
}
|
||||
|
||||
func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) {
|
||||
uiSelector := NewUiSelectorHelper().Text("a").String()
|
||||
if uiSelector != `new UiSelector().text("a");` {
|
||||
t.Fatal("[ERROR]", uiSelector)
|
||||
}
|
||||
|
||||
uiSelector = NewUiSelectorHelper().Text("a").TextStartsWith("b").String()
|
||||
if uiSelector != `new UiSelector().text("a").textStartsWith("b");` {
|
||||
t.Fatal("[ERROR]", uiSelector)
|
||||
}
|
||||
|
||||
uiSelector = NewUiSelectorHelper().ClassName("android.widget.LinearLayout").Index(6).String()
|
||||
if uiSelector != `new UiSelector().className("android.widget.LinearLayout").index(6);` {
|
||||
t.Fatal("[ERROR]", uiSelector)
|
||||
}
|
||||
|
||||
uiSelector = NewUiSelectorHelper().Focused(false).Instance(6).String()
|
||||
if uiSelector != `new UiSelector().focused(false).instance(6);` {
|
||||
t.Fatal("[ERROR]", uiSelector)
|
||||
}
|
||||
|
||||
uiSelector = NewUiSelectorHelper().ChildSelector(NewUiSelectorHelper().Enabled(true)).String()
|
||||
if uiSelector != `new UiSelector().childSelector(new UiSelector().enabled(true));` {
|
||||
t.Fatal("[ERROR]", uiSelector)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFreePort(t *testing.T) {
|
||||
freePort, err := builtin.GetFreePort()
|
||||
if err != nil {
|
||||
@@ -271,16 +244,6 @@ func Test_getFreePort(t *testing.T) {
|
||||
t.Log(freePort)
|
||||
}
|
||||
|
||||
func TestDeviceList(t *testing.T) {
|
||||
devices, err := GetAndroidDevices()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range devices {
|
||||
t.Log(devices[i].Serial())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_AppLaunch(t *testing.T) {
|
||||
device, _ := NewAndroidDevice()
|
||||
driver, err := device.NewDriver()
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user