mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
Merge branch 'feat/yuhongzheng/pre_auto_install' into 'video-release'
Feat/yuhongzheng/pre auto install See merge request iesqa/httprunner!34
This commit is contained in:
14
go.mod
14
go.mod
@@ -26,8 +26,9 @@ require (
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
gocv.io/x/gocv v0.32.1
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/oauth2 v0.8.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.57.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -47,10 +48,12 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.10 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/incu6us/goimports-reviser/v2 v2.5.3 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
@@ -67,17 +70,20 @@ require (
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.5.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
mvdan.cc/gofumpt v0.6.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/httprunner/funplugin => ../funplugin
|
||||
|
||||
20
go.sum
20
go.sum
@@ -146,6 +146,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -178,6 +180,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/incu6us/goimports-reviser/v2 v2.5.3 h1:DzvFl1+qOIDukqN8vMM/10MQswFQywUdwXxsjuowxlc=
|
||||
github.com/incu6us/goimports-reviser/v2 v2.5.3/go.mod h1:P18aXhQaED7izHIP9IPI9PqEs7Y7D9okq71Q8Y8yHN4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
||||
@@ -290,6 +294,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
@@ -369,6 +375,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -402,6 +410,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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=
|
||||
@@ -420,6 +430,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -468,6 +480,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
@@ -479,6 +493,8 @@ 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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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=
|
||||
@@ -522,6 +538,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -638,6 +656,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
|
||||
mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -483,3 +483,10 @@ func ConvertToStringSlice(val interface{}) ([]string, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("invalid type for conversion to []string")
|
||||
}
|
||||
|
||||
func GetCurrentDay() string {
|
||||
now := time.Now()
|
||||
// 格式化日期为 yyyyMMdd
|
||||
formattedDate := now.Format("20060102")
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
v4.3.10
|
||||
v4.5.0
|
||||
|
||||
|
||||
@@ -107,25 +107,38 @@ func (d *Device) features() (features Features, err error) {
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func (d Device) HasAttribute(key string) bool {
|
||||
func (d *Device) HasAttribute(key string) bool {
|
||||
_, ok := d.attrs[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d Device) Product() (string, error) {
|
||||
func (d *Device) Product() (string, error) {
|
||||
if d.HasAttribute("product") {
|
||||
return d.attrs["product"], nil
|
||||
}
|
||||
return "", errors.New("does not have attribute: product")
|
||||
}
|
||||
|
||||
func (d Device) Model() (string, error) {
|
||||
func (d *Device) Model() (string, error) {
|
||||
if d.HasAttribute("model") {
|
||||
return d.attrs["model"], nil
|
||||
}
|
||||
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) {
|
||||
if d.HasAttribute("usb") {
|
||||
return d.attrs["usb"], nil
|
||||
@@ -133,7 +146,7 @@ func (d *Device) Usb() (string, error) {
|
||||
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") {
|
||||
return d.attrs["transport_id"], nil
|
||||
}
|
||||
@@ -524,7 +537,7 @@ func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
|
||||
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 (
|
||||
tp transport
|
||||
filesize int64
|
||||
@@ -537,8 +550,11 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = tp.Close() }()
|
||||
|
||||
cmd := fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)
|
||||
cmd := "abb_exec:package\x00install\x00-t"
|
||||
for _, arg := range args {
|
||||
cmd += "\x00" + arg
|
||||
}
|
||||
cmd += fmt.Sprintf("\x00-S\x00%d", filesize)
|
||||
if err = tp.SendWithCheck(cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -555,7 +571,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
||||
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 {
|
||||
return strings.Contains(ret, "Failure")
|
||||
}
|
||||
@@ -575,8 +591,9 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error pushing: %v", err)
|
||||
}
|
||||
|
||||
res, err := d.RunShellCommand("pm", "install", "-f", remote)
|
||||
args = append([]string{"install"}, args...)
|
||||
args = append(args, "-f", remote)
|
||||
res, err := d.RunShellCommand("pm", args...)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "install apk failed")
|
||||
}
|
||||
@@ -591,7 +608,7 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
|
||||
if len(keepData) == 0 {
|
||||
keepData = []bool{false}
|
||||
}
|
||||
packageName = strings.ReplaceAll(packageName, " ", "")
|
||||
packageName = strings.TrimSpace(packageName)
|
||||
if len(packageName) == 0 {
|
||||
return "", fmt.Errorf("invalid package name")
|
||||
}
|
||||
@@ -603,6 +620,33 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
|
||||
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.TrimSpace(packageName)
|
||||
if len(packageName) == 0 {
|
||||
return false
|
||||
}
|
||||
return builtin.Contains(packages, packageName)
|
||||
}
|
||||
|
||||
func (d *Device) ScreenCap() ([]byte, error) {
|
||||
if d.HasFeature(FeatShellV2) {
|
||||
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
||||
|
||||
@@ -60,7 +60,10 @@ func TestDevice_Product(t *testing.T) {
|
||||
|
||||
for i := range devices {
|
||||
dev := devices[i]
|
||||
product := dev.Product()
|
||||
product, err := dev.Product()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(dev.Serial(), product)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +73,24 @@ func TestDevice_Model(t *testing.T) {
|
||||
|
||||
for i := range devices {
|
||||
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 {
|
||||
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 := dev.IsPackagesInstalled("io.appium.uiautomator2.server")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(installed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevice_HasFeature(t *testing.T) {
|
||||
setupDevices(t)
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ func (o *ActionOptions) updateData(data map[string]interface{}) {
|
||||
data["frequency"] = o.Frequency
|
||||
}
|
||||
if _, ok := data["frequency"]; !ok {
|
||||
data["frequency"] = 60 // default frequency
|
||||
data["frequency"] = 10 // default frequency
|
||||
}
|
||||
|
||||
if _, ok := data["replace"]; !ok {
|
||||
@@ -320,6 +320,11 @@ func NewActionOptions(options ...ActionOption) *ActionOptions {
|
||||
return actionOptions
|
||||
}
|
||||
|
||||
type TapTextAction struct {
|
||||
Text string
|
||||
Options []ActionOption
|
||||
}
|
||||
|
||||
type ActionOption func(o *ActionOptions)
|
||||
|
||||
func WithCustomOption(key string, value interface{}) ActionOption {
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/funplugin/myexec"
|
||||
"github.com/pkg/errors"
|
||||
"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/env"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
||||
)
|
||||
|
||||
const AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
||||
const (
|
||||
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
||||
UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
|
||||
)
|
||||
|
||||
type adbDriver struct {
|
||||
Driver
|
||||
@@ -91,7 +98,14 @@ func (ad *adbDriver) WindowSize() (size Size, err error) {
|
||||
return Size{Width: width, Height: height}, nil
|
||||
}
|
||||
}
|
||||
|
||||
orientation, err := ad.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
if orientation != OrientationPortrait {
|
||||
size.Width, size.Height = size.Height, size.Width
|
||||
}
|
||||
err = errors.New("physical window size not found by adb")
|
||||
return
|
||||
}
|
||||
@@ -185,6 +199,24 @@ func (ad *adbDriver) StopCamera() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Orientation() (orientation Orientation, err error) {
|
||||
output, err := ad.adbClient.RunShellCommand("dumpsys", "input", "|", "grep", "'SurfaceOrientation'")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(`SurfaceOrientation: (\d)`)
|
||||
matches := re.FindStringSubmatch(output)
|
||||
if len(matches) > 1 { // 确保找到了匹配项
|
||||
if matches[1] == "0" || matches[1] == "2" {
|
||||
return OrientationPortrait, nil
|
||||
} else if matches[1] == "1" || matches[1] == "3" {
|
||||
return OrientationLandscapeLeft, nil
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("not found SurfaceOrientation value")
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Homescreen() (err error) {
|
||||
return ad.PressKeyCode(KCHome, KMEmpty)
|
||||
}
|
||||
@@ -326,6 +358,15 @@ func (ad *adbDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||
err = ad.SendUnicodeKeys(text, options...)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
err = ad.InputText(text, options...)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) InputText(text string, options ...ActionOption) (err error) {
|
||||
// adb shell input text <text>
|
||||
_, err = ad.adbClient.RunShellCommand("input", "text", text)
|
||||
if err != nil {
|
||||
@@ -334,6 +375,36 @@ func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
|
||||
// If the Unicode IME is not installed, fall back to the old interface.
|
||||
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
|
||||
// In release version: without the Unicode IME installed, the test cannot execute.
|
||||
if !ad.IsUnicodeIMEInstalled() {
|
||||
return fmt.Errorf("appium unicode ime not installed")
|
||||
}
|
||||
currentIme, err := ad.GetIme()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if currentIme != UnicodeImePackageName {
|
||||
defer func() {
|
||||
_ = ad.SetIme(currentIme)
|
||||
}()
|
||||
err = ad.SetIme(UnicodeImePackageName)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("set Unicode Ime failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
|
||||
return
|
||||
}
|
||||
err = ad.InputText("\""+strings.ReplaceAll(encodedStr, "\"", "\\\"")+"\"", options...)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad *adbDriver) IsAdbKeyBoardInstalled() bool {
|
||||
output, err := ad.adbClient.RunShellCommand("ime", "list", "-a")
|
||||
if err != nil {
|
||||
@@ -342,6 +413,14 @@ func (ad *adbDriver) IsAdbKeyBoardInstalled() bool {
|
||||
return strings.Contains(output, AdbKeyBoardPackageName)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) IsUnicodeIMEInstalled() bool {
|
||||
output, err := ad.adbClient.RunShellCommand("ime", "list", "-s")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(output, UnicodeImePackageName)
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) {
|
||||
defer func() {
|
||||
// Reset to default, don't care which keyboard was chosen before switch:
|
||||
@@ -404,10 +483,103 @@ func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
err = errDriverNotImplemented
|
||||
_, err = ad.adbClient.RunShellCommand("rm", "-rf", "/sdcard/window_dump.xml")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 高版本报错 ERROR: null root node returned by UiTestAutomationBridge.
|
||||
_, 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
|
||||
}
|
||||
|
||||
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||
source, err := ad.Source()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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.Layout, 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 []Layout, text string, options ...ActionOption) []Bounds {
|
||||
actionOptions := NewActionOptions(options...)
|
||||
var results []Bounds
|
||||
for _, node := range nodes {
|
||||
result := ad.searchNodes(node.Layout, 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.Layout, text, options...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if node.Bounds != nil {
|
||||
results = append(results, *node.Bounds)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (ad *adbDriver) AccessibleSource() (source string, err error) {
|
||||
err = errDriverNotImplemented
|
||||
return
|
||||
@@ -435,14 +607,8 @@ func (ad *adbDriver) IsHealthy() (healthy bool, err error) {
|
||||
|
||||
func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
||||
log.Info().Msg("start adb log recording")
|
||||
|
||||
// clear logcat
|
||||
if _, err = ad.adbClient.RunShellCommand("logcat", "-c"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start logcat
|
||||
err = ad.logcat.CatchLogcat()
|
||||
err = ad.logcat.CatchLogcat("iesqaMonitor:V")
|
||||
if err != nil {
|
||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
||||
fmt.Sprintf("start adb log recording failed: %v", err))
|
||||
@@ -452,17 +618,18 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
||||
}
|
||||
|
||||
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
log.Info().Msg("stop adb log recording")
|
||||
err = ad.logcat.Stop()
|
||||
defer func() {
|
||||
log.Info().Msg("stop adb log recording")
|
||||
err = ad.logcat.Stop()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get adb log recording")
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get adb log recording")
|
||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
||||
fmt.Sprintf("get adb log recording failed: %v", err))
|
||||
return "", err
|
||||
log.Error().Err(err).Msg("failed to close adb log writer")
|
||||
}
|
||||
content := ad.logcat.logBuffer.String()
|
||||
log.Info().Str("logcat content", content).Msg("display logcat content")
|
||||
pointRes := ConvertPoints(content)
|
||||
pointRes := ConvertPoints(ad.logcat.logs)
|
||||
|
||||
// 没有解析到打点日志,走兜底逻辑
|
||||
if len(pointRes) == 0 {
|
||||
log.Info().Msg("action log is null, use action file >>>")
|
||||
@@ -476,7 +643,6 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 先保持原有状态码不变,这里不return error
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read log file fail")
|
||||
@@ -488,13 +654,28 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||
return pointRes, nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(files[0])
|
||||
reader, err := os.Open(files[0])
|
||||
if err != nil {
|
||||
log.Info().Msg("read File error")
|
||||
log.Info().Msg("open File error")
|
||||
return pointRes, nil
|
||||
}
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
}()
|
||||
|
||||
var lines []string // 创建一个空的字符串数组来存储文件的每一行
|
||||
|
||||
// 使用 bufio.NewScanner 读取文件
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text()) // 将每行文本添加到字符串数组
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return pointRes, nil
|
||||
}
|
||||
|
||||
pointRes = ConvertPoints(string(data))
|
||||
pointRes = ConvertPoints(lines)
|
||||
}
|
||||
return pointRes, nil
|
||||
}
|
||||
@@ -534,6 +715,30 @@ func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
||||
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
|
||||
}
|
||||
|
||||
func (ad *adbDriver) SetIme(ime string) error {
|
||||
_, err := ad.adbClient.RunShellCommand("ime", "set", ime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// even if the shell command has returned,
|
||||
// as there might be a situation where the input method has not been completely switched yet
|
||||
// Listen to the following message.
|
||||
// InputMethodManagerService: onServiceConnected, name:ComponentInfo{io.appium.settings/io.appium.settings.UnicodeIME}, token:android.os.Binder@44f825
|
||||
// But there is no such log on Vivo.
|
||||
time.Sleep(3 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) GetIme() (ime string, err error) {
|
||||
currentIme, err := ad.adbClient.RunShellCommand("settings", "get", "secure", "default_input_method")
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("get default ime failed")
|
||||
return
|
||||
}
|
||||
currentIme = strings.TrimSpace(currentIme)
|
||||
return currentIme, nil
|
||||
}
|
||||
|
||||
func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...string) error {
|
||||
log.Debug().Str("package_name", packageName).
|
||||
Strs("activity_type", activityType).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -22,7 +23,6 @@ var (
|
||||
AdbServerPort = gadb.AdbServerPort // 5037
|
||||
UIA2ServerHost = "localhost"
|
||||
UIA2ServerPort = 6790
|
||||
DeviceTempPath = "/data/local/tmp"
|
||||
)
|
||||
|
||||
const forwardToPrefix = "forward-to-"
|
||||
@@ -276,27 +276,43 @@ func getFreePort() (int, error) {
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
type LineCallback func(string)
|
||||
|
||||
type AdbLogcat struct {
|
||||
serial string
|
||||
logBuffer *bytes.Buffer
|
||||
errs []error
|
||||
stopping chan struct{}
|
||||
done chan struct{}
|
||||
cmd *exec.Cmd
|
||||
serial string
|
||||
// logBuffer *bytes.Buffer
|
||||
errs []error
|
||||
stopping chan struct{}
|
||||
done chan struct{}
|
||||
cmd *exec.Cmd
|
||||
callback LineCallback
|
||||
logs []string
|
||||
}
|
||||
|
||||
func NewAdbLogcatWithCallback(serial string, callback LineCallback) *AdbLogcat {
|
||||
return &AdbLogcat{
|
||||
serial: serial,
|
||||
// logBuffer: new(bytes.Buffer),
|
||||
stopping: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
callback: callback,
|
||||
logs: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdbLogcat(serial string) *AdbLogcat {
|
||||
return &AdbLogcat{
|
||||
serial: serial,
|
||||
logBuffer: new(bytes.Buffer),
|
||||
stopping: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
serial: serial,
|
||||
// logBuffer: new(bytes.Buffer),
|
||||
stopping: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
logs: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// CatchLogcatContext starts logcat with timeout context
|
||||
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
|
||||
if err = l.CatchLogcat(); err != nil {
|
||||
if err = l.CatchLogcat(""); err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
@@ -331,7 +347,7 @@ func (l *AdbLogcat) Errors() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (l *AdbLogcat) CatchLogcat() (err error) {
|
||||
func (l *AdbLogcat) CatchLogcat(filter string) (err error) {
|
||||
if l.cmd != nil {
|
||||
log.Warn().Msg("logcat already start")
|
||||
return nil
|
||||
@@ -341,33 +357,43 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
||||
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{"-s", l.serial, "logcat", "--format", "time"}
|
||||
if filter != "" {
|
||||
args = append(args, "-s", filter)
|
||||
}
|
||||
// start logcat
|
||||
l.cmd = myexec.Command("adb", "-s", l.serial,
|
||||
"logcat", "--format", "time", "-s", "iesqaMonitor:V")
|
||||
l.cmd.Stderr = l.logBuffer
|
||||
l.cmd.Stdout = l.logBuffer
|
||||
l.cmd = myexec.Command("adb", args...)
|
||||
// l.cmd.Stderr = l.logBuffer
|
||||
// l.cmd.Stdout = l.logBuffer
|
||||
reader, err := l.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = l.cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if l.callback != nil {
|
||||
l.callback(line) // Process each line with callback
|
||||
} else {
|
||||
l.logs = append(l.logs, line) // Store line if no callback
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
<-l.stopping
|
||||
if e := reader.Close(); e != nil {
|
||||
log.Error().Err(e).Msg("close logcat reader failed")
|
||||
}
|
||||
if e := myexec.KillProcessesByGpid(l.cmd); e != nil {
|
||||
log.Error().Err(e).Msg("kill logcat process failed")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -381,8 +407,8 @@ type ExportPoint struct {
|
||||
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
|
||||
}
|
||||
|
||||
func ConvertPoints(data string) (eps []ExportPoint) {
|
||||
lines := strings.Split(data, "\n")
|
||||
func ConvertPoints(lines []string) (eps []ExportPoint) {
|
||||
log.Info().Msg("ConvertPoints")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "ext") {
|
||||
idx := strings.Index(line, "{")
|
||||
@@ -396,6 +422,7 @@ func ConvertPoints(data string) (eps []ExportPoint) {
|
||||
log.Error().Msg("failed to parse point data")
|
||||
continue
|
||||
}
|
||||
log.Info().Msg(line)
|
||||
eps = append(eps, p)
|
||||
}
|
||||
}
|
||||
@@ -562,7 +589,7 @@ func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
|
||||
//
|
||||
// For example, to simulate a user click on
|
||||
// the third image that is enabled in a UI screen, you
|
||||
// could specify a a search criteria where the instance is
|
||||
// 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:
|
||||
|
||||
62
hrp/pkg/uixt/android_layout.go
Normal file
62
hrp/pkg/uixt/android_layout.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Attributes struct {
|
||||
Index int `xml:"index,attr"`
|
||||
Package string `xml:"package,attr"`
|
||||
Class string `xml:"class,attr"`
|
||||
Text string `xml:"text,attr"`
|
||||
ResourceId string `xml:"resource-id,attr"`
|
||||
Checkable bool `xml:"checkable,attr"`
|
||||
Checked bool `xml:"checked,attr"`
|
||||
Clickable bool `xml:"clickable,attr"`
|
||||
Enabled bool `xml:"enabled,attr"`
|
||||
Focusable bool `xml:"focusable,attr"`
|
||||
Focused bool `xml:"focused,attr"`
|
||||
LongClickable bool `xml:"long-clickable,attr"`
|
||||
Password bool `xml:"password,attr"`
|
||||
Scrollable bool `xml:"scrollable,attr"`
|
||||
Selected bool `xml:"selected,attr"`
|
||||
Bounds *Bounds `xml:"bounds,attr"`
|
||||
Displayed bool `xml:"displayed,attr"`
|
||||
}
|
||||
|
||||
type Hierarchy struct {
|
||||
XMLName xml.Name `xml:"hierarchy"`
|
||||
Attributes
|
||||
Layout []Layout `xml:",any"`
|
||||
}
|
||||
|
||||
type Layout struct {
|
||||
Attributes
|
||||
Layout []Layout `xml:",any"`
|
||||
}
|
||||
|
||||
type Bounds struct {
|
||||
X1, Y1, X2, Y2 int
|
||||
}
|
||||
|
||||
func (b *Bounds) Center() (float64, float64) {
|
||||
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,18 +6,21 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
uiaServerURL = "http://localhost:6790/wd/hub"
|
||||
uiaServerURL = "http://forward-to-6790:6790/wd/hub"
|
||||
driverExt *DriverExt
|
||||
)
|
||||
|
||||
func setupAndroid(t *testing.T) {
|
||||
device, err := NewAndroidDevice()
|
||||
checkErr(t, err)
|
||||
device.UIA2 = false
|
||||
driverExt, err = device.NewDriver()
|
||||
checkErr(t, err)
|
||||
}
|
||||
@@ -132,6 +135,18 @@ func TestDriver_Source(t *testing.T) {
|
||||
t.Log(source)
|
||||
}
|
||||
|
||||
func TestDriver_TapByText(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.TapByText("安装")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_BatteryInfo(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
if err != nil {
|
||||
@@ -204,7 +219,7 @@ func TestDriver_Swipe(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.Swipe(400, 1000, 400, 500)
|
||||
err = driver.Swipe(400, 1000, 400, 500, WithPressDuration(2000))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -215,6 +230,14 @@ func TestDriver_Swipe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Swipe_Relative(t *testing.T) {
|
||||
setupAndroid(t)
|
||||
err := driverExt.SwipeRelative(0.5, 0.7, 0.5, 0.5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_Drag(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
if err != nil {
|
||||
@@ -235,28 +258,26 @@ func TestDriver_Drag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDriver_SendKeys(t *testing.T) {
|
||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||
setupAndroid(t)
|
||||
|
||||
err := driverExt.Driver.SendKeys("Android\"输入速度测试", WithIdentifier("test"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.SendKeys("abc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
err = driver.SendKeys("def")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
//err = driver.SendKeys("def")
|
||||
//if err != nil {
|
||||
// t.Fatal(err)
|
||||
//}
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
err = driver.SendKeys("\\n")
|
||||
//err = driver.SendKeys("\\n")
|
||||
// err = driver.SendKeys(`\n`, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//if err != nil {
|
||||
// t.Fatal(err)
|
||||
//}
|
||||
}
|
||||
|
||||
func TestDriver_PressBack(t *testing.T) {
|
||||
@@ -421,10 +442,44 @@ func TestDriver_AppTerminate(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}}"
|
||||
eps := ConvertPoints(data)
|
||||
|
||||
eps := ConvertPoints(strings.Split(data, "\n"))
|
||||
if len(eps) != 3 {
|
||||
t.Fatal()
|
||||
}
|
||||
jsons, _ := json.Marshal(eps)
|
||||
println(fmt.Sprintf("%v", string(jsons)))
|
||||
}
|
||||
|
||||
func TestDriver_ShellInputUnicode(t *testing.T) {
|
||||
device, _ := NewAndroidDevice()
|
||||
driver, err := device.NewAdbDriver()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = driver.SendKeys("test中文输入&")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw, err := driver.Screenshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
||||
)
|
||||
|
||||
var errDriverNotImplemented = errors.New("driver method not implemented")
|
||||
@@ -224,6 +226,14 @@ func (ud *uiaDriver) WindowSize() (size Size, err error) {
|
||||
return Size{}, err
|
||||
}
|
||||
size = reply.Value.Size
|
||||
orientation, err := ud.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
if orientation != OrientationPortrait {
|
||||
size.Width, size.Height = size.Height, size.Width
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -253,6 +263,20 @@ func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...K
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) Orientation() (orientation Orientation, err error) {
|
||||
// [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = ud.httpGET("/session", ud.sessionId, "/orientation"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
reply := new(struct{ Value Orientation })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
orientation = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) Tap(x, y int, options ...ActionOption) error {
|
||||
return ud.TapFloat(float64(x), float64(y), options...)
|
||||
}
|
||||
@@ -268,15 +292,31 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
|
||||
x += actionOptions.getRandomOffset()
|
||||
y += actionOptions.getRandomOffset()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
duration := 100.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "pointer",
|
||||
"parameters": map[string]string{"pointerType": "touch"},
|
||||
"id": "touch",
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": x, "y": y, "origin": "viewport"},
|
||||
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
|
||||
map[string]interface{}{"type": "pause", "duration": duration},
|
||||
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// update data options in post data for extra uiautomator configurations
|
||||
actionOptions.updateData(data)
|
||||
|
||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap")
|
||||
return
|
||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "actions/tap")
|
||||
return err
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
|
||||
@@ -358,17 +398,30 @@ func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...Actio
|
||||
toX += actionOptions.getRandomOffset()
|
||||
toY += actionOptions.getRandomOffset()
|
||||
|
||||
duration := 200.0
|
||||
if actionOptions.PressDuration > 0 {
|
||||
duration = actionOptions.PressDuration
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"startX": fromX,
|
||||
"startY": fromY,
|
||||
"endX": toX,
|
||||
"endY": toY,
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "pointer",
|
||||
"parameters": map[string]string{"pointerType": "touch"},
|
||||
"id": "touch",
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": fromX, "y": fromY, "origin": "viewport"},
|
||||
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
|
||||
map[string]interface{}{"type": "pointerMove", "duration": duration, "x": toX, "y": toY, "origin": "viewport"},
|
||||
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// update data options in post data for extra uiautomator configurations
|
||||
actionOptions.updateData(data)
|
||||
|
||||
_, err := ud.httpPOST(data, "/session", ud.sessionId, "touch/perform")
|
||||
_, err := ud.httpPOST(data, "/session", ud.sessionId, "actions/swipe")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -415,25 +468,79 @@ func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
|
||||
return
|
||||
}
|
||||
|
||||
// SendKeys Android input does not support setting frequency.
|
||||
func (ud *uiaDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
|
||||
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
|
||||
actionOptions := NewActionOptions(options...)
|
||||
data := map[string]interface{}{
|
||||
"text": text,
|
||||
}
|
||||
// new data options in post data for extra uiautomator configurations
|
||||
actionOptions.updateData(data)
|
||||
|
||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "keys")
|
||||
err = ud.SendUnicodeKeys(text, options...)
|
||||
if err != nil {
|
||||
// use com.android.adbkeyboard if existed
|
||||
if ud.IsAdbKeyBoardInstalled() {
|
||||
err = ud.SendKeysByAdbKeyBoard(text)
|
||||
} else {
|
||||
_, err = ud.adbClient.RunShellCommand("input", "text", text)
|
||||
data := map[string]interface{}{
|
||||
"text": text,
|
||||
}
|
||||
|
||||
// new data options in post data for extra uiautomator configurations
|
||||
actionOptions.updateData(data)
|
||||
|
||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/keys")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
|
||||
// If the Unicode IME is not installed, fall back to the old interface.
|
||||
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
|
||||
// In release version: without the Unicode IME installed, the test cannot execute.
|
||||
if !ud.IsUnicodeIMEInstalled() {
|
||||
return fmt.Errorf("appium unicode ime not installed")
|
||||
}
|
||||
currentIme, err := ud.adbDriver.GetIme()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if currentIme != UnicodeImePackageName {
|
||||
defer func() {
|
||||
_ = ud.adbDriver.SetIme(currentIme)
|
||||
}()
|
||||
err = ud.adbDriver.SetIme(UnicodeImePackageName)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("set Unicode Ime failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
|
||||
return
|
||||
}
|
||||
err = ud.SendActionKey(encodedStr, options...)
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) SendActionKey(text string, options ...ActionOption) (err error) {
|
||||
actionOptions := NewActionOptions(options...)
|
||||
var actions []interface{}
|
||||
for i, c := range text {
|
||||
actions = append(actions, map[string]interface{}{"type": "keyDown", "value": string(c)},
|
||||
map[string]interface{}{"type": "keyUp", "value": string(c)})
|
||||
if i != len(text)-1 {
|
||||
actions = append(actions, map[string]interface{}{"type": "pause", "duration": 40})
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"actions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "key",
|
||||
"id": "key",
|
||||
"actions": actions,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// new data options in post data for extra uiautomator configurations
|
||||
actionOptions.updateData(data)
|
||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions/keys")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -492,3 +599,39 @@ func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
source = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||
source, err := ud.Source()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sourceTree = new(Hierarchy)
|
||||
err = xml.Unmarshal([]byte(source), sourceTree)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
|
||||
sourceTree, err := ud.sourceTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ud.tapByTextUsingHierarchy(sourceTree, text, options...)
|
||||
}
|
||||
|
||||
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||
sourceTree, err := ud.sourceTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
err := ud.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -511,6 +511,10 @@ type WebDriver interface {
|
||||
// since the location service needs some time to update the location data.
|
||||
Location() (Location, error)
|
||||
BatteryInfo() (BatteryInfo, error)
|
||||
|
||||
// WindowSize Return the width and height in portrait mode.
|
||||
// when getting the window size in wda/ui2/adb, if the device is in landscape mode,
|
||||
// the width and height will be reversed.
|
||||
WindowSize() (Size, error)
|
||||
Screen() (Screen, error)
|
||||
Scale() (float64, error)
|
||||
@@ -537,6 +541,8 @@ type WebDriver interface {
|
||||
// StopCamera Stops the camera for recording
|
||||
StopCamera() error
|
||||
|
||||
Orientation() (orientation Orientation, err error)
|
||||
|
||||
// Tap Sends a tap event at the coordinate.
|
||||
Tap(x, y int, options ...ActionOption) error
|
||||
TapFloat(x, y float64, options ...ActionOption) error
|
||||
@@ -583,6 +589,11 @@ type WebDriver interface {
|
||||
|
||||
// Source Return application elements tree
|
||||
Source(srcOpt ...SourceOption) (string, error)
|
||||
|
||||
TapByText(text string, options ...ActionOption) error
|
||||
|
||||
TapByTexts(actions ...TapTextAction) error
|
||||
|
||||
// AccessibleSource Return application elements accessibility tree
|
||||
AccessibleSource() (string, error)
|
||||
|
||||
|
||||
@@ -207,6 +207,14 @@ func (wd *wdaDriver) WindowSize() (size Size, err error) {
|
||||
}
|
||||
size.Height = size.Height * int(scale)
|
||||
size.Width = size.Width * int(scale)
|
||||
orientation, err := wd.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
if orientation != OrientationPortrait {
|
||||
size.Width, size.Height = size.Height, size.Width
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -547,8 +555,8 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...Action
|
||||
|
||||
// update data options in post data for extra WDA configurations
|
||||
actionOptions.updateData(data)
|
||||
|
||||
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration")
|
||||
// wda 43 version
|
||||
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/drag")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -751,6 +759,14 @@ func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
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) {
|
||||
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
|
||||
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
||||
|
||||
@@ -10,17 +10,23 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
bundleId = "com.apple.Preferences"
|
||||
driver WebDriver
|
||||
bundleId = "com.apple.Preferences"
|
||||
driver WebDriver
|
||||
iOSDriverExt *DriverExt
|
||||
)
|
||||
|
||||
func setup(t *testing.T) {
|
||||
device, err := NewIOSDevice()
|
||||
device, err := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800), WithWDALogOn(true))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
driver, err = device.NewUSBDriver(nil)
|
||||
capabilities := NewCapabilities()
|
||||
capabilities.WithDefaultAlertAction(AlertActionAccept)
|
||||
driver, err = device.NewUSBDriver(capabilities)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
iOSDriverExt, err = newDriverExt(device, driver, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -267,6 +273,16 @@ func Test_remoteWD_Drag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Relative_Drag(t *testing.T) {
|
||||
setup(t)
|
||||
|
||||
// err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5))
|
||||
err := iOSDriverExt.SwipeRelative(0.5, 0.7, 0.5, 0.5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_remoteWD_SetPasteboard(t *testing.T) {
|
||||
setup(t)
|
||||
|
||||
@@ -305,12 +321,14 @@ func Test_remoteWD_GetPasteboard(t *testing.T) {
|
||||
|
||||
func Test_remoteWD_SendKeys(t *testing.T) {
|
||||
setup(t)
|
||||
|
||||
err := driver.SendKeys("App Store")
|
||||
driver.StartCaptureLog("hrp_wda_log")
|
||||
err := driver.SendKeys("", WithIdentifier("test"))
|
||||
result, _ := driver.StopCaptureLog()
|
||||
// err := driver.SendKeys("App Store", WithFrequency(3))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(result)
|
||||
}
|
||||
|
||||
func Test_remoteWD_PressButton(t *testing.T) {
|
||||
@@ -374,7 +392,7 @@ func Test_remoteWD_Source(t *testing.T) {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
source, err = driver.Source(NewSourceOption().WithScope("AppiumAUT"))
|
||||
source, err = driver.Source()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -19,17 +19,30 @@ func assertRelative(p float64) bool {
|
||||
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...ActionOption) error {
|
||||
width := dExt.windowSize.Width
|
||||
height := dExt.windowSize.Height
|
||||
orientation, err := dExt.Driver.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("swipe from (%v, %v) to (%v, %v) get orientation failed, use default orientation",
|
||||
fromX, fromY, toX, toY)
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
|
||||
if !assertRelative(fromX) || !assertRelative(fromY) ||
|
||||
!assertRelative(toX) || !assertRelative(toY) {
|
||||
return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1",
|
||||
fromX, fromY, toX, toY)
|
||||
}
|
||||
|
||||
fromX = float64(width) * fromX
|
||||
fromY = float64(height) * fromY
|
||||
toX = float64(width) * toX
|
||||
toY = float64(height) * toY
|
||||
// 左转和右转都是"LANDSCAPE"
|
||||
if orientation == OrientationPortrait {
|
||||
fromX = float64(width) * fromX
|
||||
fromY = float64(height) * fromY
|
||||
toX = float64(width) * toX
|
||||
toY = float64(height) * toY
|
||||
} else {
|
||||
fromX = float64(height) * fromX
|
||||
fromY = float64(width) * fromY
|
||||
toX = float64(height) * toX
|
||||
toY = float64(width) * toY
|
||||
}
|
||||
|
||||
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package uixt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (dExt *DriverExt) TapAbsXY(x, y float64, options ...ActionOption) error {
|
||||
@@ -15,9 +16,19 @@ func (dExt *DriverExt) TapXY(x, y float64, options ...ActionOption) error {
|
||||
return fmt.Errorf("x, y percentage should be <= 1, got x=%v, y=%v", x, y)
|
||||
}
|
||||
|
||||
x = x * float64(dExt.windowSize.Width)
|
||||
y = y * float64(dExt.windowSize.Height)
|
||||
|
||||
orientation, err := dExt.Driver.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("tap (%v, %v) get orientation failed, use default orientation",
|
||||
x, y)
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
if orientation == OrientationPortrait {
|
||||
x = x * float64(dExt.windowSize.Width)
|
||||
y = y * float64(dExt.windowSize.Height)
|
||||
} else {
|
||||
x = x * float64(dExt.windowSize.Height)
|
||||
y = y * float64(dExt.windowSize.Width)
|
||||
}
|
||||
return dExt.TapAbsXY(x, y, options...)
|
||||
}
|
||||
|
||||
@@ -86,7 +97,19 @@ func (dExt *DriverExt) DoubleTapXY(x, y float64) error {
|
||||
if x > 1 || y > 1 {
|
||||
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
|
||||
}
|
||||
|
||||
orientation, err := dExt.Driver.Orientation()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("tap (%v, %v) get orientation failed, use default orientation",
|
||||
x, y)
|
||||
orientation = OrientationPortrait
|
||||
}
|
||||
if orientation == OrientationPortrait {
|
||||
x = x * float64(dExt.windowSize.Width)
|
||||
y = y * float64(dExt.windowSize.Height)
|
||||
} else {
|
||||
x = x * float64(dExt.windowSize.Height)
|
||||
y = y * float64(dExt.windowSize.Width)
|
||||
}
|
||||
x = x * float64(dExt.windowSize.Width)
|
||||
y = y * float64(dExt.windowSize.Height)
|
||||
return dExt.Driver.DoubleTapFloat(x, y)
|
||||
|
||||
149
hrp/pkg/utf7/decoder.go
Normal file
149
hrp/pkg/utf7/decoder.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7.
|
||||
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
|
||||
|
||||
type decoder struct {
|
||||
ascii bool
|
||||
}
|
||||
|
||||
func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
for i := 0; i < len(src); i++ {
|
||||
ch := src[i]
|
||||
|
||||
if ch < min || ch > max { // Illegal code point in ASCII mode
|
||||
err = ErrInvalidUTF7
|
||||
return
|
||||
}
|
||||
|
||||
if ch != '&' {
|
||||
if nDst+1 > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
|
||||
nSrc++
|
||||
|
||||
dst[nDst] = ch
|
||||
nDst++
|
||||
|
||||
d.ascii = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the end of the Base64 or "&-" segment
|
||||
start := i + 1
|
||||
for i++; i < len(src) && src[i] != '-'; i++ {
|
||||
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
|
||||
err = ErrInvalidUTF7
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(src) { // Implicit shift ("&...")
|
||||
if atEOF {
|
||||
err = ErrInvalidUTF7
|
||||
} else {
|
||||
err = transform.ErrShortSrc
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if i == start { // Escape sequence "&-"
|
||||
b = []byte{'&'}
|
||||
d.ascii = true
|
||||
} else { // Control or non-ASCII code points in base64
|
||||
if !d.ascii { // Null shift ("&...-&...-")
|
||||
err = ErrInvalidUTF7
|
||||
return
|
||||
}
|
||||
|
||||
b = decode(src[start:i])
|
||||
d.ascii = false
|
||||
}
|
||||
|
||||
if len(b) == 0 { // Bad encoding
|
||||
err = ErrInvalidUTF7
|
||||
return
|
||||
}
|
||||
|
||||
if nDst+len(b) > len(dst) {
|
||||
d.ascii = true
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
|
||||
nSrc = i + 1
|
||||
|
||||
for _, ch := range b {
|
||||
dst[nDst] = ch
|
||||
nDst++
|
||||
}
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
d.ascii = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *decoder) Reset() {
|
||||
d.ascii = true
|
||||
}
|
||||
|
||||
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
|
||||
// A nil slice is returned if the encoding is invalid.
|
||||
func decode(b64 []byte) []byte {
|
||||
var b []byte
|
||||
|
||||
// Allocate a single block of memory large enough to store the Base64 data
|
||||
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
|
||||
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
|
||||
// double the space allocation for UTF-8.
|
||||
if n := len(b64); b64[n-1] == '=' {
|
||||
return nil
|
||||
} else if n&3 == 0 {
|
||||
b = make([]byte, b64Enc.DecodedLen(n)*3)
|
||||
} else {
|
||||
n += 4 - n&3
|
||||
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
|
||||
copy(b[copy(b, b64):n], []byte("=="))
|
||||
b64, b = b[:n], b[n:]
|
||||
}
|
||||
|
||||
// Decode Base64 into the first 1/3rd of b
|
||||
n, err := b64Enc.Decode(b, b64)
|
||||
if err != nil || n&1 == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode UTF-16-BE into the remaining 2/3rds of b
|
||||
b, s := b[:n], b[n:]
|
||||
j := 0
|
||||
for i := 0; i < n; i += 2 {
|
||||
r := rune(b[i])<<8 | rune(b[i+1])
|
||||
if utf16.IsSurrogate(r) {
|
||||
if i += 2; i == n {
|
||||
return nil
|
||||
}
|
||||
r2 := rune(b[i])<<8 | rune(b[i+1])
|
||||
if r = utf16.DecodeRune(r, r2); r == repl {
|
||||
return nil
|
||||
}
|
||||
} else if min <= r && r <= max {
|
||||
return nil
|
||||
}
|
||||
j += utf8.EncodeRune(s[j:], r)
|
||||
}
|
||||
return s[:j]
|
||||
}
|
||||
91
hrp/pkg/utf7/encoder.go
Normal file
91
hrp/pkg/utf7/encoder.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
type encoder struct{}
|
||||
|
||||
func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
for i := 0; i < len(src); {
|
||||
ch := src[i]
|
||||
|
||||
var b []byte
|
||||
if min <= ch && ch <= max {
|
||||
b = []byte{ch}
|
||||
if ch == '&' {
|
||||
b = append(b, '-')
|
||||
}
|
||||
|
||||
i++
|
||||
} else {
|
||||
start := i
|
||||
|
||||
// Find the next printable ASCII code point
|
||||
i++
|
||||
for i < len(src) && (src[i] < min || src[i] > max) {
|
||||
i++
|
||||
}
|
||||
|
||||
if !atEOF && i == len(src) {
|
||||
err = transform.ErrShortSrc
|
||||
return
|
||||
}
|
||||
|
||||
b = encode(src[start:i])
|
||||
}
|
||||
|
||||
if nDst+len(b) > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
|
||||
nSrc = i
|
||||
|
||||
for _, ch := range b {
|
||||
dst[nDst] = ch
|
||||
nDst++
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (e *encoder) Reset() {}
|
||||
|
||||
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
|
||||
// removes the padding, and adds UTF-7 shifts.
|
||||
func encode(s []byte) []byte {
|
||||
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
|
||||
// control code points (see table below).
|
||||
b := make([]byte, 0, len(s)+4)
|
||||
for len(s) > 0 {
|
||||
r, size := utf8.DecodeRune(s)
|
||||
if r > utf8.MaxRune {
|
||||
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
|
||||
}
|
||||
s = s[size:]
|
||||
if r1, r2 := utf16.EncodeRune(r); r1 != repl {
|
||||
b = append(b, byte(r1>>8), byte(r1))
|
||||
r = r2
|
||||
}
|
||||
b = append(b, byte(r>>8), byte(r))
|
||||
}
|
||||
|
||||
// Encode as base64
|
||||
n := b64Enc.EncodedLen(len(b)) + 2
|
||||
b64 := make([]byte, n)
|
||||
b64Enc.Encode(b64[1:], b)
|
||||
|
||||
// Strip padding
|
||||
n -= 2 - (len(b)+2)%3
|
||||
b64 = b64[:n]
|
||||
|
||||
// Add UTF-7 shifts
|
||||
b64[0] = '&'
|
||||
b64[n-1] = '-'
|
||||
return b64
|
||||
}
|
||||
34
hrp/pkg/utf7/utf7.go
Normal file
34
hrp/pkg/utf7/utf7.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
const (
|
||||
min = 0x20 // Minimum self-representing UTF-7 value
|
||||
max = 0x7E // Maximum self-representing UTF-7 value
|
||||
|
||||
repl = '\uFFFD' // Unicode replacement code point
|
||||
)
|
||||
|
||||
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
|
||||
|
||||
type enc struct{}
|
||||
|
||||
func (e enc) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{
|
||||
Transformer: &decoder{true},
|
||||
}
|
||||
}
|
||||
|
||||
func (e enc) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{
|
||||
Transformer: &encoder{},
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding is the modified UTF-7 encoding.
|
||||
var Encoding encoding.Encoding = enc{}
|
||||
Reference in New Issue
Block a user