mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-09 14:32:31 +08:00
feat: 新增按控件点击,获取设备应用,修改日志获取
This commit is contained in:
@@ -483,3 +483,10 @@ func ConvertToStringSlice(val interface{}) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("invalid type for conversion to []string")
|
return nil, fmt.Errorf("invalid type for conversion to []string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCurrentDay() string {
|
||||||
|
now := time.Now()
|
||||||
|
// 格式化日期为 yyyyMMdd
|
||||||
|
formattedDate := now.Format("20060102")
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,25 +107,38 @@ func (d *Device) features() (features Features, err error) {
|
|||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) HasAttribute(key string) bool {
|
func (d *Device) HasAttribute(key string) bool {
|
||||||
_, ok := d.attrs[key]
|
_, ok := d.attrs[key]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) Product() (string, error) {
|
func (d *Device) Product() (string, error) {
|
||||||
if d.HasAttribute("product") {
|
if d.HasAttribute("product") {
|
||||||
return d.attrs["product"], nil
|
return d.attrs["product"], nil
|
||||||
}
|
}
|
||||||
return "", errors.New("does not have attribute: product")
|
return "", errors.New("does not have attribute: product")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) Model() (string, error) {
|
func (d *Device) Model() (string, error) {
|
||||||
if d.HasAttribute("model") {
|
if d.HasAttribute("model") {
|
||||||
return d.attrs["model"], nil
|
return d.attrs["model"], nil
|
||||||
}
|
}
|
||||||
return "", errors.New("does not have attribute: model")
|
return "", errors.New("does not have attribute: model")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Device) Brand() (string, error) {
|
||||||
|
if d.HasAttribute("brand") {
|
||||||
|
return d.attrs["brand"], nil
|
||||||
|
}
|
||||||
|
brand, err := d.RunShellCommand("getprop", "ro.product.brand")
|
||||||
|
brand = strings.TrimSpace(brand)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("does not have attribute: brand")
|
||||||
|
}
|
||||||
|
d.attrs["brand"] = brand
|
||||||
|
return brand, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Device) Usb() (string, error) {
|
func (d *Device) Usb() (string, error) {
|
||||||
if d.HasAttribute("usb") {
|
if d.HasAttribute("usb") {
|
||||||
return d.attrs["usb"], nil
|
return d.attrs["usb"], nil
|
||||||
@@ -133,7 +146,7 @@ func (d *Device) Usb() (string, error) {
|
|||||||
return "", errors.New("does not have attribute: usb")
|
return "", errors.New("does not have attribute: usb")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) transportId() (string, error) {
|
func (d *Device) transportId() (string, error) {
|
||||||
if d.HasAttribute("transport_id") {
|
if d.HasAttribute("transport_id") {
|
||||||
return d.attrs["transport_id"], nil
|
return d.attrs["transport_id"], nil
|
||||||
}
|
}
|
||||||
@@ -524,7 +537,7 @@ func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byte, err error) {
|
||||||
var (
|
var (
|
||||||
tp transport
|
tp transport
|
||||||
filesize int64
|
filesize int64
|
||||||
@@ -537,8 +550,11 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() { _ = tp.Close() }()
|
defer func() { _ = tp.Close() }()
|
||||||
|
cmd := "abb_exec:package\x00install\x00-t"
|
||||||
cmd := fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)
|
for _, arg := range args {
|
||||||
|
cmd += "\x00" + arg
|
||||||
|
}
|
||||||
|
cmd += fmt.Sprintf("\x00-S\x00%d", filesize)
|
||||||
if err = tp.SendWithCheck(cmd); err != nil {
|
if err = tp.SendWithCheck(cmd); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -555,7 +571,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
func (d *Device) InstallAPK(apk io.ReadSeeker, args ...string) (string, error) {
|
||||||
haserr := func(ret string) bool {
|
haserr := func(ret string) bool {
|
||||||
return strings.Contains(ret, "Failure")
|
return strings.Contains(ret, "Failure")
|
||||||
}
|
}
|
||||||
@@ -575,8 +591,9 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error pushing: %v", err)
|
return "", fmt.Errorf("error pushing: %v", err)
|
||||||
}
|
}
|
||||||
|
args = append([]string{"install"}, args...)
|
||||||
res, err := d.RunShellCommand("pm", "install", "-f", remote)
|
args = append(args, "-f", remote)
|
||||||
|
res, err := d.RunShellCommand("pm", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "install apk failed")
|
return "", errors.Wrap(err, "install apk failed")
|
||||||
}
|
}
|
||||||
@@ -603,6 +620,33 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
|
|||||||
return d.RunShellCommand("pm", args...)
|
return d.RunShellCommand("pm", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Device) ListPackages() ([]string, error) {
|
||||||
|
args := []string{"list", "packages"}
|
||||||
|
resRaw, err := d.RunShellCommand("pm", args...)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(resRaw, "\n")
|
||||||
|
var packages []string
|
||||||
|
for _, line := range lines {
|
||||||
|
packageName := strings.TrimPrefix(line, "package:")
|
||||||
|
packages = append(packages, packageName)
|
||||||
|
}
|
||||||
|
return packages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) IsPackagesInstalled(packageName string) bool {
|
||||||
|
packages, err := d.ListPackages()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
packageName = strings.ReplaceAll(packageName, " ", "")
|
||||||
|
if len(packageName) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return builtin.Contains(packages, packageName)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Device) ScreenCap() ([]byte, error) {
|
func (d *Device) ScreenCap() ([]byte, error) {
|
||||||
if d.HasFeature(FeatShellV2) {
|
if d.HasFeature(FeatShellV2) {
|
||||||
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ func TestDevice_Product(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
product := dev.Product()
|
product, err := dev.Product()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
t.Log(dev.Serial(), product)
|
t.Log(dev.Serial(), product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,24 @@ func TestDevice_Model(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
t.Log(dev.Serial(), dev.Model())
|
model, err := dev.Model()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevice_Brand(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
|
||||||
|
for i := range devices {
|
||||||
|
dev := devices[i]
|
||||||
|
brand, err := dev.Brand()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), brand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +99,15 @@ func TestDevice_Usb(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
t.Log(dev.Serial(), dev.Usb(), dev.IsUsb())
|
usb, err := dev.Usb()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
isUsb, err := dev.IsUsb()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), usb, isUsb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +343,22 @@ func TestDevice_InstallAPK(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevice_ListPackages(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
for _, dev := range devices {
|
||||||
|
res, err := dev.ListPackages()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(res)
|
||||||
|
installed, err := dev.IsPackagesInstalled("io.appium.uiautomator2.server")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(installed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDevice_HasFeature(t *testing.T) {
|
func TestDevice_HasFeature(t *testing.T) {
|
||||||
setupDevices(t)
|
setupDevices(t)
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,11 @@ func NewActionOptions(options ...ActionOption) *ActionOptions {
|
|||||||
return actionOptions
|
return actionOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TapTextAction struct {
|
||||||
|
Text string
|
||||||
|
Options []ActionOption
|
||||||
|
}
|
||||||
|
|
||||||
type ActionOption func(o *ActionOptions)
|
type ActionOption func(o *ActionOptions)
|
||||||
|
|
||||||
func WithCustomOption(key string, value interface{}) ActionOption {
|
func WithCustomOption(key string, value interface{}) ActionOption {
|
||||||
|
|||||||
@@ -2,27 +2,30 @@ package uixt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin/myexec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/myexec"
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
const (
|
||||||
const UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
|
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
||||||
|
UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
|
||||||
|
)
|
||||||
|
|
||||||
type adbDriver struct {
|
type adbDriver struct {
|
||||||
Driver
|
Driver
|
||||||
@@ -31,6 +34,56 @@ type adbDriver struct {
|
|||||||
logcat *AdbLogcat
|
logcat *AdbLogcat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Hierarchy struct {
|
||||||
|
XMLName xml.Name `xml:"hierarchy"`
|
||||||
|
Nodes []Node `xml:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Index string `xml:"index,attr"`
|
||||||
|
Text string `xml:"text,attr"`
|
||||||
|
ResourceID string `xml:"resource-id,attr"`
|
||||||
|
Class string `xml:"class,attr"`
|
||||||
|
Package string `xml:"package,attr"`
|
||||||
|
ContentDesc string `xml:"content-desc,attr"`
|
||||||
|
Checkable string `xml:"checkable,attr"`
|
||||||
|
Checked string `xml:"checked,attr"`
|
||||||
|
Clickable string `xml:"clickable,attr"`
|
||||||
|
Enabled string `xml:"enabled,attr"`
|
||||||
|
Focusable string `xml:"focusable,attr"`
|
||||||
|
Focused string `xml:"focused,attr"`
|
||||||
|
Scrollable string `xml:"scrollable,attr"`
|
||||||
|
LongClickable string `xml:"long-clickable,attr"`
|
||||||
|
Password string `xml:"password,attr"`
|
||||||
|
Selected string `xml:"selected,attr"`
|
||||||
|
Bounds *Bounds `xml:"bounds,attr"`
|
||||||
|
Children []Node `xml:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bounds struct {
|
||||||
|
X1, Y1, X2, Y2 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bounds) UnmarshalXMLAttr(attr xml.Attr) error {
|
||||||
|
// 正则表达式用于解析格式为"[x1,y1][x2,y2]"
|
||||||
|
re := regexp.MustCompile(`\[(\d+),(\d+)]\[(\d+),(\d+)]`)
|
||||||
|
matches := re.FindStringSubmatch(attr.Value)
|
||||||
|
if matches == nil {
|
||||||
|
return fmt.Errorf("bounds format is incorrect")
|
||||||
|
}
|
||||||
|
// 转换字符串为整数
|
||||||
|
b.X1, _ = strconv.Atoi(matches[1])
|
||||||
|
b.Y1, _ = strconv.Atoi(matches[2])
|
||||||
|
b.X2, _ = strconv.Atoi(matches[3])
|
||||||
|
b.Y2, _ = strconv.Atoi(matches[4])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center 方法计算并返回 Bounds 中心点的坐标
|
||||||
|
func (b *Bounds) Center() (float64, float64) {
|
||||||
|
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
|
||||||
|
}
|
||||||
|
|
||||||
func NewAdbDriver() *adbDriver {
|
func NewAdbDriver() *adbDriver {
|
||||||
log.Info().Msg("init adb driver")
|
log.Info().Msg("init adb driver")
|
||||||
return &adbDriver{}
|
return &adbDriver{}
|
||||||
@@ -479,10 +532,95 @@ func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||||
err = errDriverNotImplemented
|
_, err = ad.adbClient.RunShellCommand("uiautomator", "dump")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source, err = ad.adbClient.RunShellCommand("cat", "/sdcard/window_dump.xml")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||||
|
source, err := ad.Source()
|
||||||
|
sourceTree = new(Hierarchy)
|
||||||
|
err = xml.Unmarshal([]byte(source), sourceTree)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
sourceTree, err := ad.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ad.tapByTextUsingHierarchy(sourceTree, text, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) tapByTextUsingHierarchy(hierarchy *Hierarchy, text string, options ...ActionOption) error {
|
||||||
|
bounds := ad.searchNodes(hierarchy.Nodes, text, options...)
|
||||||
|
actionOptions := NewActionOptions(options...)
|
||||||
|
if len(bounds) == 0 {
|
||||||
|
if actionOptions.IgnoreNotFoundError {
|
||||||
|
log.Info().Msg("not found element by text " + text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not found element by text " + text)
|
||||||
|
}
|
||||||
|
for _, bound := range bounds {
|
||||||
|
width, height := bound.Center()
|
||||||
|
err := ad.TapFloat(width, height, options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
sourceTree, err := ad.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
err := ad.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) searchNodes(nodes []Node, text string, options ...ActionOption) []Bounds {
|
||||||
|
actionOptions := NewActionOptions(options...)
|
||||||
|
var results []Bounds
|
||||||
|
for _, node := range nodes {
|
||||||
|
result := ad.searchNodes(node.Children, text, options...)
|
||||||
|
results = append(results, result...)
|
||||||
|
if actionOptions.Regex {
|
||||||
|
// regex on, check if match regex
|
||||||
|
if !regexp.MustCompile(text).MatchString(node.Text) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// regex off, check if match exactly
|
||||||
|
if node.Text != text {
|
||||||
|
ad.searchNodes(node.Children, text, options...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Bounds != nil {
|
||||||
|
results = append(results, *node.Bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) AccessibleSource() (source string, err error) {
|
func (ad *adbDriver) AccessibleSource() (source string, err error) {
|
||||||
err = errDriverNotImplemented
|
err = errDriverNotImplemented
|
||||||
return
|
return
|
||||||
@@ -517,7 +655,7 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start logcat
|
// start logcat
|
||||||
err = ad.logcat.CatchLogcat()
|
err = ad.logcat.CatchLogcat("iesqaMonitor:V")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
err = errors.Wrap(code.AndroidCaptureLogError,
|
||||||
fmt.Sprintf("start adb log recording failed: %v", err))
|
fmt.Sprintf("start adb log recording failed: %v", err))
|
||||||
@@ -527,17 +665,15 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||||
log.Info().Msg("stop adb log recording")
|
defer func() {
|
||||||
err = ad.logcat.Stop()
|
log.Info().Msg("stop adb log recording")
|
||||||
if err != nil {
|
err = ad.logcat.Stop()
|
||||||
log.Error().Err(err).Msg("failed to get adb log recording")
|
if err != nil {
|
||||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
log.Error().Err(err).Msg("failed to get adb log recording")
|
||||||
fmt.Sprintf("get adb log recording failed: %v", err))
|
}
|
||||||
return "", err
|
}()
|
||||||
}
|
pointRes := ConvertPoints(ad.logcat.reader)
|
||||||
content := ad.logcat.logBuffer.String()
|
|
||||||
log.Info().Str("logcat content", content).Msg("display logcat content")
|
|
||||||
pointRes := ConvertPoints(content)
|
|
||||||
// 没有解析到打点日志,走兜底逻辑
|
// 没有解析到打点日志,走兜底逻辑
|
||||||
if len(pointRes) == 0 {
|
if len(pointRes) == 0 {
|
||||||
log.Info().Msg("action log is null, use action file >>>")
|
log.Info().Msg("action log is null, use action file >>>")
|
||||||
@@ -551,7 +687,6 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// 先保持原有状态码不变,这里不return error
|
// 先保持原有状态码不变,这里不return error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("read log file fail")
|
log.Error().Err(err).Msg("read log file fail")
|
||||||
@@ -563,13 +698,13 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
|||||||
return pointRes, nil
|
return pointRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(files[0])
|
reader, err := os.Open(files[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info().Msg("read File error")
|
log.Info().Msg("open File error")
|
||||||
return pointRes, nil
|
return pointRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pointRes = ConvertPoints(string(data))
|
pointRes = ConvertPoints(reader)
|
||||||
}
|
}
|
||||||
return pointRes, nil
|
return pointRes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin/myexec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin/myexec"
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||||
@@ -23,7 +24,6 @@ var (
|
|||||||
AdbServerPort = gadb.AdbServerPort // 5037
|
AdbServerPort = gadb.AdbServerPort // 5037
|
||||||
UIA2ServerHost = "localhost"
|
UIA2ServerHost = "localhost"
|
||||||
UIA2ServerPort = 6790
|
UIA2ServerPort = 6790
|
||||||
DeviceTempPath = "/data/local/tmp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const forwardToPrefix = "forward-to-"
|
const forwardToPrefix = "forward-to-"
|
||||||
@@ -148,18 +148,6 @@ func GetAndroidDevices(serial ...string) (devices []*gadb.Device, err error) {
|
|||||||
return deviceList, nil
|
return deviceList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeUnicode(c int32) string {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
// Convert each rune (character) into two bytes
|
|
||||||
buffer.WriteByte(byte(c >> 8))
|
|
||||||
buffer.WriteByte(byte(c & 0xFF))
|
|
||||||
// Convert buffer bytes to base64 encoding
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(buffer.Bytes())
|
|
||||||
// Replace "/" with "," and remove trailing "="
|
|
||||||
encoded = strings.ReplaceAll(encoded, "/", ",")
|
|
||||||
return strings.TrimRight(encoded, "=")
|
|
||||||
}
|
|
||||||
|
|
||||||
type AndroidDevice struct {
|
type AndroidDevice struct {
|
||||||
d *gadb.Device
|
d *gadb.Device
|
||||||
logcat *AdbLogcat
|
logcat *AdbLogcat
|
||||||
@@ -290,26 +278,27 @@ func getFreePort() (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AdbLogcat struct {
|
type AdbLogcat struct {
|
||||||
serial string
|
serial string
|
||||||
logBuffer *bytes.Buffer
|
// logBuffer *bytes.Buffer
|
||||||
errs []error
|
errs []error
|
||||||
stopping chan struct{}
|
stopping chan struct{}
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
|
reader io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdbLogcat(serial string) *AdbLogcat {
|
func NewAdbLogcat(serial string) *AdbLogcat {
|
||||||
return &AdbLogcat{
|
return &AdbLogcat{
|
||||||
serial: serial,
|
serial: serial,
|
||||||
logBuffer: new(bytes.Buffer),
|
// logBuffer: new(bytes.Buffer),
|
||||||
stopping: make(chan struct{}),
|
stopping: make(chan struct{}),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatchLogcatContext starts logcat with timeout context
|
// CatchLogcatContext starts logcat with timeout context
|
||||||
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
|
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
|
||||||
if err = l.CatchLogcat(); err != nil {
|
if err = l.CatchLogcat(""); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -344,7 +333,7 @@ func (l *AdbLogcat) Errors() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AdbLogcat) CatchLogcat() (err error) {
|
func (l *AdbLogcat) CatchLogcat(filter string) (err error) {
|
||||||
if l.cmd != nil {
|
if l.cmd != nil {
|
||||||
log.Warn().Msg("logcat already start")
|
log.Warn().Msg("logcat already start")
|
||||||
return nil
|
return nil
|
||||||
@@ -354,12 +343,19 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
|||||||
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
args := []string{"-s", l.serial, "logcat", "--format", "time"}
|
||||||
|
if filter != "" {
|
||||||
|
args = append(args, "-s", filter)
|
||||||
|
}
|
||||||
// start logcat
|
// start logcat
|
||||||
l.cmd = myexec.Command("adb", "-s", l.serial,
|
l.cmd = myexec.Command("adb", args...)
|
||||||
"logcat", "--format", "time", "-s", "iesqaMonitor:V")
|
// l.cmd.Stderr = l.logBuffer
|
||||||
l.cmd.Stderr = l.logBuffer
|
// l.cmd.Stdout = l.logBuffer
|
||||||
l.cmd.Stdout = l.logBuffer
|
reader, err := l.cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.reader = reader
|
||||||
if err = l.cmd.Start(); err != nil {
|
if err = l.cmd.Start(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -370,17 +366,7 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
|||||||
}
|
}
|
||||||
l.done <- struct{}{}
|
l.done <- struct{}{}
|
||||||
}()
|
}()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AdbLogcat) BufferedLogcat() (err error) {
|
|
||||||
// -d: dump the current buffered logcat result and exits
|
|
||||||
cmd := myexec.Command("adb", "-s", l.serial, "logcat", "-d")
|
|
||||||
cmd.Stdout = l.logBuffer
|
|
||||||
cmd.Stderr = l.logBuffer
|
|
||||||
if err = cmd.Run(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +380,11 @@ type ExportPoint struct {
|
|||||||
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
|
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertPoints(data string) (eps []ExportPoint) {
|
func ConvertPoints(reader io.Reader) (eps []ExportPoint) {
|
||||||
lines := strings.Split(data, "\n")
|
scanner := bufio.NewScanner(reader)
|
||||||
for _, line := range lines {
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
log.Info().Str("logcat content", line)
|
||||||
if strings.Contains(line, "ext") {
|
if strings.Contains(line, "ext") {
|
||||||
idx := strings.Index(line, "{")
|
idx := strings.Index(line, "{")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -427,15 +425,15 @@ func TestDriver_AppTerminate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertPoints(t *testing.T) {
|
//func TestConvertPoints(t *testing.T) {
|
||||||
data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}"
|
// data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}"
|
||||||
eps := ConvertPoints(data)
|
// eps := ConvertPoints(data)
|
||||||
if len(eps) != 3 {
|
// if len(eps) != 3 {
|
||||||
t.Fatal()
|
// t.Fatal()
|
||||||
}
|
// }
|
||||||
jsons, _ := json.Marshal(eps)
|
// jsons, _ := json.Marshal(eps)
|
||||||
println(fmt.Sprintf("%v", string(jsons)))
|
// println(fmt.Sprintf("%v", string(jsons)))
|
||||||
}
|
//}
|
||||||
|
|
||||||
func TestDriver_ShellInputUnicode(t *testing.T) {
|
func TestDriver_ShellInputUnicode(t *testing.T) {
|
||||||
device, _ := NewAndroidDevice()
|
device, _ := NewAndroidDevice()
|
||||||
@@ -456,3 +454,16 @@ func TestDriver_ShellInputUnicode(t *testing.T) {
|
|||||||
|
|
||||||
t.Log(os.WriteFile("s1.png", raw.Bytes(), 0o600))
|
t.Log(os.WriteFile("s1.png", raw.Bytes(), 0o600))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTapTexts(t *testing.T) {
|
||||||
|
setupAndroid(t)
|
||||||
|
actions := []TapTextAction{
|
||||||
|
{Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
{Text: "已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
{Text: "^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", Options: []ActionOption{WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
}
|
||||||
|
err := driverExt.Driver.TapByTexts(actions...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -598,3 +598,11 @@ func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
|||||||
source = reply.Value
|
source = reply.Value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
return ud.adbDriver.TapByText(text, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
return ud.adbDriver.TapByTexts(actions...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -589,6 +589,11 @@ type WebDriver interface {
|
|||||||
|
|
||||||
// Source Return application elements tree
|
// Source Return application elements tree
|
||||||
Source(srcOpt ...SourceOption) (string, error)
|
Source(srcOpt ...SourceOption) (string, error)
|
||||||
|
|
||||||
|
TapByText(text string, options ...ActionOption) error
|
||||||
|
|
||||||
|
TapByTexts(actions ...TapTextAction) error
|
||||||
|
|
||||||
// AccessibleSource Return application elements accessibility tree
|
// AccessibleSource Return application elements accessibility tree
|
||||||
AccessibleSource() (string, error)
|
AccessibleSource() (string, error)
|
||||||
|
|
||||||
|
|||||||
@@ -759,6 +759,14 @@ func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wd *wdaDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
return errDriverNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wd *wdaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
return errDriverNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (wd *wdaDriver) AccessibleSource() (source string, err error) {
|
func (wd *wdaDriver) AccessibleSource() (source string, err error) {
|
||||||
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
|
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
|
||||||
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
||||||
|
|||||||
Reference in New Issue
Block a user