diff --git a/go.mod b/go.mod index 46a736be..ecb2b046 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,11 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/fatih/color v1.16.0 github.com/getsentry/sentry-go v0.13.0 + github.com/gin-gonic/gin v1.10.0 github.com/go-openapi/spec v0.20.7 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.5.0 - github.com/httprunner/funplugin v0.5.4 + github.com/httprunner/funplugin v0.5.5 github.com/jinzhu/copier v0.3.5 github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 @@ -24,13 +25,13 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 gocv.io/x/gocv v0.32.1 - golang.org/x/net v0.20.0 + golang.org/x/net v0.25.0 golang.org/x/oauth2 v0.8.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.15.0 google.golang.org/grpc v1.57.0 - google.golang.org/protobuf v1.32.0 + google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.0 @@ -40,14 +41,24 @@ require ( cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // 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 @@ -55,7 +66,9 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -65,6 +78,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect @@ -75,8 +89,12 @@ require ( 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/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/sys v0.20.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect ) diff --git a/go.sum b/go.sum index 6fb462ea..12f26800 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -57,6 +61,10 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -74,8 +82,14 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -102,7 +116,16 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -172,8 +195,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/httprunner/funplugin v0.5.4 h1:hlfNGcYw2Rv2Mdp1l2S1R6ufwzKgpB9lheFvAxI0LfM= -github.com/httprunner/funplugin v0.5.4/go.mod h1:YZzBBSOSdLZEpHZz0P2E5SOQ+o1+Fbn30oWS4RGHBz0= +github.com/httprunner/funplugin v0.5.5 h1:VU1a6kj1AsJ/ucIhhI5NLHXOP4xnW2JGgk50vBV3Zis= +github.com/httprunner/funplugin v0.5.5/go.mod h1:YZzBBSOSdLZEpHZz0P2E5SOQ+o1+Fbn30oWS4RGHBz0= github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -200,6 +223,10 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -210,6 +237,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -252,6 +281,8 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -309,19 +340,30 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A= github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -335,6 +377,9 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= gocv.io/x/gocv v0.32.1 h1:BC9hHs5+47nVgySUFVKntc6RsF3SULFzqk6OV9xz+C0= gocv.io/x/gocv v0.32.1/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -342,6 +387,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -406,8 +453,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -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/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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= @@ -474,9 +521,10 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.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= @@ -487,8 +535,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -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/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.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= @@ -618,8 +666,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -649,6 +697,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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/hrp/cmd/adb/install.go b/hrp/cmd/adb/install.go new file mode 100644 index 00000000..cce100de --- /dev/null +++ b/hrp/cmd/adb/install.go @@ -0,0 +1,62 @@ +package adb + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +var installCmd = &cobra.Command{ + Use: "install [flags] PACKAGE", + Short: "Push package to the device and install them atomically", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + startTime := time.Now() + defer func() { + sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{ + "args": strings.Join(args, "-"), + "success": err == nil, + "engagement_time_msec": time.Since(startTime).Milliseconds(), + }) + }() + _, err = getDevice(serial) + if err != nil { + return err + } + + device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial)) + if err != nil { + fmt.Println(err) + return err + } + driverExt, err := device.NewDriver() + if err != nil { + fmt.Println(err) + return err + } + replace, _ := cmd.Flags().GetBool("replace") + downgrade, _ := cmd.Flags().GetBool("downgrade") + grant, _ := cmd.Flags().GetBool("grant") + + err = driverExt.Install(args[0], uixt.NewInstallOptions(uixt.WithReinstall(replace), uixt.WithDowngrade(downgrade), uixt.WithGrantPermission(grant))) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("success") + return nil + }, +} + +func init() { + installCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial") + installCmd.Flags().BoolP("replace", "r", false, "replace existing application") + installCmd.Flags().BoolP("downgrade", "d", false, "allow version code downgrade (debuggable packages only)") + installCmd.Flags().BoolP("grant", "g", false, "grant all runtime permissions") + androidRootCmd.AddCommand(installCmd) +} diff --git a/hrp/cmd/ios/install.go b/hrp/cmd/ios/install.go new file mode 100644 index 00000000..7fdcd3eb --- /dev/null +++ b/hrp/cmd/ios/install.go @@ -0,0 +1,52 @@ +package ios + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +var installCmd = &cobra.Command{ + Use: "install [flags] PACKAGE", + Short: "Push package to the device and install them atomically", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + startTime := time.Now() + defer func() { + sdk.SendGA4Event("hrp_adb_devices", map[string]interface{}{ + "args": strings.Join(args, "-"), + "success": err == nil, + "engagement_time_msec": time.Since(startTime).Milliseconds(), + }) + }() + _, err = getDevice(udid) + if err != nil { + return err + } + + device, err := uixt.NewIOSDevice(uixt.WithUDID(udid)) + if err != nil { + fmt.Println(err) + return err + } + + err = device.Install(args[0], uixt.NewInstallOptions()) + if err != nil { + fmt.Println(err) + return err + } + fmt.Println("success") + return nil + }, +} + +func init() { + installCmd.Flags().StringVarP(&udid, "serial", "s", "", "filter by device's serial") + + iosRootCmd.AddCommand(installCmd) +} diff --git a/hrp/cmd/server.go b/hrp/cmd/server.go new file mode 100644 index 00000000..33c30289 --- /dev/null +++ b/hrp/cmd/server.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/pkg/server" +) + +// serverCmd represents the server command +var serverCmd = &cobra.Command{ + Use: "server start", + Short: "start hrp server", + Long: `start hrp server. exec automation by http`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return server.NewServer(port) + }, +} + +var port int + +func init() { + rootCmd.AddCommand(serverCmd) + serverCmd.Flags().IntVarP(&port, "port", "p", 8082, "Port to run the server on") +} diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 05aefdde..6d6e3e5f 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -1,17 +1,23 @@ package builtin import ( + "bufio" "bytes" + "context" "crypto/hmac" "crypto/sha256" "encoding/binary" "encoding/csv" builtinJSON "encoding/json" "fmt" + "io" "math" "math/rand" "net" + "net/http" + "net/url" "os" + "os/exec" "path/filepath" "reflect" "strconv" @@ -23,6 +29,7 @@ import ( "gopkg.in/yaml.v3" "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -429,3 +436,138 @@ func GetCurrentDay() string { formattedDate := now.Format("20060102") return formattedDate } + +func DownloadFile(filePath string, fileUrl string) error { + log.Info().Str("filePath", filePath).Str("url", fileUrl).Msg("download file") + parsedURL, err := url.Parse(fileUrl) + if err != nil { + return err + } + + out, err := os.Create(filePath) + if err != nil { + return err + } + defer out.Close() + + // 创建一个新的 HTTP 请求 + req, err := http.NewRequest("GET", fileUrl, nil) + if err != nil { + return err + } + + if env.EAPI_TOKEN != "" { + if parsedURL.Host != "gtf-eapi-cn.bytedance.com" && parsedURL.Host != "gtf-eapi-cn.bytedance.net" { + return errors.New("invalid domain: must be gtf-eapi-cn.bytedance.com") + } + // 添加自定义头部 + req.Header.Add("accessKey", "ies.vedem.video") + req.Header.Add("token", env.EAPI_TOKEN) + } + + // 创建一个 HTTP 客户端并发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s, download failed", resp.Status) + } + + // 将响应主体写入文件 + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil +} + +func RunCommand(cmdName string, args ...string) error { + cmd := exec.Command(cmdName, args...) + log.Info().Str("command", cmd.String()).Msg("exec command") + + // print stderr output + var stderr bytes.Buffer + cmd.Stderr = &stderr + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + if err := cmd.Run(); err != nil { + stderrStr := stderr.String() + log.Error().Err(err).Msg("failed to exec command. msg: " + stderrStr) + if stderrStr != "" { + err = errors.Wrap(err, stderrStr) + } + return err + } + stderrStr := stderr.String() + log.Error().Msg("failed to exec command. msg: " + stderrStr) + log.Info().Msg("exec command output: " + stdout.String()) + return nil +} + +type LineCallback func(line string) bool + +// RunCommandWithCallback 运行命令并根据回调判断是否成功 +func RunCommandWithCallback(cmdName string, args []string, callback LineCallback) error { + cmd := exec.Command(cmdName, args...) + log.Info().Str("command", cmd.String()).Msg("exec command") + + // 使用管道获取标准输出 + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + log.Error().Err(err).Msg("failed to get stdout pipe") + return err + } + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + log.Error().Err(err).Msg("failed to start command") + return err + } + + // 创建一个用于标识成功的通道 + done := make(chan struct{}) + defer close(done) + + // 逐行读取 stdout + go func() { + stdoutScanner := bufio.NewScanner(stdoutPipe) + for stdoutScanner.Scan() { + line := stdoutScanner.Text() + log.Info().Msg("stdout: " + line) + if callback(line) { + done <- struct{}{} + return + } + } + }() + + // 等待命令执行完成 + err = cmd.Wait() + if err != nil { + log.Error().Msg("failed to exec command. msg: " + stderr.String()) + return err + } + + // 设置一个1秒的超时上下文 + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + select { + case <-done: + return nil + case <-ctx.Done(): + // 超时,判断失败 + log.Error().Msg("failed to exec command. msg: " + stderr.String()) + err = errors.New("command execution failed: callback failed while exec command") + log.Error().Err(err).Msg("failed to find keyword in time") + return err + } +} diff --git a/hrp/internal/env/env.go b/hrp/internal/env/env.go index 5168990d..2e053e82 100644 --- a/hrp/internal/env/env.go +++ b/hrp/internal/env/env.go @@ -13,6 +13,7 @@ var ( VEDEM_IMAGE_URL = os.Getenv("VEDEM_IMAGE_URL") VEDEM_IMAGE_AK = os.Getenv("VEDEM_IMAGE_AK") VEDEM_IMAGE_SK = os.Getenv("VEDEM_IMAGE_SK") + EAPI_TOKEN = os.Getenv("EAPI_TOKEN") DISABLE_GA = os.Getenv("DISABLE_GA") DISABLE_SENTRY = os.Getenv("DISABLE_SENTRY") PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL") diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 2b5b7c46..7378a3bb 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.9 \ No newline at end of file +v4.6.5 diff --git a/hrp/pkg/gadb/client.go b/hrp/pkg/gadb/client.go index eb6b9d8c..a81ac116 100644 --- a/hrp/pkg/gadb/client.go +++ b/hrp/pkg/gadb/client.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -209,8 +210,8 @@ func (c Client) KillServer() (err error) { return } -func (c Client) createTransport() (tp transport, err error) { - return newTransport(fmt.Sprintf("%s:%d", c.host, c.port)) +func (c Client) createTransport(readTimeout ...time.Duration) (tp transport, err error) { + return newTransport(fmt.Sprintf("%s:%d", c.host, c.port), readTimeout...) } func (c Client) executeCommand(command string, onlyVerifyResponse ...bool) (resp string, err error) { diff --git a/hrp/pkg/gadb/device.go b/hrp/pkg/gadb/device.go index f2d47e83..aba74103 100644 --- a/hrp/pkg/gadb/device.go +++ b/hrp/pkg/gadb/device.go @@ -292,6 +292,33 @@ func (d *Device) ReverseForwardKill(remoteInterface interface{}) error { return err } +func (d *Device) RunStubCommand(command []byte, processName string) (res string, err error) { + var tp transport + if tp, err = d.createDeviceTransport(); err != nil { + return "", err + } + defer func() { _ = tp.Close() }() + + if err = tp.SendWithCheck(fmt.Sprintf("localabstract:%s", processName)); err != nil { + return "", err + } + + if err = tp.SendBytes(command); err != nil { + return "", err + } + + lenBuf, err := tp.ReadBytesN(4) + if err != nil { + return "", err + } + length := binary.LittleEndian.Uint32(lenBuf) + result, err := tp.ReadBytesN(int(length) - 4) + if err != nil { + return "", err + } + return string(result), nil +} + func (d *Device) ReverseForwardKillAll() error { _, err := d.executeCommand("reverse:killforward-all") return err @@ -419,8 +446,8 @@ func (d *Device) EnableAdbOverTCP(port ...int) (err error) { return } -func (d *Device) createDeviceTransport() (tp transport, err error) { - if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port)); err != nil { +func (d *Device) createDeviceTransport(readTimeout ...time.Duration) (tp transport, err error) { + if tp, err = newTransport(fmt.Sprintf("%s:%d", d.adbClient.host, d.adbClient.port), readTimeout...); err != nil { return transport{}, err } @@ -559,7 +586,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byt if err != nil { return nil, err } - if tp, err = d.createDeviceTransport(); err != nil { + if tp, err = d.createDeviceTransport(5 * time.Minute); err != nil { return nil, err } defer func() { _ = tp.Close() }() diff --git a/hrp/pkg/gadb/transport.go b/hrp/pkg/gadb/transport.go index c55b32b7..d09b5c72 100644 --- a/hrp/pkg/gadb/transport.go +++ b/hrp/pkg/gadb/transport.go @@ -17,7 +17,7 @@ import ( var ErrConnBroken = errors.New("socket connection broken") -var DefaultAdbReadTimeout time.Duration = 60 +var DefaultAdbReadTimeout time.Duration = 300 var regexDeviceOffline = regexp.MustCompile("device .* not found") diff --git a/hrp/pkg/server/exception.go b/hrp/pkg/server/exception.go new file mode 100644 index 00000000..592a9066 --- /dev/null +++ b/hrp/pkg/server/exception.go @@ -0,0 +1,15 @@ +package server + +// 常见的错误代码和消息 +const ( + InternalServerErrorCode = 100001 + InternalServerErrorMsg = "Internal Server Error" + + InvalidParamErrorCode = 100002 + InvalidParamErrorMsg = "Invalid %s Param" +) + +const ( + DeviceNotFoundCode = 110001 + DeviceNotFoundMsg = "Device %s Not Found" +) diff --git a/hrp/pkg/server/model.go b/hrp/pkg/server/model.go new file mode 100644 index 00000000..c499c214 --- /dev/null +++ b/hrp/pkg/server/model.go @@ -0,0 +1,52 @@ +package server + +type HttpResponse struct { + Result interface{} `json:"result,omitempty"` + ErrorCode int `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} + +type TapRequest struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Duration float64 `json:"duration"` +} + +type DragRequest struct { + FromX float64 `json:"from_x"` + FromY float64 `json:"from_y"` + ToX float64 `json:"to_x"` + ToY float64 `json:"to_y"` + Duration float64 `json:"duration"` +} + +type InputRequest struct { + Text string `json:"text"` + Frequency int `json:"frequency"` // only iOS +} + +type KeycodeRequest struct { + Keycode int `json:"keycode"` +} + +type AppClearRequest struct { + PackageName string `json:"packageName"` +} + +type AppLaunchRequest struct { + PackageName string `json:"packageName"` +} + +type AppTerminalRequest struct { + PackageName string `json:"packageName"` +} + +type LoginRequest struct { + PackageName string `json:"packageName"` + PhoneNumber string `json:"phoneNumber"` + Captcha string `json:"captcha"` +} + +type LogoutRequest struct { + PackageName string `json:"packageName"` +} diff --git a/hrp/pkg/server/server.go b/hrp/pkg/server/server.go new file mode 100644 index 00000000..7f5fd3a8 --- /dev/null +++ b/hrp/pkg/server/server.go @@ -0,0 +1,558 @@ +package server + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/gadb" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func NewServer(port int) error { + router := gin.Default() + router.GET("/ping", pingHandler) + router.GET("/api/v1/:platform/devices", listDeviceHandler) + router.POST("/api/v1/:platform/:serial/ui/tap", parseDeviceInfo(), tapHandler) + router.POST("/api/v1/:platform/:serial/ui/drag", parseDeviceInfo(), dragHandler) + router.POST("/api/v1/:platform/:serial/ui/input", parseDeviceInfo(), inputHandler) + router.POST("/api/v1/:platform/:serial/key/unlock", parseDeviceInfo(), unlockHandler) + router.POST("/api/v1/:platform/:serial/key/home", parseDeviceInfo(), homeHandler) + router.POST("/api/v1/:platform/:serial/key", parseDeviceInfo(), keycodeHandler) + router.GET("/api/v1/:platform/:serial/app/foreground", parseDeviceInfo(), foregroundAppHandler) + router.POST("/api/v1/:platform/:serial/app/clear", parseDeviceInfo(), clearAppHandler) + router.POST("/api/v1/:platform/:serial/app/launch", parseDeviceInfo(), launchAppHandler) + router.POST("/api/v1/:platform/:serial/app/terminal", parseDeviceInfo(), terminalAppHandler) + router.GET("/api/v1/:platform/:serial/screenshot", parseDeviceInfo(), screenshotHandler) + router.GET("/api/v1/:platform/:serial/stub/source", parseDeviceInfo(), sourceHandler) + router.GET("/api/v1/:platform/:serial/adb/source", parseDeviceInfo(), adbSourceHandler) + router.POST("/api/v1/:platform/:serial/stub/login", parseDeviceInfo(), loginHandler) + router.POST("/api/v1/:platform/:serial/stub/logout", parseDeviceInfo(), logoutHandler) + + err := router.Run(fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + log.Err(err).Msg("failed to start http server") + return err + } + return nil +} + +func pingHandler(c *gin.Context) { + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func listDeviceHandler(c *gin.Context) { + platform := c.Param("platform") + switch strings.ToLower(platform) { + case "android": + { + client, err := gadb.NewClient() + if err != nil { + log.Err(err).Msg("failed to init adb client") + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + devices, err := client.DeviceList() + if err != nil && strings.Contains(err.Error(), "no android device found") { + c.JSON(http.StatusOK, HttpResponse{Result: nil}) + return + } else if err != nil { + log.Err(err).Msg("failed to list devices") + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + var deviceList []interface{} + for _, device := range devices { + brand, err := device.Brand() + if err != nil { + log.Err(err).Msg("failed to get device brand") + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + model, err := device.Model() + if err != nil { + log.Err(err).Msg("failed to get device model") + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + deviceInfo := map[string]interface{}{ + "serial": device.Serial(), + "brand": brand, + "model": model, + "platform": "android", + } + deviceList = append(deviceList, deviceInfo) + } + c.JSON(http.StatusOK, HttpResponse{Result: deviceList}) + return + } + default: + { + c.JSON(http.StatusBadRequest, HttpResponse{ + ErrorCode: InvalidParamErrorCode, + ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"), + }) + c.Abort() + return + } + } +} + +func tapHandler(c *gin.Context) { + var tapReq TapRequest + if err := c.ShouldBindJSON(&tapReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + if tapReq.X < 1 && tapReq.Y < 1 { + err := dExt.TapXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + } else { + err := dExt.TapAbsXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func dragHandler(c *gin.Context) { + var dragReq DragRequest + if err := c.ShouldBindJSON(&dragReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + if dragReq.FromX < 1 && dragReq.FromY < 1 && dragReq.ToX < 1 && dragReq.ToY < 1 { + err := dExt.SwipeRelative(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + } else { + err := dExt.Driver.SwipeFloat(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func inputHandler(c *gin.Context) { + var inputReq InputRequest + if err := c.ShouldBindJSON(&inputReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.SendKeys(inputReq.Text, uixt.WithFrequency(inputReq.Frequency)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input text %s", c.HandlerName(), inputReq.Text)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func unlockHandler(c *gin.Context) { + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.Unlock() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func homeHandler(c *gin.Context) { + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.Homescreen() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to enter homescreen", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func keycodeHandler(c *gin.Context) { + var keycodeReq KeycodeRequest + if err := c.ShouldBindJSON(&keycodeReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.PressKeyCode(uixt.KeyCode(keycodeReq.Keycode)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input keycode %d", c.HandlerName(), keycodeReq.Keycode)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func foregroundAppHandler(c *gin.Context) { + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + appInfo, err := dExt.Driver.GetForegroundApp() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: appInfo}) +} + +func clearAppHandler(c *gin.Context) { + var appClearReq AppClearRequest + if err := c.ShouldBindJSON(&appClearReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.Clear(appClearReq.PackageName) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func launchAppHandler(c *gin.Context) { + var appLaunchReq AppLaunchRequest + if err := c.ShouldBindJSON(&appLaunchReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.AppLaunch(appLaunchReq.PackageName) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appLaunchReq.PackageName)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func terminalAppHandler(c *gin.Context) { + var appTerminalReq AppTerminalRequest + if err := c.ShouldBindJSON(&appTerminalReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + success, err := dExt.Driver.AppTerminate(appTerminalReq.PackageName) + if !success { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appTerminalReq.PackageName)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func screenshotHandler(c *gin.Context) { + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + raw, err := dExt.Driver.Screenshot() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get screenshot", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + + c.JSON(http.StatusOK, HttpResponse{Result: base64.StdEncoding.EncodeToString(raw.Bytes())}) +} + +func sourceHandler(c *gin.Context) { + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + app, err := dExt.Driver.GetForegroundApp() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + source, err := dExt.Driver.Source(uixt.NewSourceOption().WithProcessName(app.PackageName)) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get source %s", c.HandlerName(), app.PackageName)) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + + c.JSON(http.StatusOK, HttpResponse{Result: source}) +} + +func adbSourceHandler(c *gin.Context) { + deviceObj, exists := c.Get("device") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "device")}) + c.Abort() + return + } + device := deviceObj.(*uixt.AndroidDevice) + driver, err := device.NewAdbDriver() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to new adb driver", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + source, err := driver.Source() + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get adb source", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: source}) +} + +func loginHandler(c *gin.Context) { + var loginReq LoginRequest + if err := c.ShouldBindJSON(&loginReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to login", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func logoutHandler(c *gin.Context) { + var logoutReq LogoutRequest + if err := c.ShouldBindJSON(&logoutReq); err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")}) + c.Abort() + return + } + + driverObj, exists := c.Get("driver") + if !exists { + log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")}) + c.Abort() + return + } + + dExt := driverObj.(*uixt.DriverExt) + err := dExt.Driver.LogoutNoneUI(logoutReq.PackageName) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("[%s]: failed to login", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg}) + c.Abort() + return + } + c.JSON(http.StatusOK, HttpResponse{Result: true}) +} + +func parseDeviceInfo() gin.HandlerFunc { + return func(c *gin.Context) { + platform := c.Param("platform") + switch strings.ToLower(platform) { + case "android": + serial := c.Param("serial") + if serial == "" { + log.Error().Str("platform", platform).Msg(fmt.Sprintf("[%s]: serial is empty", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{ + ErrorCode: InvalidParamErrorCode, + ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "serial"), + }) + c.Abort() + return + } + device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial), uixt.WithStub(true)) + if err != nil { + log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Device Not Found", c.HandlerName())) + c.JSON(http.StatusBadRequest, HttpResponse{ + ErrorCode: DeviceNotFoundCode, + ErrorMsg: fmt.Sprintf(DeviceNotFoundMsg, serial), + }) + c.Abort() + return + } + c.Set("device", device) + driver, err := device.NewDriver(uixt.WithDriverImageService(false), uixt.WithDriverResultFolder(false)) + if err != nil { + log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Failed New Driver", c.HandlerName())) + c.JSON(http.StatusInternalServerError, HttpResponse{ + ErrorCode: InternalServerErrorCode, + ErrorMsg: err.Error(), + }) + c.Abort() + return + } + c.Set("driver", driver) + default: + c.JSON(http.StatusBadRequest, HttpResponse{ + ErrorCode: InvalidParamErrorCode, + ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"), + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/hrp/pkg/uixt/action.go b/hrp/pkg/uixt/action.go index 79b893e3..8b26e0e3 100644 --- a/hrp/pkg/uixt/action.go +++ b/hrp/pkg/uixt/action.go @@ -17,6 +17,7 @@ type ActionMethod string const ( ACTION_AppInstall ActionMethod = "install" ACTION_AppUninstall ActionMethod = "uninstall" + ACTION_AppClear ActionMethod = "app_clear" ACTION_AppStart ActionMethod = "app_start" ACTION_AppLaunch ActionMethod = "app_launch" // 启动 app 并堵塞等待 app 首屏加载完成 ACTION_AppTerminate ActionMethod = "app_terminate" @@ -26,6 +27,10 @@ const ( ACTION_SleepRandom ActionMethod = "sleep_random" ACTION_StartCamera ActionMethod = "camera_start" // alias for app_launch camera ACTION_StopCamera ActionMethod = "camera_stop" // alias for app_terminate camera + ACTION_SetClipboard ActionMethod = "set_clipboard" + ACTION_GetClipboard ActionMethod = "get_clipboard" + ACTION_SetIme ActionMethod = "set_ime" + ACTION_GetSource ActionMethod = "get_source" // UI validation // selectors @@ -60,6 +65,9 @@ const ( ACTION_VideoCrawler ActionMethod = "video_crawler" ACTION_ClosePopups ActionMethod = "close_popups" ACTION_EndToEndDelay ActionMethod = "live_e2e" + ACTION_InstallApp ActionMethod = "install_app" + ACTION_UninstallApp ActionMethod = "uninstall_app" + ACTION_DownloadApp ActionMethod = "download_app" ) type MobileAction struct { @@ -554,8 +562,23 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) { switch action.Method { case ACTION_AppInstall: - // TODO - return errActionNotImplemented + if appUrl, ok := action.Params.(string); ok { + if err = dExt.InstallByUrl(appUrl, NewInstallOptions(WithRetryTime(action.MaxRetryTimes))); err != nil { + return errors.Wrap(err, "failed to install app") + } + } + case ACTION_AppUninstall: + if packageName, ok := action.Params.(string); ok { + if err = dExt.Uninstall(packageName, action.GetOptions()...); err != nil { + return errors.Wrap(err, "failed to uninstall app") + } + } + case ACTION_AppClear: + if packageName, ok := action.Params.(string); ok { + if err = dExt.Driver.Clear(packageName); err != nil { + return errors.Wrap(err, "failed to clear app") + } + } case ACTION_AppLaunch: if bundleId, ok := action.Params.(string); ok { return dExt.Driver.AppLaunch(bundleId) @@ -594,8 +617,34 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) { return nil } return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) + case ACTION_SetClipboard: + if text, ok := action.Params.(string); ok { + err := dExt.Driver.SetPasteboard(PasteboardTypePlaintext, text) + if err != nil { + return errors.Wrap(err, "failed to set clipboard") + } + return nil + } + return fmt.Errorf("set_clioboard params should be text(string), got %v", action.Params) case ACTION_Home: return dExt.Driver.Homescreen() + case ACTION_SetIme: + if ime, ok := action.Params.(string); ok { + err = dExt.Driver.SetIme(ime) + if err != nil { + return errors.Wrap(err, "failed to set ime") + } + return nil + } + case ACTION_GetSource: + if packageName, ok := action.Params.(string); ok { + source := NewSourceOption().WithProcessName(packageName) + _, err = dExt.Driver.Source(source) + if err != nil { + return errors.Wrap(err, "failed to set ime") + } + return nil + } case ACTION_TapXY: if location, ok := action.Params.([]interface{}); ok { // relative x,y of window size: [0.5, 0.5] diff --git a/hrp/pkg/uixt/android_adb_driver.go b/hrp/pkg/uixt/android_adb_driver.go index c642a245..cec901b5 100644 --- a/hrp/pkg/uixt/android_adb_driver.go +++ b/hrp/pkg/uixt/android_adb_driver.go @@ -3,6 +3,7 @@ package uixt import ( "bufio" "bytes" + "encoding/json" "encoding/xml" "fmt" "io/fs" @@ -218,10 +219,18 @@ func (ad *adbDriver) Orientation() (orientation Orientation, err error) { } func (ad *adbDriver) Homescreen() (err error) { - return ad.PressKeyCode(KCHome, KMEmpty) + return ad.PressKeyCodes(KCHome, KMEmpty) } -func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error) { +func (ad *adbDriver) Unlock() (err error) { + return ad.PressKeyCodes(KCMenu, KMEmpty) +} + +func (ad *adbDriver) PressKeyCode(keyCode KeyCode) (err error) { + return ad.PressKeyCodes(keyCode, KMEmpty) +} + +func (ad *adbDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta) (err error) { // adb shell input keyevent _, err = ad.adbClient.RunShellCommand( "input", "keyevent", fmt.Sprintf("%d", keyCode)) @@ -421,6 +430,14 @@ func (ad *adbDriver) IsUnicodeIMEInstalled() bool { return strings.Contains(output, UnicodeImePackageName) } +func (ad *adbDriver) ListIme() []string { + output, err := ad.adbClient.RunShellCommand("ime", "list", "-s") + if err != nil { + return []string{} + } + return strings.Split(output, "\n") +} + func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) { defer func() { // Reset to default, don't care which keyboard was chosen before switch: @@ -458,6 +475,15 @@ func (ad *adbDriver) Input(text string, options ...ActionOption) (err error) { return ad.SendKeys(text, options...) } +func (ad *adbDriver) Clear(packageName string) error { + if _, err := ad.adbClient.RunShellCommand("pm", "clear", packageName); err != nil { + log.Error().Str("packageName", packageName).Err(err).Msg("failed to clear package cache") + return err + } + + return nil +} + func (ad *adbDriver) PressButton(devBtn DeviceButton) (err error) { err = errDriverNotImplemented return @@ -499,6 +525,14 @@ func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) { return } +func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error { + return errDriverNotImplemented +} + +func (ad *adbDriver) LogoutNoneUI(packageName string) error { + return errDriverNotImplemented +} + func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) { source, err := ad.Source() if err != nil { @@ -680,46 +714,57 @@ func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) { return pointRes, nil } +func (ad *adbDriver) GetDriverResults() []*DriverResult { + return nil +} + func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) { - // adb shell dumpsys activity activities - output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities") + packageInfo, err := ad.adbClient.RunShellCommand("CLASSPATH=/data/local/tmp/evalite", "app_process", "/", "com.bytedance.iesqa.eval_process.PackageService") if err != nil { - log.Error().Err(err).Msg("failed to dumpsys activities") - return AppInfo{}, errors.Wrap(err, "dumpsys activities failed") + return app, err + } + log.Info().Msg(packageInfo) + err = json.Unmarshal([]byte(strings.TrimSpace(packageInfo)), &app) + return +} + +func (ad *adbDriver) SetIme(imeRegx string) error { + imeList := ad.ListIme() + ime := "" + for _, imeName := range imeList { + if regexp.MustCompile(imeRegx).MatchString(imeName) { + ime = imeName + break + } + } + if ime == "" { + return fmt.Errorf("failed to set ime by %s, ime list: %v", imeRegx, imeList) + } + brand, _ := ad.adbClient.Brand() + packageName := strings.Split(ime, "/")[0] + res, err := ad.adbClient.RunShellCommand("ime", "set", ime) + log.Info().Str("funcName", "SetIme").Interface("ime", ime). + Interface("output", res).Msg("set ime") + if err != nil { + return err } - lines := strings.Split(string(output), "\n") - for _, line := range lines { - trimmedLine := strings.TrimSpace(line) - // grep mResumedActivity|ResumedActivity - if strings.HasPrefix(trimmedLine, "mResumedActivity:") || strings.HasPrefix(trimmedLine, "ResumedActivity:") { - // mResumedActivity: ActivityRecord{9656d74 u0 com.android.settings/.Settings t407} - // ResumedActivity: ActivityRecord{8265c25 u0 com.android.settings/.Settings t73} - strs := strings.Split(trimmedLine, " ") - for _, str := range strs { - if strings.Contains(str, "/") { - // com.android.settings/.Settings - s := strings.Split(str, "/") - app := AppInfo{ - AppBaseInfo: AppBaseInfo{ - PackageName: s[0], - Activity: s[1], - }, - } - return app, nil + if strings.ToLower(brand) == "oppo" { + time.Sleep(1 * time.Second) + pid, _ := ad.adbClient.RunShellCommand("pidof", packageName) + if strings.TrimSpace(pid) == "" { + appInfo, err := ad.GetForegroundApp() + _ = ad.AppLaunch(packageName) + if err == nil && packageName != UnicodeImePackageName { + time.Sleep(10 * time.Second) + nextAppInfo, err := ad.GetForegroundApp() + log.Info().Str("beforeFocusedPackage", appInfo.PackageName).Str("afterFocusedPackage", nextAppInfo.PackageName).Msg("") + if err == nil && nextAppInfo.PackageName != appInfo.PackageName { + _ = ad.PressKeyCodes(KCBack, KMEmpty) } } } } - - return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed") -} - -func (ad *adbDriver) SetIme(ime string) error { - _, err := ad.adbClient.RunShellCommand("ime", "set", ime) - if err != nil { - return err - } // even if the shell command has returned, // as there might be a situation where the input method has not been completely switched yet // Listen to the following message. diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index bc7a5e25..87c54671 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -4,26 +4,43 @@ import ( "bufio" "bytes" "context" + "crypto/md5" + "embed" + "encoding/base64" + "encoding/hex" "fmt" + "io" + "os" "os/exec" + "regexp" + "strconv" "strings" + "time" "github.com/httprunner/funplugin/myexec" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/code" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/pkg/gadb" ) var ( - AdbServerHost = "localhost" - AdbServerPort = gadb.AdbServerPort // 5037 - UIA2ServerHost = "localhost" - UIA2ServerPort = 6790 + DouyinServerPort = 32316 + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerHost = "localhost" + UIA2ServerPort = 6790 + EvalInstallerPackageName = "sogou.mobile.explorer" + InstallViaInstallerCommand = "am start -S -n sogou.mobile.explorer/.PackageInstallerActivity -d" ) +//go:embed evalite +var evalite embed.FS + const forwardToPrefix = "forward-to-" type AndroidDeviceOption func(*AndroidDevice) @@ -40,6 +57,12 @@ func WithUIA2(uia2On bool) AndroidDeviceOption { } } +func WithStub(stubOn bool) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.STUB = stubOn + } +} + func WithUIA2IP(ip string) AndroidDeviceOption { return func(device *AndroidDevice) { device.UIA2IP = ip @@ -108,7 +131,14 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er device.d = dev device.logcat = NewAdbLogcat(device.SerialNumber) - + evalToolRaw, err := evalite.ReadFile("evalite") + if err != nil { + return nil, errors.Wrap(code.LoadFileError, err.Error()) + } + err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/evalite", time.Now()) + if err != nil { + return nil, errors.Wrap(code.AndroidShellExecError, err.Error()) + } log.Info().Str("serial", device.SerialNumber).Msg("init android device") return device, nil } @@ -151,9 +181,18 @@ type AndroidDevice struct { logcat *AdbLogcat SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2 + STUB bool `json:"stub,omitempty" yaml:"stub,omitempty"` // use uiautomator2 UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` +} + +func (dev *AndroidDevice) Init() error { + myexec.RunCommand("adb", "-s", dev.SerialNumber, "shell", + "ime", "enable", "io.appium.settings/.UnicodeIME") + myexec.RunCommand("adb", "-s", dev.SerialNumber, "shell", "rm", "-r", env.DeviceActionLogFilePath) + return nil } func (dev *AndroidDevice) System() string { @@ -169,7 +208,7 @@ func (dev *AndroidDevice) LogEnabled() bool { } func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) { - driverOptions := &DriverOptions{} + driverOptions := NewDriverOptions() for _, option := range options { option(driverOptions) } @@ -177,6 +216,8 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE var driver WebDriver if dev.UIA2 || dev.LogOn { driver, err = dev.NewUSBDriver(driverOptions.capabilities) + } else if dev.STUB { + driver, err = dev.NewStubDriver(driverOptions.capabilities) } else { driver, err = dev.NewAdbDriver() } @@ -184,7 +225,7 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE return nil, errors.Wrap(err, "failed to init UIA driver") } - driverExt, err = newDriverExt(dev, driver, driverOptions.plugin) + driverExt, err = newDriverExt(dev, driver, options...) if err != nil { return nil, err } @@ -221,6 +262,36 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDri return uiaDriver, nil } +func (dev *AndroidDevice) NewStubDriver(capabilities Capabilities) (driver *stubAndroidDriver, err error) { + socketLocalPort, err := dev.d.Forward(StubSocketName) + if err != nil { + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("forward port %d->%s failed: %v", + socketLocalPort, StubSocketName, err)) + } + + serverLocalPort, err := dev.d.Forward(DouyinServerPort) + if err != nil { + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("forward port %d->%d failed: %v", + serverLocalPort, DouyinServerPort, err)) + } + + rawURL := fmt.Sprintf("http://%s%d:%d", + forwardToPrefix, serverLocalPort, DouyinServerPort) + + stubDriver, err := newStubAndroidDriver(fmt.Sprintf("127.0.0.1:%d", socketLocalPort), rawURL) + if err != nil { + _ = dev.d.ForwardKill(socketLocalPort) + _ = dev.d.ForwardKill(serverLocalPort) + return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error()) + } + stubDriver.adbClient = dev.d + stubDriver.logcat = dev.logcat + + return stubDriver, nil +} + // NewHTTPDriver creates new remote HTTP client, this will also start a new session. func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.UIA2IP, dev.UIA2Port) @@ -261,6 +332,116 @@ func (dev *AndroidDevice) StopPcap() string { return "" } +func (dev *AndroidDevice) Uninstall(packageName string) error { + return myexec.RunCommand("adb", "-s", dev.SerialNumber, "uninstall", packageName) +} + +func (dev *AndroidDevice) Install(appPath string, opts *InstallOptions) error { + app, err := os.Open(appPath) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("install %s open file failed", appPath)) + } + + defer app.Close() + brand, err := dev.d.Brand() + if err != nil { + return err + } + args := []string{} + if opts.Reinstall { + args = append(args, "-r") + } + if opts.GrantPermission { + args = append(args, "-g") + } + if opts.Downgrade { + args = append(args, "-d") + } + switch strings.ToLower(brand) { + case "vivo": + return dev.installVivoSilent(app, args...) + case "oppo", "realme", "oneplus": + if dev.d.IsPackagesInstalled(EvalInstallerPackageName) { + return dev.installViaInstaller(app, args...) + } + log.Warn().Msg("oppo not install eval installer") + return dev.installCommon(app, args...) + default: + return dev.installCommon(app, args...) + } +} + +func (dev *AndroidDevice) installVivoSilent(app io.ReadSeeker, args ...string) error { + currentTime := builtin.GetCurrentDay() + md5HashInBytes := md5.Sum([]byte(currentTime)) + verifyCode := hex.EncodeToString(md5HashInBytes[:]) + verifyCode = base64.StdEncoding.EncodeToString([]byte(verifyCode)) + verifyCode = verifyCode[:8] + verifyCode = "-V" + verifyCode + args = append([]string{verifyCode}, args...) + _, err := dev.d.InstallAPK(app, args...) + return err +} + +func (dev *AndroidDevice) installViaInstaller(app io.ReadSeeker, args ...string) error { + appRemotePath := "/data/local/tmp/" + strconv.FormatInt(time.Now().UnixMilli(), 10) + ".apk" + err := dev.d.Push(app, appRemotePath, time.Now()) + if err != nil { + return err + } + done := make(chan error) + defer func() { + close(done) + }() + logcat := NewAdbLogcatWithCallback(dev.d.Serial(), func(line string) { + re := regexp.MustCompile(`\{.*?}`) + match := re.FindString(line) + if match == "" { + return + } + var result InstallResult + err := json.Unmarshal([]byte(match), &result) + if err != nil { + log.Warn().Msg("parse Install msg line error: " + match) + return + } + if result.Result == 0 { + // 安装成功 + done <- nil + } else { + done <- errors.New(match) + } + }) + err = logcat.CatchLogcat("PackageInstallerCallback") + if err != nil { + return err + } + defer func() { + _ = logcat.Stop() + }() + + // 需要监听是否完成安装 + command := strings.Split(InstallViaInstallerCommand, " ") + args = append(command, appRemotePath) + _, err = dev.d.RunShellCommand("am", args[1:]...) + if err != nil { + return err + } + // 等待安装完成或超时 + timeout := 3 * time.Minute + select { + case err := <-done: + return err + case <-time.After(timeout): + return fmt.Errorf("installation timed out after %v", timeout) + } +} + +func (dev *AndroidDevice) installCommon(app io.ReadSeeker, args ...string) error { + _, err := dev.d.InstallAPK(app, args...) + return err +} + type LineCallback func(string) type AdbLogcat struct { diff --git a/hrp/pkg/uixt/android_stub_driver.go b/hrp/pkg/uixt/android_stub_driver.go new file mode 100644 index 00000000..2f92a436 --- /dev/null +++ b/hrp/pkg/uixt/android_stub_driver.go @@ -0,0 +1,273 @@ +package uixt + +import ( + "errors" + "fmt" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +type stubAndroidDriver struct { + socket net.Conn + seq int + timeout time.Duration + adbDriver +} + +const StubSocketName = "com.bytest.device" + +// newStubAndroidDriver +// 创建stub Driver address为forward后的端口格式127.0.0.1:${port} +func newStubAndroidDriver(address string, urlPrefix string, readTimeout ...time.Duration) (*stubAndroidDriver, error) { + timeout := 10 * time.Second + if len(readTimeout) > 0 { + timeout = readTimeout[0] + } + + conn, err := net.Dial("tcp", address) + if err != nil { + log.Err(err).Msg(fmt.Sprintf("failed to connect %s", address)) + return nil, err + } + + driver := &stubAndroidDriver{ + socket: conn, + timeout: timeout, + } + + if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { + return nil, err + } + + return driver, nil +} + +func (sad *stubAndroidDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + var localPort int + { + tmpURL, _ := url.Parse(sad.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) + } + } + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + sad.client = convertToHTTPClient(conn) + return sad.httpRequest(http.MethodGet, sad.concatURL(nil, pathElem...), nil) +} + +func (sad *stubAndroidDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var localPort int + { + tmpURL, _ := url.Parse(sad.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) + } + } + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + sad.client = convertToHTTPClient(conn) + + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return sad.httpRequest(http.MethodPost, sad.concatURL(nil, pathElem...), bsJSON) +} + +func (sad *stubAndroidDriver) NewSession(capabilities Capabilities) (SessionInfo, error) { + return SessionInfo{}, errDriverNotImplemented +} + +func (sad *stubAndroidDriver) sendCommand(packageName string, cmdType string, params map[string]interface{}, readTimeout ...time.Duration) (interface{}, error) { + sad.seq++ + packet := map[string]interface{}{ + "Seq": sad.seq, + "Cmd": cmdType, + "v": "", + } + for key, value := range params { + if key == "Cmd" || key == "Seq" { + return "", errors.New("params cannot be Cmd or Seq") + } + packet[key] = value + } + data, err := json.Marshal(packet) + if err != nil { + return nil, err + } + + res, err := sad.adbClient.RunStubCommand(append(data, '\n'), packageName) + if err != nil { + return nil, err + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(res), &resultMap); err != nil { + return nil, err + } + if resultMap["Error"] != nil { + return nil, fmt.Errorf("failed to call stub command: %s", resultMap["Error"].(string)) + } + + return resultMap["Result"], nil +} + +func (sad *stubAndroidDriver) DeleteSession() error { + return sad.close() +} + +func (sad *stubAndroidDriver) close() error { + if sad.socket != nil { + return sad.socket.Close() + } + return nil +} + +func (sad *stubAndroidDriver) Status() (DeviceStatus, error) { + app, err := sad.GetForegroundApp() + if err != nil { + return DeviceStatus{}, err + } + res, err := sad.sendCommand(app.PackageName, "Hello", nil) + if err != nil { + return DeviceStatus{}, err + } + log.Info().Msg(fmt.Sprintf("ping stub result :%v", res)) + return DeviceStatus{}, nil +} + +func (sad *stubAndroidDriver) Source(srcOpt ...SourceOption) (source string, err error) { + app, err := sad.GetForegroundApp() + if err != nil { + return "", err + } + params := map[string]interface{}{ + "ClassName": "com.bytedance.byteinsight.MockOperator", + "Method": "getLayout", + "RetType": "", + "Args": []string{}, + } + res, err := sad.sendCommand(app.PackageName, "CallStaticMethod", params) + if err != nil { + return "", err + } + return res.(string), nil +} + +func (sad *stubAndroidDriver) LoginNoneUIBak(packageName, phoneNumber, captcha string) error { + _, err := sad.adbClient.RunShellCommand("am", "broadcast", "-a", fmt.Sprintf("%s.util.crony.action_login", packageName), "-e", "phone", phoneNumber, "-e", "code", captcha) + time.Sleep(10 * time.Second) + login, err := sad.isLogin(packageName) + if err != nil || !login { + log.Err(err).Msg("failed to login") + return fmt.Errorf("failed to login") + } + return err +} + +func (sad *stubAndroidDriver) LoginNoneUI(packageName, phoneNumber, captcha string) error { + params := map[string]interface{}{ + "phone": phoneNumber, + "code": captcha, + } + resp, err := sad.httpPOST(params, "/host", "/login", "account") + if err != nil { + return err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to logout %s", res["data"]) + log.Err(err).Msgf("%v", res) + return err + } + time.Sleep(10 * time.Second) + login, err := sad.isLogin(packageName) + if err != nil { + return err + } + if !login { + return fmt.Errorf("failed to login") + } + return nil +} + +func (sad *stubAndroidDriver) LogoutNoneUI(packageName string) error { + resp, err := sad.httpGET("/host", "/logout") + if err != nil { + return err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to logout %s", res["data"]) + log.Err(err).Msgf("%v", res) + return err + } + fmt.Printf("%v", resp) + if err != nil { + return err + } + return nil +} + +func (sad *stubAndroidDriver) LoginNoneUIDynamic(packageName, phoneNumber string, captcha string) error { + params := map[string]interface{}{ + "ClassName": "qe.python.test.LoginUtil", + "Method": "loginSync", + "RetType": "", + "Args": []string{phoneNumber, captcha}, + } + res, err := sad.sendCommand(packageName, "CallStaticMethod", params) + if err != nil { + return err + } + log.Info().Msg(res.(string)) + return nil +} + +func (sad *stubAndroidDriver) isLogin(packageName string) (login bool, err error) { + resp, err := sad.httpGET("/host", "/login", "/check") + if err != nil { + return false, err + } + res, err := resp.valueConvertToJsonObject() + if err != nil { + return false, err + } + log.Info().Msgf("%v", res) + if res["isSuccess"] != true { + err = fmt.Errorf("falied to get is login %s", res["data"]) + log.Err(err).Msgf("%v", res) + return false, err + } + fmt.Printf("%v", resp) + if err != nil { + return false, err + } + return true, nil +} diff --git a/hrp/pkg/uixt/android_stub_driver_test.go b/hrp/pkg/uixt/android_stub_driver_test.go new file mode 100644 index 00000000..2d5cd3b3 --- /dev/null +++ b/hrp/pkg/uixt/android_stub_driver_test.go @@ -0,0 +1,56 @@ +package uixt + +import "testing" + +var androidStubDriver *stubAndroidDriver + +func setupStubDriver(t *testing.T) { + device, err := NewAndroidDevice() + checkErr(t, err) + device.STUB = true + androidStubDriver, err = device.NewStubDriver(Capabilities{}) + checkErr(t, err) +} + +func TestHello(t *testing.T) { + setupStubDriver(t) + status, err := androidStubDriver.Status() + if err != nil { + t.Fatal(err) + } + t.Log(status) +} + +func TestSource(t *testing.T) { + setupStubDriver(t) + source, err := androidStubDriver.Source() + if err != nil { + t.Fatal(err) + } + t.Log(source) +} + +func TestIsLogin(t *testing.T) { + setupStubDriver(t) + res, err := androidStubDriver.isLogin("com.ss.android.ugc.aweme") + if err != nil { + t.Fatal(err) + } + t.Log(res) +} + +func TestLogin(t *testing.T) { + setupStubDriver(t) + err := androidStubDriver.LoginNoneUI("com.ss.android.ugc.aweme", "12342316231", "8517") + if err != nil { + t.Fatal(err) + } +} + +func TestLogout(t *testing.T) { + setupStubDriver(t) + err := androidStubDriver.LogoutNoneUI("com.ss.android.ugc.aweme") + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/uixt/android_test.go b/hrp/pkg/uixt/android_test.go index f534feca..0d23c1e5 100644 --- a/hrp/pkg/uixt/android_test.go +++ b/hrp/pkg/uixt/android_test.go @@ -3,8 +3,7 @@ package uixt import ( - "encoding/json" - "fmt" + "io/ioutil" "os" "strings" "testing" @@ -21,8 +20,8 @@ var ( func setupAndroid(t *testing.T) { device, err := NewAndroidDevice() checkErr(t, err) - device.UIA2 = false - device.LogOn = true + device.UIA2 = true + device.LogOn = false driverExt, err = device.NewDriver() checkErr(t, err) } @@ -124,12 +123,9 @@ func TestDriver_DeviceSize(t *testing.T) { } func TestDriver_Source(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } + setupAndroid(t) - source, err := driver.Source() + source, err := driverExt.Driver.Source() if err != nil { t.Fatal(err) } @@ -199,33 +195,24 @@ func TestDriver_DeviceInfo(t *testing.T) { func TestDriver_Tap(t *testing.T) { setupAndroid(t) driverExt.Driver.StartCaptureLog("") - err := driverExt.Driver.Tap(150, 340, WithIdentifier("test")) + err := driverExt.TapXY(0.5, 0.5, WithIdentifier("test"), WithPressDuration(4)) if err != nil { t.Fatal(err) } - time.Sleep(time.Second) - - err = driverExt.Driver.TapFloat(60.5, 125.5, WithIdentifier("test")) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - result, _ := driverExt.Driver.StopCaptureLog() - t.Log(result) + //time.Sleep(time.Second) + // + //err = driverExt.Driver.TapFloat(60.5, 125.5, WithIdentifier("test")) + //if err != nil { + // t.Fatal(err) + //} + //time.Sleep(time.Second) + //result, _ := driverExt.Driver.StopCaptureLog() + //t.Log(result) } func TestDriver_Swipe(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.Swipe(400, 1000, 400, 500, WithPressDuration(2000)) - if err != nil { - t.Fatal(err) - } - - err = driver.SwipeFloat(400, 555.5, 400, 1255.5) + setupAndroid(t) + err := driverExt.Driver.Swipe(400, 1000, 400, 500, WithPressDuration(0.5)) if err != nil { t.Fatal(err) } @@ -261,7 +248,7 @@ func TestDriver_Drag(t *testing.T) { func TestDriver_SendKeys(t *testing.T) { setupAndroid(t) - err := driverExt.Driver.SendKeys("Android\"输入速度测试", WithIdentifier("test")) + err := driverExt.Driver.SendKeys("辽宁省沈阳市新民市民族街36-4", WithIdentifier("test")) if err != nil { t.Fatal(err) } @@ -306,6 +293,14 @@ func TestDriver_SetRotation(t *testing.T) { } } +func TestDriver_GetOrientation(t *testing.T) { + setupAndroid(t) + _, _ = driverExt.Driver.AppTerminate("com.quark.browser") + _ = driverExt.Driver.AppLaunch("com.quark.browser") + time.Sleep(2 * time.Second) + _ = driverExt.Driver.Homescreen() +} + func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { uiSelector := NewUiSelectorHelper().Text("a").String() if uiSelector != `new UiSelector().text("a");` { @@ -448,8 +443,6 @@ func TestConvertPoints(t *testing.T) { if len(eps) != 3 { t.Fatal() } - jsons, _ := json.Marshal(eps) - println(fmt.Sprintf("%v", string(jsons))) } func TestDriver_ShellInputUnicode(t *testing.T) { diff --git a/hrp/pkg/uixt/android_uia2_driver.go b/hrp/pkg/uixt/android_uia2_driver.go index 6606cd78..c7d7056e 100644 --- a/hrp/pkg/uixt/android_uia2_driver.go +++ b/hrp/pkg/uixt/android_uia2_driver.go @@ -29,6 +29,7 @@ func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDrive log.Info().Msg("init uiautomator2 driver") if capabilities == nil { capabilities = NewCapabilities() + capabilities.WithWaitForIdleTimeout(0) } driver = new(uiaDriver) if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { @@ -141,7 +142,12 @@ func (ud *uiaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err er func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { // register(postHandler, new NewSession("/wd/hub/session")) var rawResp rawResponse - data := map[string]interface{}{"capabilities": capabilities} + data := make(map[string]interface{}) + if len(capabilities) == 0 { + data["capabilities"] = make(map[string]interface{}) + } else { + data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities} + } if rawResp, err = ud.Driver.httpPOST(data, "/session"); err != nil { return SessionInfo{SessionId: ""}, err } @@ -244,10 +250,14 @@ func (ud *uiaDriver) PressBack(options ...ActionOption) (err error) { } func (ud *uiaDriver) Homescreen() (err error) { - return ud.PressKeyCode(KCHome, KMEmpty) + return ud.PressKeyCodes(KCHome, KMEmpty) } -func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { +func (ud *uiaDriver) PressKeyCode(keyCode KeyCode) (err error) { + return ud.PressKeyCodes(keyCode, KMEmpty) +} + +func (ud *uiaDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { // register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode")) data := map[string]interface{}{ "keycode": keyCode, @@ -293,7 +303,7 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error) duration := 100.0 if actionOptions.PressDuration > 0 { - duration = actionOptions.PressDuration + duration = actionOptions.PressDuration * 1000 } data := map[string]interface{}{ "actions": []interface{}{ @@ -399,7 +409,7 @@ func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...Actio duration := 200.0 if actionOptions.PressDuration > 0 { - duration = actionOptions.PressDuration + duration = actionOptions.PressDuration * 1000 } data := map[string]interface{}{ "actions": []interface{}{ @@ -618,3 +628,10 @@ func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error { } return nil } + +func (ud *uiaDriver) GetDriverResults() []*DriverResult { + defer func() { + ud.Driver.driverResults = nil + }() + return ud.Driver.driverResults +} diff --git a/hrp/pkg/uixt/client.go b/hrp/pkg/uixt/client.go index e680f6f9..5f1feeae 100644 --- a/hrp/pkg/uixt/client.go +++ b/hrp/pkg/uixt/client.go @@ -17,10 +17,18 @@ import ( ) type Driver struct { - urlPrefix *url.URL - sessionId string - client *http.Client - scale float64 + urlPrefix *url.URL + sessionId string + client *http.Client + scale float64 + driverResults []*DriverResult +} + +type DriverResult struct { + RequestUrl string `json:"request_driver_url"` + RequestBody string `json:"request_driver_body,omitempty"` + RequestDuration time.Duration `json:"request_driver_duration"` + RequestTime time.Time `json:"request_driver_time"` } func (wd *Driver) concatURL(u *url.URL, elem ...string) string { @@ -73,7 +81,15 @@ func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (raw }() rawResp, err = io.ReadAll(resp.Body) - logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String()) + duration := time.Since(start) + driverResult := &DriverResult{ + RequestUrl: rawURL, + RequestBody: string(rawBody), + RequestDuration: duration, + RequestTime: time.Now(), + } + wd.driverResults = append(wd.driverResults, driverResult) + logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", duration.String()) if !strings.HasSuffix(rawURL, "screenshot") { // avoid printing screenshot data logger.Str("response", string(rawResp)) diff --git a/hrp/pkg/uixt/evalite b/hrp/pkg/uixt/evalite new file mode 100644 index 00000000..52697e21 Binary files /dev/null and b/hrp/pkg/uixt/evalite differ diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index a21b0455..c7906b20 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -126,11 +126,16 @@ type DriverExt struct { lastPopup *PopupInfo } -func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dExt *DriverExt, err error) { +func newDriverExt(device Device, driver WebDriver, options ...DriverOption) (dExt *DriverExt, err error) { + driverOptions := NewDriverOptions() + for _, option := range options { + option(driverOptions) + } + dExt = &DriverExt{ Device: device, Driver: driver, - plugin: plugin, + plugin: driverOptions.plugin, cacheStepData: cacheStepData{}, interruptSignal: make(chan os.Signal, 1), } @@ -150,21 +155,86 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE if err != nil { return nil, errors.Wrap(err, "get screen resolution failed") } - - if dExt.ImageService, err = newVEDEMImageService(); err != nil { - return nil, err + if driverOptions.withImageService { + if dExt.ImageService, err = newVEDEMImageService(); err != nil { + return nil, err + } } - - // create results directory - if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil { - return nil, errors.Wrap(err, "create results directory failed") - } - if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil { - return nil, errors.Wrap(err, "create screenshots directory failed") + if driverOptions.withResultFolder { + // create results directory + if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil { + return nil, errors.Wrap(err, "create results directory failed") + } + if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil { + return nil, errors.Wrap(err, "create screenshots directory failed") + } } return dExt, nil } +func (dExt *DriverExt) InstallByUrl(url string, opts *InstallOptions) error { + // 获取当前目录 + cwd, err := os.Getwd() + if err != nil { + return err + } + + // 将文件保存到当前目录 + appPath := filepath.Join(cwd, fmt.Sprint(time.Now().UnixNano())) // 替换为你想保存的文件名 + err = builtin.DownloadFile(appPath, url) + if err != nil { + return err + } + + err = dExt.Install(appPath, opts) + if err != nil { + return err + } + return nil +} + +func (dExt *DriverExt) Uninstall(packageName string, options ...ActionOption) error { + actionOptions := NewActionOptions(options...) + err := dExt.Device.Uninstall(packageName) + if err != nil { + log.Warn().Err(err).Msg("failed to uninstall") + } + if actionOptions.IgnoreNotFoundError { + return nil + } + return err +} + +func (dExt *DriverExt) Install(filePath string, opts *InstallOptions) error { + if _, ok := dExt.Device.(*AndroidDevice); ok { + stopChan := make(chan struct{}) + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + actions := []TapTextAction{ + {Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}}, + {Text: "^已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}}, + } + _ = dExt.Driver.TapByTexts(actions...) + _ = dExt.TapByOCR("^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", WithRegex(true), WithIgnoreNotFoundError(true)) + case <-stopChan: + fmt.Println("Ticker stopped") + return + } + } + }() + defer func() { + close(stopChan) + }() + } + + return dExt.Device.Install(filePath, opts) +} + // takeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path string, err error) { // iOS 优先使用 MJPEG 流进行截图,性能最优 @@ -267,7 +337,7 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} { cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls() cacheData["screen_results"] = dExt.cacheStepData.screenResults cacheData["e2e_results"] = dExt.cacheStepData.e2eDelay - + cacheData["driver_request_results"] = dExt.Driver.GetDriverResults() // clear cache dExt.cacheStepData.reset() return cacheData diff --git a/hrp/pkg/uixt/install.go b/hrp/pkg/uixt/install.go new file mode 100644 index 00000000..819d1746 --- /dev/null +++ b/hrp/pkg/uixt/install.go @@ -0,0 +1,48 @@ +package uixt + +type InstallOptions struct { + Reinstall bool + GrantPermission bool + Downgrade bool + RetryTime int +} + +type InstallOption func(o *InstallOptions) + +func NewInstallOptions(options ...InstallOption) *InstallOptions { + installOptions := &InstallOptions{} + for _, option := range options { + option(installOptions) + } + return installOptions +} + +func WithReinstall(reinstall bool) InstallOption { + return func(o *InstallOptions) { + o.Reinstall = reinstall + } +} + +func WithGrantPermission(grantPermission bool) InstallOption { + return func(o *InstallOptions) { + o.GrantPermission = grantPermission + } +} + +func WithDowngrade(downgrade bool) InstallOption { + return func(o *InstallOptions) { + o.Downgrade = downgrade + } +} + +func WithRetryTime(retryTime int) InstallOption { + return func(o *InstallOptions) { + o.RetryTime = retryTime + } +} + +type InstallResult struct { + Result int `json:"result"` + ErrorCode int `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 0ecd0145..30d0598f 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -250,11 +250,7 @@ type Screen struct { } type AppInfo struct { - ProcessArguments struct { - Env interface{} `json:"env"` - Args []interface{} `json:"args"` - } `json:"processArguments"` - Name string `json:"name"` + Name string `json:"name,omitempty"` AppBaseInfo } @@ -264,6 +260,10 @@ type AppBaseInfo struct { ViewController string `json:"viewController,omitempty"` // ios view controller PackageName string `json:"packageName,omitempty"` // android package name Activity string `json:"activity,omitempty"` // android activity + VersionName string `json:"versionName,omitempty"` + VersionCode int `json:"versionCode,omitempty"` + AppName string `json:"appName,omitempty"` + // AppIcon string `json:"appIcon,omitempty"` } type AppState int @@ -374,6 +374,11 @@ func (opt SourceOption) WithFormatAsJson() SourceOption { return opt } +func (opt SourceOption) WithProcessName(processName string) SourceOption { + opt["processName"] = processName + return opt +} + // WithFormatAsXml Application elements tree in form of xml string func (opt SourceOption) WithFormatAsXml() SourceOption { opt["format"] = "xml" @@ -446,8 +451,17 @@ type Rect struct { } type DriverOptions struct { - capabilities Capabilities - plugin funplugin.IPlugin + capabilities Capabilities + plugin funplugin.IPlugin + withImageService bool + withResultFolder bool +} + +func NewDriverOptions() *DriverOptions { + return &DriverOptions{ + withImageService: true, + withResultFolder: true, + } } type DriverOption func(*DriverOptions) @@ -458,6 +472,18 @@ func WithDriverCapabilities(capabilities Capabilities) DriverOption { } } +func WithDriverImageService(withImageService bool) DriverOption { + return func(options *DriverOptions) { + options.withImageService = withImageService + } +} + +func WithDriverResultFolder(withResultFolder bool) DriverOption { + return func(options *DriverOptions) { + options.withResultFolder = withResultFolder + } +} + func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption { return func(options *DriverOptions) { options.plugin = plugin @@ -466,8 +492,8 @@ func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption { // current implemeted device: IOSDevice, AndroidDevice type Device interface { - System() string // ios or android - UUID() string // ios udid or android serial + Init() error // init android device + UUID() string // ios udid or android serial LogEnabled() bool NewDriver(...DriverOption) (driverExt *DriverExt, err error) @@ -476,6 +502,10 @@ type Device interface { StartPcap() error StopPcap() string + + Uninstall(packageName string) error + + Install(appPath string, opts *InstallOptions) error } type ForegroundApp struct { @@ -524,6 +554,8 @@ type WebDriver interface { // Homescreen Forces the device under test to switch to the home screen Homescreen() error + Unlock() (err error) + // AppLaunch Launch an application with given bundle identifier in scope of current session. // !This method is only available since Xcode9 SDK AppLaunch(packageName string) error @@ -570,6 +602,8 @@ type WebDriver interface { // It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330 GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) + SetIme(ime string) error + // SendKeys Types a string into active element. There must be element with keyboard focus, // otherwise an error is raised. // WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60 @@ -578,17 +612,25 @@ type WebDriver interface { // Input works like SendKeys Input(text string, options ...ActionOption) error + Clear(packageName string) error + // PressButton Presses the corresponding hardware button on the device PressButton(devBtn DeviceButton) error // PressBack Presses the back button PressBack(options ...ActionOption) error + PressKeyCode(keyCode KeyCode) (err error) + Screenshot() (*bytes.Buffer, error) // Source Return application elements tree Source(srcOpt ...SourceOption) (string, error) + LoginNoneUI(packageName, phoneNumber string, captcha string) error + + LogoutNoneUI(packageName string) error + TapByText(text string, options ...ActionOption) error TapByTexts(actions ...TapTextAction) error @@ -609,4 +651,5 @@ type WebDriver interface { // triggers the log capture and returns the log entries StartCaptureLog(identifier ...string) (err error) StopCaptureLog() (result interface{}, err error) + GetDriverResults() []*DriverResult } diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 133760ad..093aea28 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -276,6 +276,7 @@ type IOSDevice struct { MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` XCTestBundleID string `json:"xctest_bundle_id,omitempty" yaml:"xctest_bundle_id,omitempty"` + IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` // switch to iOS springboard before init WDA session ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` @@ -294,8 +295,8 @@ type IOSDevice struct { pcapFile string // saved pcap file path } -func (dev *IOSDevice) System() string { - return "ios" +func (dev *IOSDevice) Init() error { + return nil } func (dev *IOSDevice) UUID() string { @@ -307,7 +308,7 @@ func (dev *IOSDevice) LogEnabled() bool { } func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) { - driverOptions := &DriverOptions{} + driverOptions := NewDriverOptions() for _, option := range options { option(driverOptions) } @@ -338,7 +339,7 @@ func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, } } - driverExt, err = newDriverExt(dev, driver, driverOptions.plugin) + driverExt, err = newDriverExt(dev, driver, options...) if err != nil { return nil, err } @@ -471,6 +472,20 @@ func (dev *IOSDevice) StopPcap() string { return dev.pcapFile } +func (dev *IOSDevice) Install(appPath string, opts *InstallOptions) (err error) { + for i := 0; i <= opts.RetryTime; i++ { + err = builtin.RunCommand("go-ios", "install", "--path="+appPath, "--udid="+dev.UDID) + if err == nil { + return nil + } + } + return err +} + +func (dev *IOSDevice) Uninstall(bundleId string) error { + return builtin.RunCommand("go-ios", "uninstall", bundleId, "--udid="+dev.UDID) +} + func (dev *IOSDevice) forward(localPort, remotePort int) error { log.Info().Int("localPort", localPort).Int("remotePort", remotePort). Str("udid", dev.UDID).Msg("forward tcp port") @@ -635,7 +650,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver wd := new(wdaDriver) wd.client = http.DefaultClient - host := "127.0.0.1" + host := "localhost" if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil { return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) } diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index 9acc164c..b94a8eff 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -591,6 +591,14 @@ func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe return } +func (wd *wdaDriver) SetIme(ime string) error { + return errDriverNotImplemented +} + +func (wd *wdaDriver) PressKeyCode(keyCode KeyCode) (err error) { + return errDriverNotImplemented +} + func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error) { // [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] actionOptions := NewActionOptions(options...) @@ -607,6 +615,10 @@ func (wd *wdaDriver) Input(text string, options ...ActionOption) (err error) { return wd.SendKeys(text, options...) } +func (wd *wdaDriver) Clear(packageName string) error { + return errDriverNotImplemented +} + // PressBack simulates a short press on the BACK button. func (wd *wdaDriver) PressBack(options ...ActionOption) (err error) { actionOptions := NewActionOptions(options...) @@ -651,6 +663,14 @@ func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) { return } +func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error { + return errDriverNotImplemented +} + +func (wd *wdaDriver) LogoutNoneUI(packageName string) error { + return errDriverNotImplemented +} + func (wd *wdaDriver) StartCamera() (err error) { // start camera, alias for app_launch com.apple.camera return wd.AppLaunch("com.apple.camera") @@ -877,6 +897,13 @@ func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { return reply.Value, nil } +func (wd *wdaDriver) GetDriverResults() []*DriverResult { + defer func() { + wd.Driver.driverResults = nil + }() + return wd.Driver.driverResults +} + type rawResponse []byte func (r rawResponse) checkErr() (err error) { @@ -939,6 +966,13 @@ func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, return } +func (r rawResponse) valueConvertToJsonObject() (obj map[string]interface{}, err error) { + if err = json.Unmarshal(r, &obj); err != nil { + return nil, err + } + return +} + func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) { str, err := r.valueConvertToString() if err != nil { diff --git a/hrp/pkg/uixt/ios_test.go b/hrp/pkg/uixt/ios_test.go index bcb3eb3a..73f7d7ce 100644 --- a/hrp/pkg/uixt/ios_test.go +++ b/hrp/pkg/uixt/ios_test.go @@ -7,6 +7,8 @@ import ( "fmt" "testing" "time" + + "github.com/rs/zerolog/log" ) var ( @@ -26,7 +28,7 @@ func setup(t *testing.T) { if err != nil { t.Fatal(err) } - iOSDriverExt, err = newDriverExt(device, driver, nil) + iOSDriverExt, err = newDriverExt(device, driver) if err != nil { t.Fatal(err) } @@ -37,6 +39,15 @@ func TestViaUSB(t *testing.T) { t.Log(driver.Status()) } +func TestInstall(t *testing.T) { + setup(t) + err := iOSDriverExt.Install("/Users/bytedance/Downloads/com.yueyou.cyreader_1387717110_7.54.20.ipa", NewInstallOptions(WithRetryTime(5))) + log.Error().Err(err) + if err != nil { + t.Fatal(err) + } +} + func TestNewIOSDevice(t *testing.T) { device, _ := NewIOSDevice() if device != nil { diff --git a/hrp/pkg/uixt/live_e2e.go b/hrp/pkg/uixt/live_e2e.go index 58a00913..cda4d72c 100644 --- a/hrp/pkg/uixt/live_e2e.go +++ b/hrp/pkg/uixt/live_e2e.go @@ -62,11 +62,11 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { // filter ocr texts with time format var liveTimeTexts []string for _, ocrText := range ocrTexts { - if len(ocrText.Text) < 10 || strings.Contains(ocrText.Text, ":") { + if len(ocrText.Text) < 13 || strings.Contains(ocrText.Text, ":") { continue } // exclude digit(s) recognized as letter(s) - _, errParseInt := strconv.ParseInt(ocrText.Text[:10], 10, 64) + _, errParseInt := strconv.ParseInt(ocrText.Text[:13], 10, 64) if errParseInt != nil { continue } @@ -81,11 +81,6 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { return nil } - if len(liveTimeText) < 13 { - for (13 - len(liveTimeText)) > 0 { - liveTimeText += "0" - } - } liveTimeInt, err := strconv.Atoi(liveTimeText) if err != nil { liveTimeInt = 0 diff --git a/hrp/pkg/utf7/utf7_test.go b/hrp/pkg/utf7/utf7_test.go new file mode 100644 index 00000000..c729f04e --- /dev/null +++ b/hrp/pkg/utf7/utf7_test.go @@ -0,0 +1,11 @@ +package utf7 + +import "testing" + +func Test_Decode(t *testing.T) { + str, err := Encoding.NewDecoder().String("&j71bgXcBbIiWM14CZbBsEV4CbBFlz4hX-36-4") + if err != nil { + t.Fatal(err) + } + t.Log(str) +} diff --git a/hrp/runner.go b/hrp/runner.go index 684195fe..c5720691 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -648,6 +648,16 @@ func (r *SessionRunner) initWithParameters(parameters map[string]interface{}) { } } +func (r *SessionRunner) IgnorePopup() bool { + if r.caseRunner.testCase.Config.Android != nil { + return r.caseRunner.testCase.Config.Android[0].IgnorePopup + } + if r.caseRunner.testCase.Config.IOS != nil { + return r.caseRunner.testCase.Config.IOS[0].IgnorePopup + } + return false +} + // updateSummary updates summary of StepResult. func (r *SessionRunner) updateSummary(stepResult *StepResult) { switch stepResult.StepType { diff --git a/hrp/step.go b/hrp/step.go index c9382537..5c60078f 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -53,6 +53,7 @@ type TStep struct { Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` Export []string `json:"export,omitempty" yaml:"export,omitempty"` Loops int `json:"loops,omitempty" yaml:"loops,omitempty"` + IgnorePopup bool `json:"ignore_popup,omitempty" yaml:"ignore_popup,omitempty"` } // IStep represents interface for all types for teststeps, includes: diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index a57bd6e1..8b138c8e 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -605,18 +605,18 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err } // report GA event - sdk.SendGA4Event("hrp_run_ui", map[string]interface{}{ + go sdk.SendGA4Event("hrp_run_ui", map[string]interface{}{ "osType": osType, }) - identifer := mobileStep.Identifier - if mobileStep.Options != nil && identifer == "" { - identifer = mobileStep.Options.Identifier + identifier := mobileStep.Identifier + if mobileStep.Options != nil && identifier == "" { + identifier = mobileStep.Options.Identifier } - if len(mobileStep.Actions) != 0 && identifer == "" { + if len(mobileStep.Actions) != 0 && identifier == "" { for _, action := range mobileStep.Actions { if action.Identifier != "" { - identifer = action.Identifier + identifier = action.Identifier break } } @@ -624,7 +624,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err stepResult = &StepResult{ Name: step.Name, - Identifier: identifer, + Identifier: identifier, StepType: StepType(osType), Success: false, ContentSize: 0, @@ -650,8 +650,10 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err } // automatic handling of pop-up windows on each step finished - if err2 := uiDriver.ClosePopupsHandler(); err2 != nil { - log.Error().Err(err2).Str("step", step.Name).Msg("handle popup failed on step finished") + if !step.IgnorePopup && !s.IgnorePopup() { + if err2 := uiDriver.ClosePopups(); err2 != nil { + log.Error().Err(err2).Str("step", step.Name).Msg("auto handle popup failed") + } } // save attachments