Merge branch 'master' into dependabot/go_modules/golang.org/x/text-0.3.8

This commit is contained in:
debugtalk
2023-04-15 00:21:17 +08:00
committed by GitHub
32 changed files with 909 additions and 181 deletions

View File

@@ -1,5 +1,20 @@
# Release History
## v4.3.3 (2023-04-14)
**go version**
- feat: add `sleep_random` to sleep random seconds, with weight for multiple time ranges
- feat: input text with adb
- feat: add adb `screencap` sub command
- feat: add `IsAppInForeground` to check if the given package is in foreground
- feat: check if app is in foreground when step failed
- fix: adb driver for TapFloat
- fix: stop logcat only when enabled
- fix: do not fail case when kill logcat error
- fix: take screenshot after each step
- fix: screencap compatibility for shell v1 and v2
## v4.3.2 (2022-12-26)
**go version**

View File

@@ -43,7 +43,7 @@
}
},
{
"name": "滑动 Feed 35 次,随机间隔 0-20s",
"name": "滑动 Feed 3 次,随机间隔 0-5s",
"android": {
"actions": [
{
@@ -54,15 +54,15 @@
"method": "sleep_random",
"params": [
0,
20
5
]
}
]
},
"loops": 35
"loops": 3
},
{
"name": "滑动 Feed 15 次,随机间隔 15-50s",
"name": "滑动 Feed 1 次,随机间隔 5-10s",
"android": {
"actions": [
{
@@ -72,13 +72,36 @@
{
"method": "sleep_random",
"params": [
15,
50
5,
10
]
}
]
},
"loops": 15
"loops": 1
},
{
"name": "滑动 Feed 10 次70% 随机间隔 0-5s30% 随机间隔 5-10s",
"android": {
"actions": [
{
"method": "swipe",
"params": "up"
},
{
"method": "sleep_random",
"params": [
0,
5,
0.7,
5,
10,
0.3
]
}
]
},
"loops": 10
}
]
}

View File

@@ -25,16 +25,21 @@ func TestAndroidDouyinFeedTest(t *testing.T) {
hrp.NewStep("处理青少年弹窗").
Android().
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)),
hrp.NewStep("滑动 Feed 35 次,随机间隔 0-20s").
Loop(35).
hrp.NewStep("滑动 Feed 3 次,随机间隔 0-5s").
Loop(3).
Android().
SwipeUp().
SleepRandom(0, 20),
hrp.NewStep("滑动 Feed 15 次,随机间隔 15-50s").
Loop(15).
SleepRandom(0, 5),
hrp.NewStep("滑动 Feed 1 次,随机间隔 5-10s").
Loop(1).
Android().
SwipeUp().
SleepRandom(15, 50),
SleepRandom(5, 10),
hrp.NewStep("滑动 Feed 10 次70% 随机间隔 0-5s30% 随机间隔 5-10s").
Loop(10).
Android().
SwipeUp().
SleepRandom(0, 5, 0.7, 5, 10, 0.3),
},
}

6
go.mod
View File

@@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
gocv.io/x/gocv v0.31.0
golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1
@@ -75,8 +75,8 @@ require (
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 // indirect

12
go.sum
View File

@@ -513,8 +513,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1 h1:TWZxd/th7FbRSMret2MVQdlI8uT49QEtwZdvJrxjEHU=
golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -626,8 +626,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -639,8 +639,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@@ -5,10 +5,8 @@ import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
@@ -21,22 +19,10 @@ var listAndroidDevicesCmd = &cobra.Command{
Use: "devices",
Short: "List all Android devices",
RunE: func(cmd *cobra.Command, args []string) error {
devices, err := uixt.DeviceList()
deviceList, err := uixt.GetAndroidDevices(serial)
if err != nil {
return errors.Wrap(err, "list android devices failed")
}
var deviceList []gadb.Device
// filter by serial
for _, d := range devices {
if serial != "" && serial != d.Serial() {
continue
}
deviceList = append(deviceList, d)
}
if serial != "" && len(deviceList) == 0 {
fmt.Printf("no android device found for serial: %s\n", serial)
os.Exit(1)
fmt.Println(err)
os.Exit(0)
}
for _, d := range deviceList {

View File

@@ -1,6 +1,13 @@
package adb
import "github.com/spf13/cobra"
import (
"fmt"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
var androidRootCmd = &cobra.Command{
Use: "adb",
@@ -8,6 +15,17 @@ var androidRootCmd = &cobra.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
}
func getDevice(serial string) (*gadb.Device, error) {
devices, err := uixt.GetAndroidDevices(serial)
if err != nil {
return nil, err
}
if len(devices) > 1 {
return nil, fmt.Errorf("found multiple attached devices, please specify android serial")
}
return devices[0], nil
}
func Init(rootCmd *cobra.Command) {
rootCmd.AddCommand(androidRootCmd)
}

37
hrp/cmd/adb/screencap.go Normal file
View File

@@ -0,0 +1,37 @@
package adb
import (
"fmt"
"io/ioutil"
"time"
"github.com/spf13/cobra"
)
var screencapAndroidDevicesCmd = &cobra.Command{
Use: "screencap",
Short: "Start android screen capture",
RunE: func(cmd *cobra.Command, args []string) error {
device, err := getDevice(serial)
if err != nil {
return err
}
res, err := device.ScreenCap()
if err != nil {
return err
}
filepath := fmt.Sprintf("screencap_%d.png", time.Now().Unix())
if err = ioutil.WriteFile(filepath, res, 0o644); err != nil {
return err
}
fmt.Println("screencap saved to", filepath)
return nil
},
}
func init() {
screencapAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial")
androidRootCmd.AddCommand(screencapAndroidDevicesCmd)
}

View File

@@ -70,18 +70,10 @@ var listDevicesCmd = &cobra.Command{
Short: "List all iOS devices",
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
RunE: func(cmd *cobra.Command, args []string) error {
devices, err := uixt.IOSDevices(udid)
devices, err := uixt.GetIOSDevices(udid)
if err != nil {
return err
}
if len(devices) == 0 {
if udid != "" {
fmt.Printf("no ios device found for udid: %s\n", udid)
os.Exit(1)
} else {
fmt.Println("no ios device found")
os.Exit(0)
}
fmt.Println(err)
os.Exit(0)
}
for _, d := range devices {

View File

@@ -2,7 +2,6 @@ package ios
import (
"fmt"
"os"
"github.com/spf13/cobra"
@@ -16,16 +15,12 @@ var iosRootCmd = &cobra.Command{
}
func getDevice(udid string) (gidevice.Device, error) {
devices, err := uixt.IOSDevices(udid)
devices, err := uixt.GetIOSDevices(udid)
if err != nil {
return nil, err
}
if len(devices) == 0 {
fmt.Println("no ios device found")
os.Exit(1)
}
if len(devices) > 1 {
return nil, fmt.Errorf("multiple devices found, please specify udid")
return nil, fmt.Errorf("found multiple attached devices, please specify ios udid")
}
return devices[0], nil
}

View File

@@ -67,8 +67,9 @@ var (
// UI automation related: [70, 80)
var (
MobileUIDriverError = errors.New("mobile UI driver error") // 70
MobileUIValidationError = errors.New("mobile UI validation error") // 75
MobileUIDriverError = errors.New("mobile UI driver error") // 70
MobileUIValidationError = errors.New("mobile UI validation error") // 75
MobileUIAppNotInForegroundError = errors.New("mobile UI app not in foreground error") // 76
)
// OCR related: [80, 90)

View File

@@ -1 +1 @@
v4.3.2
v4.3.3.2304142356

View File

@@ -1,5 +1,11 @@
# gadb
This module is initially forked from [electricbubble/gadb@v0.0.7].
This module is initially forked from [electricbubble/gadb@v0.0.7] and optimized by [@appl3s].
- feat: add reverse forward command
- feat: add `RunShellCommandV2` which supports running nohup
- feat: add `InstallAPK` with feature judgment
- feat: add `Uninstall` for specified package name
[electricbubble/gadb@v0.0.7]: https://github.com/electricbubble/gadb/tree/v0.0.7
[@appl3s]: https://github.com/appl3s

View File

@@ -38,6 +38,16 @@ func NewClientWith(host string, port ...int) (adbClient Client, err error) {
return
}
func NewClientWithoutTransport(host string, port ...int) (adbClient Client, err error) {
if len(port) == 0 {
port = []int{AdbServerPort}
}
adbClient.host = host
adbClient.port = port[0]
return
}
func (c Client) ServerVersion() (version int, err error) {
var resp string
if resp, err = c.executeCommand("host:version"); err != nil {
@@ -73,14 +83,14 @@ func (c Client) DeviceSerialList() (serials []string, err error) {
return
}
func (c Client) DeviceList() (devices []Device, err error) {
func (c Client) DeviceList() (devices []*Device, err error) {
var resp string
if resp, err = c.executeCommand("host:devices-l"); err != nil {
return
}
lines := strings.Split(resp, "\n")
devices = make([]Device, 0, len(lines))
devices = make([]*Device, 0, len(lines))
for i := range lines {
line := strings.TrimSpace(lines[i])
@@ -101,7 +111,7 @@ func (c Client) DeviceList() (devices []Device, err error) {
key, val := split[0], split[1]
mapAttrs[key] = val
}
devices = append(devices, Device{adbClient: c, serial: fields[0], attrs: mapAttrs})
devices = append(devices, &Device{adbClient: c, serial: fields[0], attrs: mapAttrs})
}
return

View File

@@ -3,6 +3,7 @@
package gadb
import (
"io/ioutil"
"testing"
)
@@ -127,3 +128,22 @@ func TestClient_KillServer(t *testing.T) {
t.Fatal(err)
}
}
func TestScreenCap(t *testing.T) {
adbClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
dl, err := adbClient.DeviceList()
if err != nil {
t.Error(err)
}
d := dl[0]
res, err := d.ScreenCap()
if err != nil {
t.Error(err)
}
t.Log(len(res))
ioutil.WriteFile("/tmp/1.png", res, 0o644)
}

View File

@@ -1,6 +1,8 @@
package gadb
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
@@ -48,9 +50,10 @@ func deviceStateConv(k string) (deviceState DeviceState) {
}
type DeviceForward struct {
Serial string
Local string
Remote string
Serial string
Local string
Remote string
Reverse bool
// LocalProtocol string
// RemoteProtocol string
}
@@ -59,51 +62,92 @@ type Device struct {
adbClient Client
serial string
attrs map[string]string
feat Features
}
func (d Device) Product() string {
func (d *Device) HasFeature(name Feature) bool {
feats, err := d.GetFeatures()
if err != nil || len(feats) == 0 {
return false
}
return feats.HasFeature(name)
}
func (d *Device) GetFeatures() (features Features, err error) {
if len(d.feat) > 0 {
return d.feat, nil
}
return d.features()
}
func (d *Device) features() (features Features, err error) {
res, err := d.executeCommand("host:features")
if err != nil {
return nil, err
}
if len(res) > 4 {
// stip hash
res = res[4:]
}
fs := strings.Split(string(res), ",")
features = make(Features, len(fs))
for _, f := range fs {
features[Feature(f)] = struct{}{}
}
d.feat = features
return features, nil
}
func (d *Device) Product() string {
return d.attrs["product"]
}
func (d Device) Model() string {
func (d *Device) Model() string {
return d.attrs["model"]
}
func (d Device) Usb() string {
func (d *Device) Usb() string {
return d.attrs["usb"]
}
func (d Device) transportId() string {
func (d *Device) transportId() string {
return d.attrs["transport_id"]
}
func (d Device) DeviceInfo() map[string]string {
func (d *Device) DeviceInfo() map[string]string {
return d.attrs
}
func (d Device) Serial() string {
func (d *Device) Serial() string {
// resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-serialno", d.serial))
return d.serial
}
func (d Device) IsUsb() bool {
func (d *Device) IsUsb() bool {
return d.Usb() != ""
}
func (d Device) State() (DeviceState, error) {
func (d *Device) State() (DeviceState, error) {
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-state", d.serial))
return deviceStateConv(resp), err
}
func (d Device) DevicePath() (string, error) {
func (d *Device) DevicePath() (string, error) {
resp, err := d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:get-devpath", d.serial))
return resp, err
}
func (d Device) Forward(localPort, remotePort int, noRebind ...bool) (err error) {
func (d *Device) Forward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
command := ""
var remote string
local := fmt.Sprintf("tcp:%d", localPort)
remote := fmt.Sprintf("tcp:%d", remotePort)
switch r := remoteInterface.(type) {
// for unix sockets
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
if len(noRebind) != 0 && noRebind[0] {
command = fmt.Sprintf("host-serial:%s:forward:norebind:%s;%s", d.serial, local, remote)
@@ -115,7 +159,7 @@ func (d Device) Forward(localPort, remotePort int, noRebind ...bool) (err error)
return
}
func (d Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
func (d *Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
var forwardList []DeviceForward
if forwardList, err = d.adbClient.ForwardList(); err != nil {
return nil, err
@@ -131,18 +175,80 @@ func (d Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
return
}
func (d Device) ForwardKill(localPort int) (err error) {
func (d *Device) ForwardKill(localPort int) (err error) {
local := fmt.Sprintf("tcp:%d", localPort)
_, err = d.adbClient.executeCommand(fmt.Sprintf("host-serial:%s:killforward:%s", d.serial, local), true)
return
}
func (d Device) RunShellCommand(cmd string, args ...string) (string, error) {
func (d *Device) ReverseForward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
var command string
var remote string
local := fmt.Sprintf("tcp:%d", localPort)
switch r := remoteInterface.(type) {
// for unix sockets
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
if len(noRebind) != 0 && noRebind[0] {
command = fmt.Sprintf("reverse:forward:norebind:%s;%s", remote, local)
} else {
command = fmt.Sprintf("reverse:forward:%s;%s", remote, local)
}
_, err = d.executeCommand(command, true)
return
}
func (d *Device) ReverseForwardList() (deviceForwardList []DeviceForward, err error) {
res, err := d.executeCommand("reverse:list-forward")
if err != nil {
return nil, err
}
resStr := string(res)
lines := strings.Split(resStr, "\n")
for _, line := range lines {
groups := strings.Split(line, " ")
if len(groups) == 3 {
deviceForwardList = append(deviceForwardList, DeviceForward{
Reverse: true,
Serial: d.serial,
Remote: groups[1],
Local: groups[2],
})
}
}
return
}
func (d *Device) ReverseForwardKill(remoteInterface interface{}) error {
remote := ""
switch r := remoteInterface.(type) {
case string:
remote = r
case int:
remote = fmt.Sprintf("tcp:%d", r)
}
_, err := d.executeCommand(fmt.Sprintf("reverse:killforward:%s", remote), true)
return err
}
func (d *Device) ReverseForwardKillAll() error {
_, err := d.executeCommand("reverse:killforward-all")
return err
}
func (d *Device) RunShellCommand(cmd string, args ...string) (string, error) {
raw, err := d.RunShellCommandWithBytes(cmd, args...)
return string(raw), err
}
func (d Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
func (d *Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, error) {
if d.HasFeature(FeatShellV2) {
return d.RunShellCommandV2WithBytes(cmd, args...)
}
if len(args) > 0 {
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
}
@@ -156,7 +262,86 @@ func (d Device) RunShellCommandWithBytes(cmd string, args ...string) ([]byte, er
return raw, err
}
func (d Device) EnableAdbOverTCP(port ...int) (err error) {
func (d *Device) RunShellCommandV2(cmd string, args ...string) (string, error) {
raw, err := d.RunShellCommandV2WithBytes(cmd, args...)
return string(raw), err
}
// RunShellCommandV2WithBytes shell v2, 支持后台运行而不会阻断
func (d *Device) RunShellCommandV2WithBytes(cmd string, args ...string) ([]byte, error) {
if len(args) > 0 {
cmd = fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))
}
if strings.TrimSpace(cmd) == "" {
return nil, errors.New("adb shell: command cannot be empty")
}
log.Debug().Str("cmd",
fmt.Sprintf("adb -s %s shell %s", d.serial, cmd)).
Msg("run adb command in v2")
raw, err := d.executeCommand(fmt.Sprintf("shell,v2,raw:%s", cmd))
if err != nil {
return raw, err
}
return d.parseV2CommandWithBytes(raw)
}
func (d *Device) parseV2CommandWithBytes(input []byte) (res []byte, err error) {
if len(input) == 0 {
return input, nil
}
reader := bytes.NewReader(input)
sizeBuf := make([]byte, 4)
var (
resBuf []byte
exitCode int
)
loop:
for {
msgCode, err := reader.ReadByte()
if err != nil {
return input, err
}
switch msgCode {
case 0x01, 0x02: // STDOUT, STDERR
_, err = io.ReadFull(reader, sizeBuf)
if err != nil {
return input, err
}
size := binary.LittleEndian.Uint32(sizeBuf)
if cap(resBuf) < int(size) {
resBuf = make([]byte, int(size))
}
_, err = io.ReadFull(reader, resBuf[:size])
if err != nil {
return input, err
}
res = append(res, resBuf[:size]...)
case 0x03: // EXIT
_, err = io.ReadFull(reader, sizeBuf)
if err != nil {
return input, err
}
size := binary.LittleEndian.Uint32(sizeBuf)
if cap(resBuf) < int(size) {
resBuf = make([]byte, int(size))
}
ec, err := reader.ReadByte()
if err != nil {
return input, err
}
exitCode = int(ec)
break loop
default:
return input, nil
}
}
if exitCode != 0 {
return nil, errors.New(string(res))
}
return res, nil
}
func (d *Device) EnableAdbOverTCP(port ...int) (err error) {
if len(port) == 0 {
port = []int{AdbDaemonPort}
}
@@ -168,7 +353,7 @@ func (d Device) EnableAdbOverTCP(port ...int) (err error) {
return
}
func (d Device) createDeviceTransport() (tp transport, err error) {
func (d *Device) createDeviceTransport() (tp transport, err error) {
if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port)); err != nil {
return transport{}, err
}
@@ -180,7 +365,7 @@ func (d Device) createDeviceTransport() (tp transport, err error) {
return
}
func (d Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw []byte, err error) {
func (d *Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw []byte, err error) {
if len(onlyVerifyResponse) == 0 {
onlyVerifyResponse = []bool{false}
}
@@ -207,7 +392,7 @@ func (d Device) executeCommand(command string, onlyVerifyResponse ...bool) (raw
return
}
func (d Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err error) {
func (d *Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return nil, err
@@ -237,7 +422,7 @@ func (d Device) List(remotePath string) (devFileInfos []DeviceFileInfo, err erro
return
}
func (d Device) PushFile(local *os.File, remotePath string, modification ...time.Time) (err error) {
func (d *Device) PushFile(local *os.File, remotePath string, modification ...time.Time) (err error) {
if len(modification) == 0 {
var stat os.FileInfo
if stat, err = local.Stat(); err != nil {
@@ -249,7 +434,7 @@ func (d Device) PushFile(local *os.File, remotePath string, modification ...time
return d.Push(local, remotePath, modification[0], DefaultFileMode)
}
func (d Device) Push(source io.Reader, remotePath string, modification time.Time, mode ...os.FileMode) (err error) {
func (d *Device) Push(source io.Reader, remotePath string, modification time.Time, mode ...os.FileMode) (err error) {
if len(mode) == 0 {
mode = []os.FileMode{DefaultFileMode}
}
@@ -285,7 +470,7 @@ func (d Device) Push(source io.Reader, remotePath string, modification time.Time
return
}
func (d Device) Pull(remotePath string, dest io.Writer) (err error) {
func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return err
@@ -305,3 +490,102 @@ func (d Device) Pull(remotePath string, dest io.Writer) (err error) {
err = sync.WriteStream(dest)
return
}
func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
var (
tp transport
filesize int64
)
filesize, err = apk.Seek(0, io.SeekEnd)
if err != nil {
return nil, err
}
if tp, err = d.createDeviceTransport(); err != nil {
return nil, err
}
defer func() { _ = tp.Close() }()
if err = tp.Send(fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)); err != nil {
return nil, err
}
if err = tp.VerifyResponse(); err != nil {
return nil, err
}
_, err = apk.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
_, err = io.Copy(tp.Conn(), apk)
if err != nil {
return nil, err
}
raw, err = tp.ReadBytesAll()
return
}
func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
haserr := func(ret string) bool {
return strings.Contains(ret, "Failure")
}
if d.HasFeature(FeatAbbExec) {
raw, err := d.installViaABBExec(apk)
if err != nil {
return "", fmt.Errorf("error installing: %v", err)
}
if haserr(string(raw)) {
return "", errors.New(string(raw))
}
return string(raw), err
}
remote := fmt.Sprintf("/data/local/tmp/gadb_remote_%d.apk", time.Now().Unix())
err := d.Push(apk, remote, time.Now())
if err != nil {
return "", fmt.Errorf("error pushing: %v", err)
}
res, err := d.RunShellCommand("pm", "install", "-f", remote)
if err != nil {
return "", fmt.Errorf("error installing: %v", err)
}
if haserr(res) {
return "", errors.New(res)
}
return res, nil
}
func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error) {
if len(keepData) == 0 {
keepData = []bool{false}
}
packageName = strings.ReplaceAll(packageName, " ", "")
if len(packageName) == 0 {
return "", fmt.Errorf("invalid package name")
}
args := []string{"uninstall"}
if keepData[0] {
args = append(args, "-k")
}
args = append(args, packageName)
return d.RunShellCommandV2("pm", args...)
}
func (d *Device) ScreenCap() ([]byte, error) {
if d.HasFeature(FeatShellV2) {
return d.RunShellCommandV2WithBytes("screencap", "-p")
}
// for shell v1, screenshot buffer maybe truncated
// thus we firstly save it to local file and then pull it
tempPath := fmt.Sprintf("/data/local/tmp/screenshot_%d.png",
time.Now().Unix())
_, err := d.RunShellCommandWithBytes("screencap", "-p", tempPath)
if err != nil {
return nil, err
}
buffer := bytes.NewBuffer(nil)
err = d.Pull(tempPath, buffer)
return buffer.Bytes(), err
}

View File

@@ -6,6 +6,7 @@ import (
"bytes"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
"time"
@@ -145,6 +146,42 @@ func TestDevice_Forward(t *testing.T) {
}
}
func TestDevice_ReverseForward(t *testing.T) {
adbClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := adbClient.DeviceList()
if err != nil {
t.Fatal(err)
}
localPort := 5005
err = devices[0].ReverseForward(localPort, "localabstract:scrcpy")
if err != nil {
t.Fatal(err)
}
err = devices[0].ReverseForward(localPort, "localabstract:scrcpy1")
if err != nil {
t.Fatal(err)
}
_, err = devices[0].ReverseForwardList()
if err != nil {
t.Fatal(err)
}
err = devices[0].ReverseForwardKill("localabstract:scrcpy1")
if err != nil {
t.Fatal(err)
}
err = devices[0].ReverseForwardKillAll()
if err != nil {
t.Fatal(err)
}
}
func TestDevice_ForwardList(t *testing.T) {
adbClient, err := NewClient()
if err != nil {
@@ -314,3 +351,94 @@ func TestDevice_Pull(t *testing.T) {
t.Fatal(err)
}
}
func TestDevice_RunShellCommandBackgroundWithBytes(t *testing.T) {
type fields struct {
adbClient Client
serial string
attrs map[string]string
}
type args struct {
cmd string
args []string
}
tests := []struct {
name string
fields fields
args args
want []byte
wantErr bool
}{
{
name: "runShellCommandBackground",
fields: fields{
adbClient: func() Client {
c, _ := NewClient()
return c
}(),
serial: "63c1ee94",
},
args: args{
cmd: "nohup sleep 10 2>/dev/null 1>/dev/null &",
// cmd: "sleep 10",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := Device{
adbClient: tt.fields.adbClient,
serial: tt.fields.serial,
attrs: tt.fields.attrs,
}
got, err := d.RunShellCommandV2WithBytes(tt.args.cmd, tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("Device.RunShellCommandBackgroundWithBytes() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Device.RunShellCommandBackgroundWithBytes() = %v, want %v", got, tt.want)
}
})
}
}
func TestDevice_InstallAPK(t *testing.T) {
apk, _ := os.Open("test.apk")
adbClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := adbClient.DeviceList()
if err != nil {
t.Fatal(err)
}
dev := devices[len(devices)-1]
dev = devices[0]
res, err := dev.InstallAPK(apk)
if err != nil {
t.Fatal(err)
}
t.Log(res)
}
func TestDevice_HasFeature(t *testing.T) {
adbClient, err := NewClient()
if err != nil {
t.Fatal(err)
}
devices, err := adbClient.DeviceList()
if err != nil {
t.Fatal(err)
}
dev := devices[len(devices)-1]
dev = devices[0]
t.Log(dev.GetFeatures())
}

26
hrp/pkg/gadb/features.go Normal file
View File

@@ -0,0 +1,26 @@
package gadb
type (
Feature string
Features map[Feature]struct{}
)
var (
FeatSendrecvV2Brotli = Feature("sendrecv_v2_brotli")
FeatRemountShell = Feature("remount_shell")
FeatSendrecvV2 = Feature("sendrecv_v2")
FeatAbbExec = Feature("abb_exec")
FeatFixedPushMkdir = Feature("fixed_push_mkdir")
FeatFixedPushSymlinkTimestamp = Feature("fixed_push_symlink_timestamp")
FeatAbb = Feature("abb")
FeatShellV2 = Feature("shell_v2")
FeatCmd = Feature("cmd")
FeatLsV2 = Feature("ls_v2")
FeatApex = Feature("apex")
FeatStatV2 = Feature("stat_v2")
)
func (fs Features) HasFeature(name Feature) bool {
_, has := fs[name]
return has
}

View File

@@ -250,5 +250,6 @@ func (sync syncTransport) Close() (err error) {
if sync.sock == nil {
return nil
}
_ = DisableTimeWait(sync.sock.(*net.TCPConn))
return sync.sock.Close()
}

View File

@@ -37,6 +37,14 @@ func (t transport) Send(command string) (err error) {
return _send(t.sock, []byte(msg))
}
func (t transport) SendBytes(b []byte) (err error) {
return _send(t.sock, b)
}
func (t transport) Conn() net.Conn {
return t.sock
}
func (t transport) VerifyResponse() (err error) {
var status string
if status, err = t.ReadStringN(4); err != nil {
@@ -103,6 +111,7 @@ func (t transport) Close() (err error) {
if t.sock == nil {
return nil
}
_ = DisableTimeWait(t.sock.(*net.TCPConn))
return t.sock.Close()
}

View File

@@ -13,7 +13,6 @@ func Test_transport_VerifyResponse(t *testing.T) {
}
defer transport.Close()
// err = transport.Send("host:123version")
err = transport.Send("host:version")
if err != nil {
t.Fatal(err)

9
hrp/pkg/gadb/utils.go Normal file
View File

@@ -0,0 +1,9 @@
package gadb
import (
"net"
)
func DisableTimeWait(conn *net.TCPConn) error {
return conn.SetLinger(0)
}

View File

@@ -17,7 +17,7 @@ import (
type adbDriver struct {
Driver
adbClient gadb.Device
adbClient *gadb.Device
logcat *AdbLogcat
}
@@ -153,11 +153,11 @@ func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error
return
}
func (ad *adbDriver) AppLaunch(bundleId string) (err error) {
func (ad *adbDriver) AppLaunch(packageName string) (err error) {
// 不指定 Activity 名称启动(启动主 Activity
// adb shell monkey -p <packagename> -c android.intent.category.LAUNCHER 1
sOutput, err := ad.adbClient.RunShellCommand(
"monkey", "-p", bundleId, "-c", "android.intent.category.LAUNCHER", "1",
"monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1",
)
if err != nil {
return err
@@ -165,14 +165,22 @@ func (ad *adbDriver) AppLaunch(bundleId string) (err error) {
if strings.Contains(sOutput, "monkey aborted") {
return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput))
}
ad.lastLaunchedPackageName = packageName
return nil
}
func (ad *adbDriver) AppTerminate(bundleId string) (successful bool, err error) {
func (ad *adbDriver) AppTerminate(packageName string) (successful bool, err error) {
// 强制停止应用,停止 <packagename> 相关的进程
// adb shell am force-stop <packagename>
_, err = ad.adbClient.RunShellCommand("am", "force-stop", bundleId)
return err == nil, err
_, err = ad.adbClient.RunShellCommand("am", "force-stop", packageName)
if err != nil {
return false, err
}
if ad.lastLaunchedPackageName == packageName {
ad.lastLaunchedPackageName = "" // reset last launched package name
}
return true, nil
}
func (ad *adbDriver) Tap(x, y int, options ...DataOption) error {
@@ -280,9 +288,7 @@ func (ad *adbDriver) SetRotation(rotation Rotation) (err error) {
func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
// adb shell screencap -p
resp, err := ad.adbClient.RunShellCommandWithBytes(
"screencap", "-p",
)
resp, err := ad.adbClient.ScreenCap()
if err == nil {
return bytes.NewBuffer(resp), nil
}
@@ -323,7 +329,7 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
log.Info().Msg("start adb log recording")
// clear logcat
if _, err = ad.adbClient.RunShellCommand("logcat", "--clear"); err != nil {
if _, err = ad.adbClient.RunShellCommand("logcat", "-c"); err != nil {
return err
}
@@ -349,3 +355,34 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
content := ad.logcat.logBuffer.String()
return ConvertPoints(content), nil
}
func (ad *adbDriver) GetLastLaunchedApp() (packageName string) {
return ad.lastLaunchedPackageName
}
func (ad *adbDriver) IsAppInForeground(packageName string) (bool, error) {
if packageName == "" {
return false, errors.New("package name is not given")
}
// adb shell dumpsys activity activities | grep mResumedActivity
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
if err != nil {
return false, err
}
lines := strings.Split(string(output), "\n")
isInForeground := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "mResumedActivity:") {
if strings.Contains(trimmedLine, packageName) {
isInForeground = true
}
break
}
}
return isInForeground, nil
}

View File

@@ -81,15 +81,6 @@ func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceO
// uiautomator2 server must be started before
// adb shell am instrument -w io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner
func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) {
deviceList, err := DeviceList()
if err != nil {
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
fmt.Sprintf("get attached devices failed: %v", err))
} else if len(deviceList) == 0 {
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
"not attached device found")
}
device = &AndroidDevice{
UIA2IP: UIA2ServerHost,
UIA2Port: UIA2ServerPort,
@@ -98,34 +89,56 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
option(device)
}
serialNumber := device.SerialNumber
for _, dev := range deviceList {
// find device by serial number if specified
if serialNumber != "" && dev.Serial() != serialNumber {
continue
}
device.SerialNumber = dev.Serial()
device.d = dev
device.logcat = NewAdbLogcat(device.SerialNumber)
return device, nil
deviceList, err := GetAndroidDevices(device.SerialNumber)
if err != nil {
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
}
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
fmt.Sprintf("device %s not found", device.SerialNumber))
dev := deviceList[0]
device.SerialNumber = dev.Serial()
device.d = dev
device.logcat = NewAdbLogcat(device.SerialNumber)
log.Info().Str("serial", device.SerialNumber).Msg("select android device")
return device, nil
}
func DeviceList() (devices []gadb.Device, err error) {
func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
var adbClient gadb.Client
if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil {
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
}
return adbClient.DeviceList()
if devices, err = adbClient.DeviceList(); err != nil {
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
fmt.Sprintf("list android devices failed: %v", 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 {
d gadb.Device
d *gadb.Device
logcat *AdbLogcat
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
@@ -315,7 +328,7 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
}
// clear logcat
if err = myexec.RunCommand("adb", "-s", l.serial, "logcat", "--clear"); err != nil {
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
return
}

View File

@@ -324,7 +324,7 @@ func Test_getFreePort(t *testing.T) {
}
func TestDeviceList(t *testing.T) {
devices, err := DeviceList()
devices, err := GetAndroidDevices()
if err != nil {
t.Fatal(err)
}
@@ -353,6 +353,34 @@ func TestDriver_AppLaunch(t *testing.T) {
t.Log(ioutil.WriteFile("s1.png", raw.Bytes(), 0o600))
}
func TestDriver_IsAppInForeground(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)
if err != nil {
t.Fatal(err)
}
err = driver.Driver.AppLaunch("com.android.settings")
if err != nil {
t.Fatal(err)
}
yes, err := driver.Driver.IsAppInForeground(driver.Driver.GetLastLaunchedApp())
if err != nil || !yes {
t.Fatal(err)
}
_, err = driver.Driver.AppTerminate("com.android.settings")
if err != nil {
t.Fatal(err)
}
yes, err = driver.Driver.IsAppInForeground("com.android.settings")
if err != nil || yes {
t.Fatal(err)
}
}
func TestDriver_KeepAlive(t *testing.T) {
device, _ := NewAndroidDevice()
driver, err := device.NewDriver(nil)

View File

@@ -20,6 +20,8 @@ type Driver struct {
urlPrefix *url.URL
sessionId string
client *http.Client
// cache the last launched package name
lastLaunchedPackageName string
}
func (wd *Driver) concatURL(u *url.URL, elem ...string) string {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"math/rand"
@@ -284,6 +285,8 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
err = png.Encode(file, img)
case "jpeg":
err = jpeg.Encode(file, img, nil)
case "gif":
err = gif.Encode(file, img, nil)
default:
return "", fmt.Errorf("unsupported image format: %s", format)
}
@@ -348,6 +351,19 @@ func (dExt *DriverExt) IsImageExist(text string) bool {
var errActionNotImplemented = errors.New("UI action not implemented")
func convertToFloat64(val interface{}) (float64, error) {
switch v := val.(type) {
case float64:
return v, nil
case int:
return float64(v), nil
case int64:
return float64(v), nil
default:
return 0, fmt.Errorf("invalid type for conversion to float64: %T, value: %+v", val, val)
}
}
func (dExt *DriverExt) DoAction(action MobileAction) error {
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
@@ -609,24 +625,54 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
}
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case CtlSleepRandom:
if params, ok := action.Params.([]interface{}); ok && len(params) == 2 {
var a, b float64
if v, ok := params[0].(float64); ok {
a = v
} else if v, ok := params[0].(int64); ok {
a = float64(v)
params, ok := action.Params.([]interface{})
if !ok {
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
}
// append default weight 1
if len(params) == 2 {
params = append(params, 1.0)
}
var sections []struct {
min, max, weight float64
}
totalProb := 0.0
for i := 0; i+3 <= len(params); i += 3 {
min, err := convertToFloat64(params[i])
if err != nil {
return errors.Wrapf(err, "invalid minimum time: %v", params[i])
}
if v, ok := params[1].(float64); ok {
b = v
} else if v, ok := params[1].(int64); ok {
b = float64(v)
max, err := convertToFloat64(params[i+1])
if err != nil {
return errors.Wrapf(err, "invalid maximum time: %v", params[i+1])
}
n := a + rand.Float64()*(b-a)
log.Info().Float64("duration", n).Msg("sleep random seconds")
time.Sleep(time.Duration(n*1000) * time.Millisecond)
weight, err := convertToFloat64(params[i+2])
if err != nil {
return errors.Wrapf(err, "invalid weight value: %v", params[i+2])
}
totalProb += weight
sections = append(sections,
struct{ min, max, weight float64 }{min, max, weight},
)
}
if totalProb == 0 {
log.Warn().Msg("total weight is 0, skip sleep")
return nil
}
return fmt.Errorf("invalid sleep random params: %v(%T)", action.Params, action.Params)
r := rand.Float64()
accProb := 0.0
for _, s := range sections {
accProb += s.weight / totalProb
if r < accProb {
n := s.min + rand.Float64()*(s.max-s.min)
log.Info().Float64("duration", n).Msg("sleep random seconds")
time.Sleep(time.Duration(n*1000) * time.Millisecond)
return nil
}
}
case CtlScreenShot:
// take snapshot
log.Info().Msg("take snapshot for current screen")

View File

@@ -627,10 +627,14 @@ type WebDriver interface {
// AppLaunch Launch an application with given bundle identifier in scope of current session.
// !This method is only available since Xcode9 SDK
AppLaunch(bundleId string) error
// AppTerminate Terminate an application with the given bundle id.
AppLaunch(packageName string) error
// AppTerminate Terminate an application with the given pacakge name.
// Either `true` if the app has been successfully terminated or `false` if it was not running
AppTerminate(bundleId string) (bool, error)
AppTerminate(packageName string) (bool, error)
// GetLastLaunchedApp returns the package name of the last launched app
GetLastLaunchedApp() string
// IsAppInForeground returns true if the given package is in foreground
IsAppInForeground(packageName string) (bool, error)
// StartCamera Starts a new camera for recording
StartCamera() error

View File

@@ -141,7 +141,7 @@ func WithIOSPcapOptions(options ...gidevice.PcapOption) IOSDeviceOption {
}
}
func IOSDevices(udid ...string) (devices []gidevice.Device, err error) {
func GetIOSDevices(udid ...string) (devices []gidevice.Device, err error) {
var usbmux gidevice.Usbmux
if usbmux, err = gidevice.NewUsbmux(); err != nil {
return nil, errors.Wrap(code.IOSDeviceConnectionError,
@@ -168,6 +168,15 @@ func IOSDevices(udid ...string) (devices []gidevice.Device, err error) {
}
}
if len(deviceList) == 0 {
var err error
if udid == nil || (len(udid) == 1 && udid[0] == "") {
err = fmt.Errorf("no ios device found")
} else {
err = fmt.Errorf("no ios device found for udid %v", udid)
}
return nil, err
}
return deviceList, nil
}
@@ -223,31 +232,27 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
option(device)
}
deviceList, err := IOSDevices(device.UDID)
deviceList, err := GetIOSDevices(device.UDID)
if err != nil {
return nil, err
return nil, errors.Wrap(code.IOSDeviceConnectionError, err.Error())
}
for _, dev := range deviceList {
udid := dev.Properties().SerialNumber
device.UDID = udid
device.d = dev
dev := deviceList[0]
udid := dev.Properties().SerialNumber
device.UDID = udid
device.d = dev
// run xctest if XCTestBundleID is set
if device.XCTestBundleID != "" {
_, err = device.RunXCTest(device.XCTestBundleID)
if err != nil {
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
continue
}
// run xctest if XCTestBundleID is set
if device.XCTestBundleID != "" {
_, err = device.RunXCTest(device.XCTestBundleID)
if err != nil {
log.Error().Err(err).Str("udid", udid).Msg("failed to init XCTest")
return
}
log.Info().Str("udid", device.UDID).Msg("select device")
return device, nil
}
return nil, errors.Wrap(code.IOSDeviceConnectionError,
fmt.Sprintf("device %s not found", device.UDID))
log.Info().Str("udid", device.UDID).Msg("select ios device")
return device, nil
}
type IOSDevice struct {

View File

@@ -308,6 +308,9 @@ func (wd *wdaDriver) AppLaunch(bundleId string) (err error) {
data := make(map[string]interface{})
data["bundleId"] = bundleId
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch")
if err == nil {
wd.lastLaunchedPackageName = bundleId
}
return
}
@@ -328,6 +331,9 @@ func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error)
if successful, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
if wd.lastLaunchedPackageName == bundleId {
wd.lastLaunchedPackageName = "" // reset last launched package name
}
return
}
@@ -348,6 +354,14 @@ func (wd *wdaDriver) AppDeactivate(second float64) (err error) {
return
}
func (wd *wdaDriver) GetLastLaunchedApp() (packageName string) {
return wd.lastLaunchedPackageName
}
func (wd *wdaDriver) IsAppInForeground(packageName string) (bool, error) {
return false, errors.New("not implemented")
}
func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error {
return wd.TapFloat(float64(x), float64(y), options...)
}

View File

@@ -93,16 +93,19 @@ func (s *veDEMOCRService) getOCRResult(imageBuf *bytes.Buffer) ([]OCRResult, err
// retry 3 times
for i := 1; i <= 3; i++ {
resp, err = client.Do(req)
if err == nil {
break
}
var logID string
if resp != nil {
logID = getLogID(resp.Header)
}
if err == nil && resp.StatusCode == http.StatusOK {
log.Debug().
Str("X-TT-LOGID", logID).
Int("imageBufSize", size).
Msg("request OCR service success")
break
}
log.Error().Err(err).
Str("logID", logID).
Str("X-TT-LOGID", logID).
Int("imageBufSize", size).
Msgf("request OCR service failed, retry %d", i)
time.Sleep(1 * time.Second)

View File

@@ -281,10 +281,14 @@ func (s *StepMobile) Sleep(n float64) *StepMobile {
return &StepMobile{step: s.step}
}
func (s *StepMobile) SleepRandom(a, b float64) *StepMobile {
// SleepRandom specify random sleeping seconds after last action
// params have two different kinds:
// 1. [min, max] : min and max are float64 time range boudaries
// 2. [min1, max1, weight1, min2, max2, weight2, ...] : weight is the probability of the time range
func (s *StepMobile) SleepRandom(params ...float64) *StepMobile {
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
Method: uixt.CtlSleepRandom,
Params: []float64{a, b},
Params: params,
})
return &StepMobile{step: s.step}
}
@@ -557,6 +561,24 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
attachments := make(map[string]interface{})
if err != nil {
attachments["error"] = err.Error()
// check if app is in foreground
packageName := uiDriver.Driver.GetLastLaunchedApp()
yes, err2 := uiDriver.Driver.IsAppInForeground(packageName)
if packageName != "" && (!yes || err2 != nil) {
log.Error().Err(err2).Str("packageName", packageName).Msg("app is not in foreground")
err = errors.Wrap(code.MobileUIAppNotInForegroundError, err.Error())
}
}
// take screenshot after each step
screenshotPath, err := uiDriver.ScreenShot(
fmt.Sprintf("step_%d", time.Now().Unix()))
if err != nil {
log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed")
} else {
log.Info().Str("path", screenshotPath).Msg("take screenshot on step finished")
screenshots = append(screenshots, screenshotPath)
}
// save attachments
@@ -595,16 +617,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
}
}
// take snapshot
screenshotPath, err := uiDriver.ScreenShot(
fmt.Sprintf("validate_%d", time.Now().Unix()))
if err != nil {
log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed")
} else {
log.Info().Str("path", screenshotPath).Msg("take screenshot before validation")
screenshots = append(screenshots, screenshotPath)
}
// validate
validateResults, err := validateUI(uiDriver, step.Validators)
if err != nil {