From 7a94a7860caed25491d27adbae65dbc55e35ecad Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 26 Jul 2022 15:26:39 +0800 Subject: [PATCH 001/169] fix: golint --- hrp/server.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/hrp/server.go b/hrp/server.go index c060d409..fef1dbb2 100644 --- a/hrp/server.go +++ b/hrp/server.go @@ -9,9 +9,10 @@ import ( "net/http" "strings" + "github.com/mitchellh/mapstructure" + "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/json" - "github.com/mitchellh/mapstructure" ) const jsonContentType = "application/json; encoding=utf-8" @@ -134,16 +135,14 @@ type CommonResponseBody struct { ServerStatus } -type APIGetWorkersRequestBody struct { -} +type APIGetWorkersRequestBody struct{} type APIGetWorkersResponseBody struct { ServerStatus Data []boomer.WorkerNode `json:"data"` } -type APIGetMasterRequestBody struct { -} +type APIGetMasterRequestBody struct{} type APIGetMasterResponseBody struct { ServerStatus @@ -204,7 +203,7 @@ func (api *apiHandler) Start(w http.ResponseWriter, r *http.Request) { for k := range req.Other { keys = append(keys, k) } - err = errors.New(fmt.Sprintf("failed to recognize params: %v", keys)) + err = fmt.Errorf("failed to recognize params: %v", keys) return } @@ -258,7 +257,7 @@ func (api *apiHandler) ReBalance(w http.ResponseWriter, r *http.Request) { for k := range req.Other { keys = append(keys, k) } - err = errors.New(fmt.Sprintf("failed to recognize params: %v", keys)) + err = fmt.Errorf("failed to recognize params: %v", keys) return } @@ -370,7 +369,7 @@ func (b *HRPBoomer) StartServer(ctx context.Context, addr string) { } }() - log.Println(fmt.Sprintf("starting HTTP server (%v), please use the API to control master", server.Addr)) + log.Printf("starting HTTP server (%v), please use the API to control master", server.Addr) err := server.ListenAndServe() if err != nil { if err == http.ErrServerClosed { From aa4d5faa120a864c79cb4be4cd2949049d539530 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 26 Jul 2022 17:39:48 +0800 Subject: [PATCH 002/169] feat: create scaffold for mobile UI test --- hrp/step.go | 26 ++++++ hrp/step_android_ui.go | 199 +++++++++++++++++++++++++++++++++++++++++ hrp/step_ios_ui.go | 159 ++++++++++++++++++++++++++++++++ hrp/step_request.go | 16 ++++ hrp/step_ui_test.go | 44 +++++++++ 5 files changed, 444 insertions(+) create mode 100644 hrp/step_android_ui.go create mode 100644 hrp/step_ios_ui.go create mode 100644 hrp/step_ui_test.go diff --git a/hrp/step.go b/hrp/step.go index b4583852..0097c6bb 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -10,8 +10,32 @@ const ( stepTypeRendezvous StepType = "rendezvous" stepTypeThinkTime StepType = "thinktime" stepTypeWebSocket StepType = "websocket" + stepTypeAndroid StepType = "android" + stepTypeIOS StepType = "ios" ) +type MobileMethod string + +const ( + appInstall MobileMethod = "install" + appStart MobileMethod = "app_start" + cameraStart MobileMethod = "camera_start" + cameraStop MobileMethod = "camera_stop" + recordStart MobileMethod = "record_start" + recordStop MobileMethod = "record_stop" + uiClick MobileMethod = "click" + uiDoubleClick MobileMethod = "double_click" + uiLongClick MobileMethod = "long_click" + uiSwipe MobileMethod = "swipe" + uiInput MobileMethod = "input" + appClick MobileMethod = "app_click" +) + +type MobileAction struct { + Method MobileMethod `json:"method" yaml:"method"` + Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` +} + type StepResult struct { Name string `json:"name" yaml:"name"` // step name StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous @@ -35,6 +59,8 @@ type TStep struct { Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"` ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocket *WebSocketAction `json:"websocket,omitempty" yaml:"websocket,omitempty"` + Android *AndroidAction `json:"android,omitempty" yaml:"android,omitempty"` + IOS *IOSAction `json:"ios,omitempty" yaml:"ios,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go new file mode 100644 index 00000000..9aa851f7 --- /dev/null +++ b/hrp/step_android_ui.go @@ -0,0 +1,199 @@ +package hrp + +type AndroidAction struct { + MobileAction + Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` + Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` +} + +// StepAndroid implements IStep interface. +type StepAndroid struct { + step *TStep +} + +func (s *StepAndroid) Serial(serial string) *StepAndroid { + s.step.Android.Serial = serial + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) InstallApp(path string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: appInstall, + Params: path, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: appStart, + Params: activity, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StartCamera() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: cameraStart, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StopCamera() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: cameraStop, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StartRecording() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: recordStart, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StopRecording() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: recordStop, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) Click(params interface{}) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiClick, + Params: params, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) DoubleClick(params interface{}) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiDoubleClick, + Params: params, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) LongClick(params interface{}) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiLongClick, + Params: params, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) Swipe(sx, sy, ex, ey int) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiSwipe, + Params: []int{sx, sy, ex, ey}, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeUp() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiSwipe, + Params: "up", + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeDown() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiSwipe, + Params: "down", + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeLeft() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiSwipe, + Params: "left", + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeRight() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiSwipe, + Params: "right", + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) Input(text string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: uiInput, + Params: text, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) StartAppByClick(name string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ + Method: appClick, + Params: name, + }) + return &StepAndroid{step: s.step} +} + +// Validate switches to step validation. +func (s *StepAndroid) Validate() *StepAndroidValidation { + return &StepAndroidValidation{ + step: s.step, + } +} + +func (s *StepAndroid) Name() string { + return s.step.Name +} + +func (s *StepAndroid) Type() StepType { + return stepTypeAndroid +} + +func (s *StepAndroid) Struct() *TStep { + return s.step +} + +func (s *StepAndroid) Run(r *SessionRunner) (*StepResult, error) { + return runStepAndroid(r, s.step) +} + +// StepAndroidValidation implements IStep interface. +type StepAndroidValidation struct { + step *TStep +} + +func (s *StepAndroidValidation) Name() string { + return s.step.Name +} + +func (s *StepAndroidValidation) Type() StepType { + return stepTypeAndroid +} + +func (s *StepAndroidValidation) Struct() *TStep { + return s.step +} + +func (s *StepAndroidValidation) Run(r *SessionRunner) (*StepResult, error) { + return runStepAndroid(r, s.step) +} + +func runStepAndroid(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { + stepResult = &StepResult{ + Name: step.Name, + StepType: stepTypeAndroid, + Success: false, + ContentSize: 0, + } + return stepResult, nil +} diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go new file mode 100644 index 00000000..b6903e80 --- /dev/null +++ b/hrp/step_ios_ui.go @@ -0,0 +1,159 @@ +package hrp + +type IOSAction struct { + MobileAction + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` +} + +// StepIOS implements IStep interface. +type StepIOS struct { + step *TStep +} + +func (s *StepIOS) UDID(udid string) *StepIOS { + s.step.IOS.UDID = udid + return &StepIOS{step: s.step} +} + +func (s *StepIOS) InstallApp(path string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: appInstall, + Params: path, + }) + return s +} + +func (s *StepIOS) Click(params interface{}) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiClick, + Params: params, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) DoubleClick(params interface{}) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiDoubleClick, + Params: params, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) LongClick(params interface{}) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiLongClick, + Params: params, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) Swipe(sx, sy, ex, ey int) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiSwipe, + Params: []int{sx, sy, ex, ey}, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeUp() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiSwipe, + Params: "up", + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeDown() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiSwipe, + Params: "down", + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeLeft() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiSwipe, + Params: "left", + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeRight() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiSwipe, + Params: "right", + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) Input(text string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiInput, + Params: text, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) StartAppByClick(name string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: appClick, + Params: name, + }) + return &StepIOS{step: s.step} +} + +// Validate switches to step validation. +func (s *StepIOS) Validate() *StepIOSValidation { + return &StepIOSValidation{ + step: s.step, + } +} + +func (s *StepIOS) Name() string { + return s.step.Name +} + +func (s *StepIOS) Type() StepType { + return stepTypeAndroid +} + +func (s *StepIOS) Struct() *TStep { + return s.step +} + +func (s *StepIOS) Run(r *SessionRunner) (*StepResult, error) { + return runStepIOS(r, s.step) +} + +// StepIOSValidation implements IStep interface. +type StepIOSValidation struct { + step *TStep +} + +func (s *StepIOSValidation) Name() string { + return s.step.Name +} + +func (s *StepIOSValidation) Type() StepType { + return stepTypeAndroid +} + +func (s *StepIOSValidation) Struct() *TStep { + return s.step +} + +func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { + return runStepIOS(r, s.step) +} + +func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { + stepResult = &StepResult{ + Name: step.Name, + StepType: stepTypeAndroid, + Success: false, + ContentSize: 0, + } + return stepResult, nil +} diff --git a/hrp/step_request.go b/hrp/step_request.go index 651ce8fe..f2669236 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -759,6 +759,22 @@ func (s *StepRequest) WebSocket() *StepWebSocket { } } +// Android creates a new android action +func (s *StepRequest) Android() *StepAndroid { + s.step.Android = &AndroidAction{} + return &StepAndroid{ + step: s.step, + } +} + +// IOS creates a new ios action +func (s *StepRequest) IOS() *StepIOS { + s.step.IOS = &IOSAction{} + return &StepIOS{ + step: s.step, + } +} + // StepRequestWithOptionalArgs implements IStep interface. type StepRequestWithOptionalArgs struct { step *TStep diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go new file mode 100644 index 00000000..91d730a9 --- /dev/null +++ b/hrp/step_ui_test.go @@ -0,0 +1,44 @@ +package hrp + +import ( + "fmt" + "testing" +) + +func TestAndroidAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("android ui action"), + TestSteps: []IStep{ + NewStep("launch douyin"). + Android().Serial("xxx").Click("抖音"), + NewStep("swipe up and down"). + Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), + }, + } + tCase := testCase.ToTCase() + fmt.Println(tCase) + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action"), + TestSteps: []IStep{ + NewStep("launch douyin"). + IOS().UDID("xxx").Click("抖音"), + NewStep("swipe up and down"). + IOS().UDID("xxx").SwipeUp().SwipeUp().SwipeDown(), + }, + } + tCase := testCase.ToTCase() + fmt.Println(tCase) + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} From 74aea47b99ae1fd6ef7fcd3c1d1c8e238c065462 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 26 Jul 2022 18:53:38 +0800 Subject: [PATCH 003/169] feat: add mobile ui validation --- hrp/step_android_ui.go | 11 +++++++++++ hrp/step_ios_ui.go | 11 +++++++++++ hrp/step_ui_test.go | 10 ++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 9aa851f7..f49adb6d 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -172,6 +172,17 @@ type StepAndroidValidation struct { step *TStep } +func (s *StepAndroidValidation) AssertTextExists(expectedText string, msg string) *StepAndroidValidation { + v := Validator{ + Check: "android_ui", + Assert: "text_exists", + Expect: expectedText, + Message: msg, + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepAndroidValidation) Name() string { return s.step.Name } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index b6903e80..cf2fb8c4 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -132,6 +132,17 @@ type StepIOSValidation struct { step *TStep } +func (s *StepIOSValidation) AssertTextExists(expectedText string, msg string) *StepIOSValidation { + v := Validator{ + Check: "ios_ui", + Assert: "text_exists", + Expect: expectedText, + Message: msg, + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepIOSValidation) Name() string { return s.step.Name } diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 91d730a9..02e2044e 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -10,7 +10,10 @@ func TestAndroidAction(t *testing.T) { Config: NewConfig("android ui action"), TestSteps: []IStep{ NewStep("launch douyin"). - Android().Serial("xxx").Click("抖音"), + Android().Serial("xxx").Click("抖音"). + Validate(). + AssertTextExists("首页", "首页 tab 不存在"). + AssertTextExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), }, @@ -29,7 +32,10 @@ func TestIOSAction(t *testing.T) { Config: NewConfig("ios ui action"), TestSteps: []IStep{ NewStep("launch douyin"). - IOS().UDID("xxx").Click("抖音"), + IOS().UDID("xxx").Click("抖音"). + Validate(). + AssertTextExists("首页", "首页 tab 不存在"). + AssertTextExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). IOS().UDID("xxx").SwipeUp().SwipeUp().SwipeDown(), }, From d8cbe1ae68d2801c85b17341b71c31b26cc95e4e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 10:22:04 +0800 Subject: [PATCH 004/169] feat: implement ios ui swipe --- go.mod | 1 + go.sum | 9 +++ hrp/runner.go | 2 + hrp/step_ios_ui.go | 170 +++++++++++++++++++++++++++++++++++++++++++- hrp/step_ui_test.go | 4 +- 5 files changed, 183 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c175a55c..3cfe23b2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 + github.com/electricbubble/gwda v0.3.0 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 github.com/go-errors/errors v1.0.1 diff --git a/go.sum b/go.sum index 600942d3..1e0bd26b 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,10 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/electricbubble/gidevice v0.0.4 h1:PbOt4AngNQTtO5j0vCZ3Xcj9mByDtZmjBYLTh8PJ9kc= +github.com/electricbubble/gidevice v0.0.4/go.mod h1:hWRHIPf4uyiEB56hnVHVvu6MoVg7RlJY8ZV2FVgLKZA= +github.com/electricbubble/gwda v0.3.0 h1:uQMZxmp5D51iMsXrWfi21MlftrkPmOeLDE+gtw06fg4= +github.com/electricbubble/gwda v0.3.0/go.mod h1:co3ynSIVXEyI3aKdzfjqkFDFloFcxhc+e27U0ajyZsM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -270,6 +274,7 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= @@ -419,6 +424,8 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -900,6 +907,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= +howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/hrp/runner.go b/hrp/runner.go index e6662613..454058e8 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -48,6 +48,7 @@ func NewRunner(t *testing.T) *HRPRunner { Transport: &http2.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, + Jar: jar, // insert response cookies into request Timeout: 120 * time.Second, }, // use default handshake timeout (no timeout limit) here, enable timeout at step level @@ -69,6 +70,7 @@ type HRPRunner struct { httpClient *http.Client http2Client *http.Client wsDialer *websocket.Dialer + wdaClients map[string]*wdaClient // wda client used for iOS UI automation, key is udid } // SetClientTransport configures transport of http client for high concurrency load testing diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index cf2fb8c4..06b19ca8 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -1,5 +1,13 @@ package hrp +import ( + "fmt" + + "github.com/electricbubble/gwda" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + type IOSAction struct { MobileAction UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` @@ -159,12 +167,172 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } +func (r *HRPRunner) InitWDAClient(udid string) (*wdaClient, error) { + // avoid duplicate init + if udid == "" && len(r.wdaClients) == 1 { + for _, v := range r.wdaClients { + return v, nil + } + } + + targetDevice, err := getAttachedIOSDevice(udid) + if err != nil { + return nil, err + } + + // avoid duplicate init + if client, ok := r.wdaClients[targetDevice.SerialNumber()]; ok { + return client, nil + } + + // init WDA driver + driver, err := gwda.NewUSBDriver(nil, *targetDevice) + if err != nil { + return nil, errors.Wrap(err, "failed to init WDA driver") + } + + // get device window size + windowSize, err := driver.WindowSize() + if err != nil { + return nil, errors.Wrap(err, "failed to get windows size") + } + + // cache wda client + r.wdaClients = make(map[string]*wdaClient) + client := &wdaClient{ + Driver: driver, + WindowSize: windowSize, + } + r.wdaClients[targetDevice.SerialNumber()] = client + + return client, nil +} + +func getAttachedIOSDevice(udid string) (*gwda.Device, error) { + // get all attached deivces + devices, err := gwda.DeviceList() + if err != nil { + return nil, errors.Wrap(err, "failed to get attached ios devices list") + } + if len(devices) == 0 { + return nil, errors.New("no ios devices attached") + } + + if udid == "" { + return &devices[0], nil + } + + // find device by udid + for _, device := range devices { + if device.SerialNumber() == udid { + return &device, nil + } + } + + return nil, fmt.Errorf("device %s is not attached", udid) +} + func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, - StepType: stepTypeAndroid, + StepType: stepTypeIOS, Success: false, ContentSize: 0, } + + // init wdaClient driver + wdaClient, err := r.hrpRunner.InitWDAClient(step.IOS.UDID) + if err != nil { + return + } + + // prepare actions + var actions []MobileAction + if step.IOS.Actions == nil { + actions = []MobileAction{ + { + Method: step.IOS.Method, + Params: step.IOS.Params, + }, + } + } else { + actions = step.IOS.Actions + } + + // run actions + for _, action := range actions { + if err := wdaClient.doAction(action); err != nil { + return stepResult, err + } + } + + // do validation + // step.Validators + + stepResult.Success = true return stepResult, nil } + +var errActionNotImplemented = errors.New("UI action not implemented") + +type wdaClient struct { + Driver gwda.WebDriver + WindowSize gwda.Size +} + +func (w *wdaClient) doAction(action MobileAction) error { + log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") + + switch action.Method { + case appInstall: + // TODO + return errActionNotImplemented + case appStart: + // TODO + return errActionNotImplemented + case uiClick: + // TODO + return errActionNotImplemented + case uiDoubleClick: + // TODO + return errActionNotImplemented + case uiLongClick: + // TODO + return errActionNotImplemented + case uiSwipe: + width := w.WindowSize.Width + height := w.WindowSize.Height + + var fromX, fromY, toX, toY int + if direction, ok := action.Params.(string); ok { + switch direction { + case "up": + fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 + case "down": + fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4 + case "left": + fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2 + case "right": + fromX, fromY, toX, toY = width*1/4, height/2, width*3/4, height/2 + } + } else if params, ok := action.Params.([]int); ok { + if len(params) != 4 { + return fmt.Errorf("invalid swipe params: %v", params) + } + fromX, fromY, toX, toY = params[0], params[1], params[2], params[3] + } + return w.Driver.Swipe(fromX, fromY, toX, toY) + case uiInput: + // TODO + return errActionNotImplemented + case appClick: + // TODO + return errActionNotImplemented + } + return nil +} + +func (w *wdaClient) doValidation() error { + // TODO + return errActionNotImplemented +} diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 02e2044e..4e8ec019 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -32,12 +32,12 @@ func TestIOSAction(t *testing.T) { Config: NewConfig("ios ui action"), TestSteps: []IStep{ NewStep("launch douyin"). - IOS().UDID("xxx").Click("抖音"). + IOS().Click("抖音"). Validate(). AssertTextExists("首页", "首页 tab 不存在"). AssertTextExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). - IOS().UDID("xxx").SwipeUp().SwipeUp().SwipeDown(), + IOS().SwipeUp().SwipeUp().SwipeDown(), }, } tCase := testCase.ToTCase() From 9eea7c632f4a77dc5abce251867aa4e57d4eceec Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 12:04:39 +0800 Subject: [PATCH 005/169] feat: check if WDA is healthy before each step --- hrp/step_ios_ui.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 06b19ca8..a414896f 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -167,7 +167,23 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(udid string) (*wdaClient, error) { +func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { + defer func() { + if err != nil { + return + } + // check if WDA is healthy + ok, e := client.Driver.IsWdaHealthy() + if err != nil { + err = errors.Wrap(e, "check WDA health failed") + return + } + if !ok { + err = errors.New("WDA is not healthy") + return + } + }() + // avoid duplicate init if udid == "" && len(r.wdaClients) == 1 { for _, v := range r.wdaClients { @@ -199,7 +215,7 @@ func (r *HRPRunner) InitWDAClient(udid string) (*wdaClient, error) { // cache wda client r.wdaClients = make(map[string]*wdaClient) - client := &wdaClient{ + client = &wdaClient{ Driver: driver, WindowSize: windowSize, } From d8b8d63d6031f2700e689196ce0a78668e0875cf Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 12:07:21 +0800 Subject: [PATCH 006/169] feat: click on coordinate --- hrp/step_ios_ui.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index a414896f..5e744294 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -307,8 +307,18 @@ func (w *wdaClient) doAction(action MobileAction) error { // TODO return errActionNotImplemented case uiClick: - // TODO - return errActionNotImplemented + // click on coordinate + if location, ok := action.Params.([]int); ok { + if len(location) != 2 { + return fmt.Errorf("invalid click location params: %v", location) + } + return w.Driver.Tap(location[0], location[1]) + } + // click on xpath + if _, ok := action.Params.(string); ok { + return errActionNotImplemented + } + return fmt.Errorf("invalid click params: %v", action.Params) case uiDoubleClick: // TODO return errActionNotImplemented From 4abc38221361ffe4f175e7ccb16d4642303a7bfa Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 13:42:23 +0800 Subject: [PATCH 007/169] feat: click on xpath --- hrp/step_ios_ui.go | 13 +++++++++++-- hrp/step_ui_test.go | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5e744294..aca120e4 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -315,8 +315,15 @@ func (w *wdaClient) doAction(action MobileAction) error { return w.Driver.Tap(location[0], location[1]) } // click on xpath - if _, ok := action.Params.(string); ok { - return errActionNotImplemented + if xpath, ok := action.Params.(string); ok { + selector := gwda.BySelector{ + XPath: xpath, + } + ele, err := w.Driver.FindElement(selector) + if err != nil { + return errors.Wrap(err, "failed to find element") + } + return ele.Click() } return fmt.Errorf("invalid click params: %v", action.Params) case uiDoubleClick: @@ -346,6 +353,8 @@ func (w *wdaClient) doAction(action MobileAction) error { return fmt.Errorf("invalid swipe params: %v", params) } fromX, fromY, toX, toY = params[0], params[1], params[2], params[3] + } else { + return fmt.Errorf("invalid swipe params: %v", action.Params) } return w.Driver.Swipe(fromX, fromY, toX, toY) case uiInput: diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 4e8ec019..c49c479d 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -32,7 +32,7 @@ func TestIOSAction(t *testing.T) { Config: NewConfig("ios ui action"), TestSteps: []IStep{ NewStep("launch douyin"). - IOS().Click("抖音"). + IOS().Click("//*[@label='抖音']"). Validate(). AssertTextExists("首页", "首页 tab 不存在"). AssertTextExists("消息", "消息 tab 不存在"), @@ -40,8 +40,6 @@ func TestIOSAction(t *testing.T) { IOS().SwipeUp().SwipeUp().SwipeDown(), }, } - tCase := testCase.ToTCase() - fmt.Println(tCase) err := NewRunner(t).Run(testCase) if err != nil { From 88ee963036baab9a8841fd80db65c26779b90959 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 21:13:53 +0800 Subject: [PATCH 008/169] feat: validate UI exists --- hrp/step_android_ui.go | 8 +-- hrp/step_ios_ui.go | 148 ++++++++++++++++++++++++++++++++++++----- hrp/step_ui_test.go | 36 +++++++--- 3 files changed, 164 insertions(+), 28 deletions(-) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index f49adb6d..106c5a76 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -172,11 +172,11 @@ type StepAndroidValidation struct { step *TStep } -func (s *StepAndroidValidation) AssertTextExists(expectedText string, msg string) *StepAndroidValidation { +func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg string) *StepAndroidValidation { v := Validator{ - Check: "android_ui", - Assert: "text_exists", - Expect: expectedText, + Check: "UI", + Assert: "xpath_exists", + Expect: expectedXpath, Message: msg, } s.step.Validators = append(s.step.Validators, v) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index aca120e4..e466da98 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -2,6 +2,7 @@ package hrp import ( "fmt" + "strings" "github.com/electricbubble/gwda" "github.com/pkg/errors" @@ -140,11 +141,44 @@ type StepIOSValidation struct { step *TStep } -func (s *StepIOSValidation) AssertTextExists(expectedText string, msg string) *StepIOSValidation { +func (s *StepIOSValidation) AssertNameExists(expectedName string, msg string) *StepIOSValidation { v := Validator{ - Check: "ios_ui", - Assert: "text_exists", - Expect: expectedText, + Check: "UI", + Assert: "name_exists", + Expect: expectedName, + Message: msg, + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg string) *StepIOSValidation { + v := Validator{ + Check: "UI", + Assert: "name_not_exists", + Expect: expectedName, + Message: msg, + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg string) *StepIOSValidation { + v := Validator{ + Check: "UI", + Assert: "xpath_exists", + Expect: expectedXpath, + Message: msg, + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertXpathNotExists(expectedXpath string, msg string) *StepIOSValidation { + v := Validator{ + Check: "UI", + Assert: "xpath_not_exists", + Expect: expectedXpath, Message: msg, } s.step.Validators = append(s.step.Validators, v) @@ -282,9 +316,14 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro } } - // do validation - // step.Validators - + // validate + validateResults, err := wdaClient.doValidation(step.Validators) + if err != nil { + return + } + sessionData := newSessionData() + sessionData.Validators = validateResults + stepResult.Data = sessionData stepResult.Success = true return stepResult, nil } @@ -314,11 +353,21 @@ func (w *wdaClient) doAction(action MobileAction) error { } return w.Driver.Tap(location[0], location[1]) } - // click on xpath - if xpath, ok := action.Params.(string); ok { - selector := gwda.BySelector{ - XPath: xpath, + // click on name or xpath + if param, ok := action.Params.(string); ok { + var selector gwda.BySelector + if strings.HasPrefix(param, "/") { + // xpath + selector = gwda.BySelector{ + XPath: param, + } + } else { + // name + selector = gwda.BySelector{ + Name: param, + } } + ele, err := w.Driver.FindElement(selector) if err != nil { return errors.Wrap(err, "failed to find element") @@ -340,9 +389,9 @@ func (w *wdaClient) doAction(action MobileAction) error { if direction, ok := action.Params.(string); ok { switch direction { case "up": - fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 - case "down": fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4 + case "down": + fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 case "left": fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2 case "right": @@ -367,7 +416,74 @@ func (w *wdaClient) doAction(action MobileAction) error { return nil } -func (w *wdaClient) doValidation() error { - // TODO - return errActionNotImplemented +func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []*ValidationResult, err error) { + for _, iValidator := range iValidators { + validator, ok := iValidator.(Validator) + if !ok { + return nil, errors.New("validator type error") + } + + validataResult := &ValidationResult{ + Validator: validator, + CheckResult: "fail", + } + + // parse check value + if validator.Check != "UI" { + validataResult.CheckResult = "skip" + log.Warn().Interface("validator", validator).Msg("skip validator") + validateResults = append(validateResults, validataResult) + continue + } + + expected, ok := validator.Expect.(string) + if !ok { + return nil, errors.New("validator expect should be string") + } + + var result bool + switch validator.Assert { + case "xpath_exists": + result = w.assertXpath(expected, true) + case "xpath_not_exists": + result = w.assertXpath(expected, false) + case "name_exists": + result = w.assertName(expected, true) + case "name_not_exists": + result = w.assertName(expected, false) + } + if result { + log.Info(). + Str("assert", validator.Assert). + Str("expect", expected). + Msg("validate UI success") + validataResult.CheckResult = "pass" + validateResults = append(validateResults, validataResult) + } else { + log.Error(). + Str("assert", validator.Assert). + Str("expect", expected). + Str("msg", validator.Message). + Msg("validate UI failed") + validateResults = append(validateResults, validataResult) + err = errors.New("step validation failed") + } + } + return +} + +func (w *wdaClient) assertName(name string, exists bool) bool { + selector := gwda.BySelector{ + Name: name, + } + _, err := w.Driver.FindElement(selector) + return exists == (err == nil) +} + +func (w *wdaClient) assertXpath(xpath string, exists bool) bool { + selector := gwda.BySelector{ + XPath: xpath, + } + _, err := w.Driver.FindElement(selector) + return exists == (err == nil) } diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index c49c479d..2dc01076 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -12,8 +12,8 @@ func TestAndroidAction(t *testing.T) { NewStep("launch douyin"). Android().Serial("xxx").Click("抖音"). Validate(). - AssertTextExists("首页", "首页 tab 不存在"). - AssertTextExists("消息", "消息 tab 不存在"), + AssertXpathExists("首页", "首页 tab 不存在"). + AssertXpathExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), }, @@ -27,15 +27,35 @@ func TestAndroidAction(t *testing.T) { } } -func TestIOSAction(t *testing.T) { +func TestIOSSettingsAction(t *testing.T) { testCase := &TestCase{ - Config: NewConfig("ios ui action"), + Config: NewConfig("ios ui action on Settings"), TestSteps: []IStep{ - NewStep("launch douyin"). - IOS().Click("//*[@label='抖音']"). + NewStep("launch Settings"). + IOS().Click("//*[@label='设置']"). Validate(). - AssertTextExists("首页", "首页 tab 不存在"). - AssertTextExists("消息", "消息 tab 不存在"), + AssertNameExists("飞行模式", "「飞行模式」不存在"). + AssertNameNotExists("飞行模式2", "「飞行模式2」不存在"), + NewStep("swipe up and down"). + IOS().SwipeUp().SwipeUp().SwipeDown(), + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSDouyinAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on 抖音"), + TestSteps: []IStep{ + NewStep("launch douyin"). + IOS().Click("//*[@label='抖音']"). + Validate(). + AssertNameExists("首页", "首页 tab 不存在"). + AssertNameExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), }, From ebe7d7ecf4315f2edd17d816f89114785fe562e1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 21:57:51 +0800 Subject: [PATCH 009/169] feat: double/long click on name or xpath --- hrp/step_ios_ui.go | 54 ++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index e466da98..3f50a502 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -355,20 +355,7 @@ func (w *wdaClient) doAction(action MobileAction) error { } // click on name or xpath if param, ok := action.Params.(string); ok { - var selector gwda.BySelector - if strings.HasPrefix(param, "/") { - // xpath - selector = gwda.BySelector{ - XPath: param, - } - } else { - // name - selector = gwda.BySelector{ - Name: param, - } - } - - ele, err := w.Driver.FindElement(selector) + ele, err := w.findElement(param) if err != nil { return errors.Wrap(err, "failed to find element") } @@ -376,11 +363,25 @@ func (w *wdaClient) doAction(action MobileAction) error { } return fmt.Errorf("invalid click params: %v", action.Params) case uiDoubleClick: - // TODO - return errActionNotImplemented + // double click on name or xpath + if param, ok := action.Params.(string); ok { + ele, err := w.findElement(param) + if err != nil { + return errors.Wrap(err, "failed to find element") + } + return ele.DoubleTap() + } + return fmt.Errorf("invalid click params: %v", action.Params) case uiLongClick: - // TODO - return errActionNotImplemented + // long click 2s on name or xpath + if param, ok := action.Params.(string); ok { + ele, err := w.findElement(param) + if err != nil { + return errors.Wrap(err, "failed to find element") + } + return ele.TouchAndHold(2) + } + return fmt.Errorf("invalid click params: %v", action.Params) case uiSwipe: width := w.WindowSize.Width height := w.WindowSize.Height @@ -472,6 +473,23 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* return } +func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { + var selector gwda.BySelector + if strings.HasPrefix(param, "/") { + // xpath + selector = gwda.BySelector{ + XPath: param, + } + } else { + // name + selector = gwda.BySelector{ + Name: param, + } + } + + return w.Driver.FindElement(selector) +} + func (w *wdaClient) assertName(name string, exists bool) bool { selector := gwda.BySelector{ Name: name, From 27c371e6a0aba9dbfbb58798fd63bb272f9572cc Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 22:02:54 +0800 Subject: [PATCH 010/169] feat: click on relative coordinate --- hrp/step_ios_ui.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 3f50a502..cf162b41 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -348,11 +348,21 @@ func (w *wdaClient) doAction(action MobileAction) error { case uiClick: // click on coordinate if location, ok := action.Params.([]int); ok { + // absolute x,y if len(location) != 2 { return fmt.Errorf("invalid click location params: %v", location) } return w.Driver.Tap(location[0], location[1]) } + if location, ok := action.Params.([]float64); ok { + // relative x,y of window size + if len(location) != 2 { + return fmt.Errorf("invalid click location params: %v", location) + } + x := location[0] * float64(w.WindowSize.Width) + y := location[1] * float64(w.WindowSize.Height) + return w.Driver.TapFloat(x, y) + } // click on name or xpath if param, ok := action.Params.(string); ok { ele, err := w.findElement(param) From 6215232ddc6ad65bdeda608271234ac629a5a886 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 22:10:49 +0800 Subject: [PATCH 011/169] feat: go back home on ios device --- hrp/step.go | 1 + hrp/step_ios_ui.go | 10 ++++++++++ hrp/step_ui_test.go | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 0097c6bb..7f63767a 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -23,6 +23,7 @@ const ( cameraStop MobileMethod = "camera_stop" recordStart MobileMethod = "record_start" recordStop MobileMethod = "record_stop" + uiHome MobileMethod = "home" uiClick MobileMethod = "click" uiDoubleClick MobileMethod = "double_click" uiLongClick MobileMethod = "long_click" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index cf162b41..2607a038 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -33,6 +33,14 @@ func (s *StepIOS) InstallApp(path string) *StepIOS { return s } +func (s *StepIOS) Home() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiHome, + Params: nil, + }) + return &StepIOS{step: s.step} +} + func (s *StepIOS) Click(params interface{}) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ Method: uiClick, @@ -345,6 +353,8 @@ func (w *wdaClient) doAction(action MobileAction) error { case appStart: // TODO return errActionNotImplemented + case uiHome: + return w.Driver.Homescreen() case uiClick: // click on coordinate if location, ok := action.Params.([]int); ok { diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 2dc01076..02739b51 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -32,7 +32,7 @@ func TestIOSSettingsAction(t *testing.T) { Config: NewConfig("ios ui action on Settings"), TestSteps: []IStep{ NewStep("launch Settings"). - IOS().Click("//*[@label='设置']"). + IOS().Home().Click("//*[@label='设置']"). Validate(). AssertNameExists("飞行模式", "「飞行模式」不存在"). AssertNameNotExists("飞行模式2", "「飞行模式2」不存在"), @@ -52,7 +52,7 @@ func TestIOSDouyinAction(t *testing.T) { Config: NewConfig("ios ui action on 抖音"), TestSteps: []IStep{ NewStep("launch douyin"). - IOS().Click("//*[@label='抖音']"). + IOS().Home().Click("//*[@label='抖音']"). Validate(). AssertNameExists("首页", "首页 tab 不存在"). AssertNameExists("消息", "消息 tab 不存在"), From 7915e50dafcc1e09e16ca7b648796eee9595caf8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 27 Jul 2022 22:52:26 +0800 Subject: [PATCH 012/169] feat: input text on current active element --- hrp/step_ios_ui.go | 7 +++++-- hrp/step_ui_test.go | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 2607a038..b4795446 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -428,8 +428,11 @@ func (w *wdaClient) doAction(action MobileAction) error { } return w.Driver.Swipe(fromX, fromY, toX, toY) case uiInput: - // TODO - return errActionNotImplemented + // input text on current active element + // append \n to send text with enter + // send \b\b\b to delete 3 chars + param := fmt.Sprintf("%v", action.Params) + return w.Driver.SendKeys(param) case appClick: // TODO return errActionNotImplemented diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 02739b51..6022c73f 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -47,6 +47,25 @@ func TestIOSSettingsAction(t *testing.T) { } } +func TestIOSSearchApp(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on Search App 资源库"), + TestSteps: []IStep{ + NewStep("进入 App 资源库 搜索框"). + IOS().Home().SwipeLeft().SwipeLeft().Click("dewey-search-field"). + Validate(). + AssertNameExists("取消", "「取消」不存在"), + NewStep("搜索抖音"). + IOS().Input("抖音\n"), + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + func TestIOSDouyinAction(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 抖音"), From 8e5469a9b42be8b1e59a18bd216d213bdbc90ad8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 29 Jul 2022 15:34:47 +0800 Subject: [PATCH 013/169] fix: set snapshotMaxDepth to avoid dump too many levels of hierarchy --- hrp/step_ios_ui.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index b4795446..4f43846f 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -248,6 +248,12 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } + // set snapshotMaxDepth to avoid dump too many levels of hierarchy + settings, err := driver.SetAppiumSettings(map[string]interface{}{"snapshotMaxDepth": 10}) + if err != nil { + return nil, errors.Wrap(err, "failed to set snapshotMaxDepth in appium WDA settings") + } + log.Info().Interface("appiumWDASettings", settings).Msg("set snapshotMaxDepth in appium WDA settings") // get device window size windowSize, err := driver.WindowSize() From 661302569557237e54fda0e51be82bb6b53d4a91 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 29 Jul 2022 16:02:17 +0800 Subject: [PATCH 014/169] feat: run last action with given times --- hrp/step_ios_ui.go | 21 +++++++++++++++++++++ hrp/step_ui_test.go | 27 +++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 4f43846f..1f0e307d 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -121,6 +121,27 @@ func (s *StepIOS) StartAppByClick(name string) *StepIOS { return &StepIOS{step: s.step} } +// run last action with given times +func (s *StepIOS) Times(n int) *StepIOS { + if n <= 0 { + log.Warn().Int("n", n).Msg("times should be positive, set to 1") + n = 1 + } + + actionsTotal := len(s.step.IOS.Actions) + if actionsTotal == 0 { + return s + } + + // actionsTotal >=1 && n >= 1 + lastAction := s.step.IOS.Actions[actionsTotal-1 : actionsTotal][0] + for i := 0; i < n-1; i++ { + // duplicate last action n-1 times + s.step.IOS.Actions = append(s.step.IOS.Actions, lastAction) + } + return &StepIOS{step: s.step} +} + // Validate switches to step validation. func (s *StepIOS) Validate() *StepIOSValidation { return &StepIOSValidation{ diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 6022c73f..388128a7 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -52,7 +52,7 @@ func TestIOSSearchApp(t *testing.T) { Config: NewConfig("ios ui action on Search App 资源库"), TestSteps: []IStep{ NewStep("进入 App 资源库 搜索框"). - IOS().Home().SwipeLeft().SwipeLeft().Click("dewey-search-field"). + IOS().Home().SwipeLeft().Times(2).Click("dewey-search-field"). Validate(). AssertNameExists("取消", "「取消」不存在"), NewStep("搜索抖音"). @@ -66,6 +66,29 @@ func TestIOSSearchApp(t *testing.T) { } } +func TestIOSWeixin(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios ui action on 微信"), + TestSteps: []IStep{ + NewStep("启动微信"). + IOS().Home().Click("微信"). + Validate(). + AssertNameExists("通讯录", "微信启动失败,「通讯录」不存在"), + NewStep("进入直播页"). + IOS().Click("发现").Click([]float64{0.5, 0.3}). + Validate(). + AssertNameExists("直播", "「直播」不存在"), + NewStep("向上滑动 5 次"). + IOS().SwipeUp().Times(5), + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + func TestIOSDouyinAction(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 抖音"), @@ -76,7 +99,7 @@ func TestIOSDouyinAction(t *testing.T) { AssertNameExists("首页", "首页 tab 不存在"). AssertNameExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). - IOS().SwipeUp().SwipeUp().SwipeDown(), + IOS().SwipeUp().Times(3).SwipeDown(), }, } From f158d1e05a2bd2bf8c03e500c7ca6301569d99f2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 29 Jul 2022 18:09:18 +0800 Subject: [PATCH 015/169] feat: app launch and terminate --- hrp/step.go | 30 ++++++++++++--------- hrp/step_android_ui.go | 8 ------ hrp/step_ios_ui.go | 60 ++++++++++++++++++++++++++++++++---------- hrp/step_ui_test.go | 21 +++++++++++++++ 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 7f63767a..f4794a89 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -17,19 +17,23 @@ const ( type MobileMethod string const ( - appInstall MobileMethod = "install" - appStart MobileMethod = "app_start" - cameraStart MobileMethod = "camera_start" - cameraStop MobileMethod = "camera_stop" - recordStart MobileMethod = "record_start" - recordStop MobileMethod = "record_stop" - uiHome MobileMethod = "home" - uiClick MobileMethod = "click" - uiDoubleClick MobileMethod = "double_click" - uiLongClick MobileMethod = "long_click" - uiSwipe MobileMethod = "swipe" - uiInput MobileMethod = "input" - appClick MobileMethod = "app_click" + appInstall MobileMethod = "install" + appUninstall MobileMethod = "uninstall" + appStart MobileMethod = "app_start" + appLaunch MobileMethod = "app_launch" // 等待 app 打开并堵塞到 app 首屏加载完成,可以传入 app 的启动参数、环境变量 + appLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app,不堵塞等待,不可传入启动参数 + appTerminate MobileMethod = "app_terminate" + appStop MobileMethod = "app_stop" + cameraStart MobileMethod = "camera_start" + cameraStop MobileMethod = "camera_stop" + recordStart MobileMethod = "record_start" + recordStop MobileMethod = "record_stop" + uiHome MobileMethod = "home" + uiClick MobileMethod = "click" + uiDoubleClick MobileMethod = "double_click" + uiLongClick MobileMethod = "long_click" + uiSwipe MobileMethod = "swipe" + uiInput MobileMethod = "input" ) type MobileAction struct { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 106c5a76..8ea3db2c 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -136,14 +136,6 @@ func (s *StepAndroid) Input(text string) *StepAndroid { return &StepAndroid{step: s.step} } -func (s *StepAndroid) StartAppByClick(name string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: appClick, - Params: name, - }) - return &StepAndroid{step: s.step} -} - // Validate switches to step validation. func (s *StepAndroid) Validate() *StepAndroidValidation { return &StepAndroidValidation{ diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 1f0e307d..e3406165 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -33,6 +33,30 @@ func (s *StepIOS) InstallApp(path string) *StepIOS { return s } +func (s *StepIOS) AppLaunch(bundleId string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: appLaunch, + Params: bundleId, + }) + return s +} + +func (s *StepIOS) AppLaunchUnattached(bundleId string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: appLaunchUnattached, + Params: bundleId, + }) + return s +} + +func (s *StepIOS) AppTerminate(bundleId string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: appTerminate, + Params: bundleId, + }) + return s +} + func (s *StepIOS) Home() *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ Method: uiHome, @@ -113,14 +137,6 @@ func (s *StepIOS) Input(text string) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) StartAppByClick(name string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: appClick, - Params: name, - }) - return &StepIOS{step: s.step} -} - // run last action with given times func (s *StepIOS) Times(n int) *StepIOS { if n <= 0 { @@ -377,9 +393,28 @@ func (w *wdaClient) doAction(action MobileAction) error { case appInstall: // TODO return errActionNotImplemented - case appStart: - // TODO - return errActionNotImplemented + case appLaunch: + if bundleId, ok := action.Params.(string); ok { + return w.Driver.AppLaunch(bundleId) + } + return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params) + case appLaunchUnattached: + if bundleId, ok := action.Params.(string); ok { + return w.Driver.AppLaunchUnattached(bundleId) + } + return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params) + case appTerminate: + if bundleId, ok := action.Params.(string); ok { + success, err := w.Driver.AppTerminate(bundleId) + if err != nil { + return errors.Wrap(err, "failed to terminate app") + } + if !success { + log.Warn().Str("bundleId", bundleId).Msg("app was not running") + } + return nil + } + return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) case uiHome: return w.Driver.Homescreen() case uiClick: @@ -460,9 +495,6 @@ func (w *wdaClient) doAction(action MobileAction) error { // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) return w.Driver.SendKeys(param) - case appClick: - // TODO - return errActionNotImplemented } return nil } diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index 388128a7..fa3fc767 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -66,6 +66,27 @@ func TestIOSSearchApp(t *testing.T) { } } +func TestIOSAppLaunch(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("启动 & 关闭 App"), + TestSteps: []IStep{ + NewStep("终止今日头条"). + IOS().AppTerminate("com.ss.iphone.article.News"), + NewStep("启动今日头条"). + IOS().AppLaunch("com.ss.iphone.article.News"), + NewStep("终止今日头条"). + IOS().AppTerminate("com.ss.iphone.article.News"), + NewStep("启动今日头条"). + IOS().AppLaunchUnattached("com.ss.iphone.article.News"), + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + func TestIOSWeixin(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 微信"), From ac438c68d9e96988b3c8344413c7a6707f11adc5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 29 Jul 2022 22:56:44 +0800 Subject: [PATCH 016/169] feat: set acceptAlertButtonSelector --- hrp/step_ios_ui.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index e3406165..9c687f63 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -9,6 +9,21 @@ import ( "github.com/rs/zerolog/log" ) +const ( + // Changes the value of maximum depth for traversing elements source tree. + // It may help to prevent out of memory or timeout errors while getting the elements source tree, + // but it might restrict the depth of source tree. + // A part of elements source tree might be lost if the value was too small. Defaults to 50 + snapshotMaxDepth = 10 + // Allows to customize accept/dismiss alert button selector. + // It helps you to handle an arbitrary element as accept button in accept alert command. + // The selector should be a valid class chain expression, where the search root is the alert element itself. + // The default button location algorithm is used if the provided selector is wrong or does not match any element. + // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] + acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" + dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" +) + type IOSAction struct { MobileAction UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` @@ -281,16 +296,20 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { } // init WDA driver - driver, err := gwda.NewUSBDriver(nil, *targetDevice) + capabilities := gwda.NewCapabilities() + capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) + driver, err := gwda.NewUSBDriver(capabilities, *targetDevice) if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } - // set snapshotMaxDepth to avoid dump too many levels of hierarchy - settings, err := driver.SetAppiumSettings(map[string]interface{}{"snapshotMaxDepth": 10}) + settings, err := driver.SetAppiumSettings(map[string]interface{}{ + "snapshotMaxDepth": snapshotMaxDepth, + "acceptAlertButtonSelector": acceptAlertButtonSelector, + }) if err != nil { - return nil, errors.Wrap(err, "failed to set snapshotMaxDepth in appium WDA settings") + return nil, errors.Wrap(err, "failed to set appium WDA settings") } - log.Info().Interface("appiumWDASettings", settings).Msg("set snapshotMaxDepth in appium WDA settings") + log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") // get device window size windowSize, err := driver.WindowSize() From 8a1128a1a3b0b8967311dfd238cc97b614793194 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 30 Jul 2022 00:40:05 +0800 Subject: [PATCH 017/169] feat: take snapshot for each step --- hrp/step_ios_ui.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 9c687f63..43c7f811 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -2,11 +2,19 @@ package hrp import ( "fmt" + "image" + "image/jpeg" + "image/png" + "os" + "path/filepath" "strings" + "time" "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) const ( @@ -386,6 +394,13 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro } } + // take snapshot + log.Info().Str("name", step.Name).Msg("take snapshot") + err = wdaClient.screenshot() + if err != nil { + log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") + } + // validate validateResults, err := wdaClient.doValidation(step.Validators) if err != nil { @@ -405,6 +420,47 @@ type wdaClient struct { WindowSize gwda.Size } +func (w *wdaClient) screenshot() error { + raw, err := w.Driver.Screenshot() + if err != nil { + return errors.Wrap(err, "screenshot by WDA failed") + } + + img, format, err := image.Decode(raw) + if err != nil { + return errors.Wrap(err, "decode screenshot image failed") + } + + dir, _ := os.Getwd() + screenshotsDir := filepath.Join(dir, "screenshots") + if err := builtin.EnsureFolderExists(screenshotsDir); err != nil { + return errors.Wrap(err, "create screenshots failed") + } + path := filepath.Join(screenshotsDir, fmt.Sprintf("%d", time.Now().UnixMilli())+"."+format) + file, err := os.Create(path) + if err != nil { + return errors.Wrap(err, "create screenshot image file failed") + } + defer func() { + _ = file.Close() + }() + + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + default: + return fmt.Errorf("unsupported image format: %s", format) + } + if err != nil { + return errors.Wrap(err, "encode screenshot image failed") + } + + log.Info().Str("path", path).Msg("screenshot generated") + return nil +} + func (w *wdaClient) doAction(action MobileAction) error { log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") From 89e209eb23095982e0c60ea01d997c811fb19aa7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 30 Jul 2022 17:45:39 +0800 Subject: [PATCH 018/169] feat: sleep n seconds after last action --- hrp/step.go | 1 + hrp/step_ios_ui.go | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/hrp/step.go b/hrp/step.go index f4794a89..9b6f070a 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -34,6 +34,7 @@ const ( uiLongClick MobileMethod = "long_click" uiSwipe MobileMethod = "swipe" uiInput MobileMethod = "input" + ctlSleep MobileMethod = "sleep" ) type MobileAction struct { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 43c7f811..13189fc9 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -160,7 +160,7 @@ func (s *StepIOS) Input(text string) *StepIOS { return &StepIOS{step: s.step} } -// run last action with given times +// Times specify running times for run last action func (s *StepIOS) Times(n int) *StepIOS { if n <= 0 { log.Warn().Int("n", n).Msg("times should be positive, set to 1") @@ -181,6 +181,15 @@ func (s *StepIOS) Times(n int) *StepIOS { return &StepIOS{step: s.step} } +// Sleep specify sleep seconds after last action +func (s *StepIOS) Sleep(n int) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: ctlSleep, + Params: n, + }) + return &StepIOS{step: s.step} +} + // Validate switches to step validation. func (s *StepIOS) Validate() *StepIOSValidation { return &StepIOSValidation{ @@ -570,6 +579,12 @@ func (w *wdaClient) doAction(action MobileAction) error { // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) return w.Driver.SendKeys(param) + case ctlSleep: + if param, ok := action.Params.(int); ok { + time.Sleep(time.Duration(param) * time.Second) + return nil + } + return fmt.Errorf("invalid sleep params: %v", action.Params) } return nil } From effec946d22a81ddbcd43a1f40d53f66a39ab0a3 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 Jul 2022 12:15:55 +0800 Subject: [PATCH 019/169] feat: add screenshot --- hrp/step.go | 1 + hrp/step_ios_ui.go | 18 +++++++++++++++--- hrp/step_ui_test.go | 17 ++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 9b6f070a..721e70a8 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -34,6 +34,7 @@ const ( uiLongClick MobileMethod = "long_click" uiSwipe MobileMethod = "swipe" uiInput MobileMethod = "input" + ctlScreenShot MobileMethod = "screenshot" ctlSleep MobileMethod = "sleep" ) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 13189fc9..425856c0 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -190,6 +190,14 @@ func (s *StepIOS) Sleep(n int) *StepIOS { return &StepIOS{step: s.step} } +func (s *StepIOS) ScreenShot() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: ctlScreenShot, + Params: nil, + }) + return &StepIOS{step: s.step} +} + // Validate switches to step validation. func (s *StepIOS) Validate() *StepIOSValidation { return &StepIOSValidation{ @@ -404,8 +412,8 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // take snapshot - log.Info().Str("name", step.Name).Msg("take snapshot") - err = wdaClient.screenshot() + log.Info().Str("name", step.Name).Msg("take snapshot before validation") + err = wdaClient.screenShot() if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } @@ -429,7 +437,7 @@ type wdaClient struct { WindowSize gwda.Size } -func (w *wdaClient) screenshot() error { +func (w *wdaClient) screenShot() error { raw, err := w.Driver.Screenshot() if err != nil { return errors.Wrap(err, "screenshot by WDA failed") @@ -585,6 +593,10 @@ func (w *wdaClient) doAction(action MobileAction) error { return nil } return fmt.Errorf("invalid sleep params: %v", action.Params) + case ctlScreenShot: + // take snapshot + log.Info().Msg("take snapshot for current screen") + w.screenShot() } return nil } diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index fa3fc767..f888b55a 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -87,20 +87,27 @@ func TestIOSAppLaunch(t *testing.T) { } } -func TestIOSWeixin(t *testing.T) { +func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ - Config: NewConfig("ios ui action on 微信"), + Config: NewConfig("ios ui action on 微信直播"), TestSteps: []IStep{ NewStep("启动微信"). - IOS().Home().Click("微信"). + IOS(). + Home(). + AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 + Click("微信"). Validate(). AssertNameExists("通讯录", "微信启动失败,「通讯录」不存在"), NewStep("进入直播页"). - IOS().Click("发现").Click([]float64{0.5, 0.3}). + IOS(). + Click("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 + Click([]float64{0.5, 0.3}). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 Validate(). AssertNameExists("直播", "「直播」不存在"), NewStep("向上滑动 5 次"). - IOS().SwipeUp().Times(5), + IOS(). + SwipeUp().Times(3).ScreenShot(). // 上划 3 次,截图保存 + SwipeUp().Times(2).ScreenShot(), // 再上划 2 次,截图保存 }, } From 744aa8417a9a3c9fd72cdc470f8d6f41c88c56de Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 Jul 2022 14:50:11 +0800 Subject: [PATCH 020/169] feat: switch to iOS springboard before init WDA session --- hrp/step_ios_ui.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 425856c0..09fe1ac6 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -320,6 +320,15 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { return client, nil } + // switch to iOS springboard before init WDA session + // aviod getting stuck when some super app is activate such as douyin or wexin + log.Info().Msg("switch to iOS springboard") + bundleID := "com.apple.springboard" + _, err = targetDevice.GIDevice().AppLaunch(bundleID) + if err != nil { + return nil, errors.Wrap(err, "launch springboard failed") + } + // init WDA driver capabilities := gwda.NewCapabilities() capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) @@ -345,6 +354,7 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { // cache wda client r.wdaClients = make(map[string]*wdaClient) client = &wdaClient{ + Device: targetDevice, Driver: driver, WindowSize: windowSize, } @@ -433,12 +443,18 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro var errActionNotImplemented = errors.New("UI action not implemented") type wdaClient struct { + Device *gwda.Device Driver gwda.WebDriver WindowSize gwda.Size } +// screenShot takes screenshot and saves image file to $CWD/screenshots/ folder func (w *wdaClient) screenShot() error { - raw, err := w.Driver.Screenshot() + // gidevice 和 gwda 均可实现截图功能,但 gidevice 的截图性能更优 + // gwda 通过 wda 请求获取(分辨率、响应时间均由 wda 决定) + // gidevice 直接通过 Apple 允许的底层通信获取 + // raw, err := w.Driver.Screenshot() + raw, err := w.Device.GIDevice().Screenshot() if err != nil { return errors.Wrap(err, "screenshot by WDA failed") } From 237df67c81113fc0a66bec8f104b1f8eb408b106 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 Jul 2022 15:29:29 +0800 Subject: [PATCH 021/169] feat: start & stop camera --- hrp/step.go | 22 ++++++++++--------- hrp/step_android_ui.go | 4 ++-- hrp/step_ios_ui.go | 31 +++++++++++++++++++++++++- hrp/step_ui_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 721e70a8..c05a3166 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -24,18 +24,20 @@ const ( appLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app,不堵塞等待,不可传入启动参数 appTerminate MobileMethod = "app_terminate" appStop MobileMethod = "app_stop" - cameraStart MobileMethod = "camera_start" - cameraStop MobileMethod = "camera_stop" - recordStart MobileMethod = "record_start" - recordStop MobileMethod = "record_stop" - uiHome MobileMethod = "home" - uiClick MobileMethod = "click" - uiDoubleClick MobileMethod = "double_click" - uiLongClick MobileMethod = "long_click" - uiSwipe MobileMethod = "swipe" - uiInput MobileMethod = "input" ctlScreenShot MobileMethod = "screenshot" ctlSleep MobileMethod = "sleep" + ctlStartCamera MobileMethod = "camera_start" // alias for app_launch camera + ctlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera + recordStart MobileMethod = "record_start" + recordStop MobileMethod = "record_stop" + + // UI handling + uiHome MobileMethod = "home" + uiClick MobileMethod = "click" + uiDoubleClick MobileMethod = "double_click" + uiLongClick MobileMethod = "long_click" + uiSwipe MobileMethod = "swipe" + uiInput MobileMethod = "input" ) type MobileAction struct { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 8ea3db2c..322dfb7b 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -34,7 +34,7 @@ func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { func (s *StepAndroid) StartCamera() *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: cameraStart, + Method: ctlStartCamera, Params: nil, }) return &StepAndroid{step: s.step} @@ -42,7 +42,7 @@ func (s *StepAndroid) StartCamera() *StepAndroid { func (s *StepAndroid) StopCamera() *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: cameraStop, + Method: ctlStopCamera, Params: nil, }) return &StepAndroid{step: s.step} diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 09fe1ac6..1db74798 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -198,6 +198,22 @@ func (s *StepIOS) ScreenShot() *StepIOS { return &StepIOS{step: s.step} } +func (s *StepIOS) StartCamera() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: ctlStartCamera, + Params: nil, + }) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) StopCamera() *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: ctlStopCamera, + Params: nil, + }) + return &StepIOS{step: s.step} +} + // Validate switches to step validation. func (s *StepIOS) Validate() *StepIOSValidation { return &StepIOSValidation{ @@ -612,7 +628,20 @@ func (w *wdaClient) doAction(action MobileAction) error { case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") - w.screenShot() + return w.screenShot() + case ctlStartCamera: + // start camera, alias for app_launch com.apple.camera + return w.Driver.AppLaunch("com.apple.camera") + case ctlStopCamera: + // stop camera, alias for app_terminate com.apple.camera + success, err := w.Driver.AppTerminate("com.apple.camera") + if err != nil { + return errors.Wrap(err, "failed to terminate camera") + } + if !success { + log.Warn().Msg("camera was not running") + } + return nil } return nil } diff --git a/hrp/step_ui_test.go b/hrp/step_ui_test.go index f888b55a..1ec259ae 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ui_test.go @@ -117,6 +117,56 @@ func TestIOSWeixinLive(t *testing.T) { } } +func TestIOSCameraPhotoCapture(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios camera photo capture"), + TestSteps: []IStep{ + NewStep("launch camera"). + IOS().Home(). + StopCamera(). + StartCamera(). + Validate(). + AssertNameExists("PhotoCapture", "拍照按钮不存在"), + NewStep("start recording"). + IOS().Click("PhotoCapture"), + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + +func TestIOSCameraVideoCapture(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("ios camera video capture"), + TestSteps: []IStep{ + NewStep("launch camera"). + IOS().Home(). + StopCamera(). + StartCamera(). + Validate(). + AssertNameExists("PhotoCapture", "录像按钮不存在"), + NewStep("switch to video capture"). + IOS(). + SwipeRight(). + Validate(). + AssertNameExists("VideoCapture", "拍摄按钮不存在"), + NewStep("start recording"). + IOS(). + Click("VideoCapture"). // 开始录像 + Sleep(5). + Click("VideoCapture"), // 停止录像 + }, + } + + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} + func TestIOSDouyinAction(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 抖音"), From 17fb17f95feeeb839e17a5686345a781d46c7a9e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 31 Jul 2022 20:52:11 +0800 Subject: [PATCH 022/169] change: upgrade gwda --- go.mod | 2 +- go.sum | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 3cfe23b2..0612d3fa 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 - github.com/electricbubble/gwda v0.3.0 + github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 github.com/go-errors/errors v1.0.1 diff --git a/go.sum b/go.sum index 1e0bd26b..6b6a1c15 100644 --- a/go.sum +++ b/go.sum @@ -99,10 +99,10 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/electricbubble/gidevice v0.0.4 h1:PbOt4AngNQTtO5j0vCZ3Xcj9mByDtZmjBYLTh8PJ9kc= -github.com/electricbubble/gidevice v0.0.4/go.mod h1:hWRHIPf4uyiEB56hnVHVvu6MoVg7RlJY8ZV2FVgLKZA= -github.com/electricbubble/gwda v0.3.0 h1:uQMZxmp5D51iMsXrWfi21MlftrkPmOeLDE+gtw06fg4= -github.com/electricbubble/gwda v0.3.0/go.mod h1:co3ynSIVXEyI3aKdzfjqkFDFloFcxhc+e27U0ajyZsM= +github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= +github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= +github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588 h1:CAq1AvRKkG8/PX6LCLwYpERq/TIXkVNTXq4MJEWGlG0= +github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -319,6 +319,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -886,6 +888,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -907,8 +910,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 0407c0239129a9a84ca935fc436aae866b6d5e1e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 15 Aug 2022 21:53:33 +0800 Subject: [PATCH 023/169] change: make assert msg optional --- hrp/step.go | 6 ++ hrp/step_android_ui.go | 31 +++++-- hrp/step_android_ui_test.go | 28 ++++++ hrp/step_ios_ui.go | 64 +++++++++----- hrp/{step_ui_test.go => step_ios_ui_test.go} | 93 ++++++++------------ 5 files changed, 139 insertions(+), 83 deletions(-) create mode 100644 hrp/step_android_ui_test.go rename hrp/{step_ui_test.go => step_ios_ui_test.go} (72%) diff --git a/hrp/step.go b/hrp/step.go index c05a3166..dcc2c7ff 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -38,6 +38,12 @@ const ( uiLongClick MobileMethod = "long_click" uiSwipe MobileMethod = "swipe" uiInput MobileMethod = "input" + + // UI validation + assertionNameExists string = "name_exists" + assertionNameNotExists string = "name_not_exists" + assertionXpathExists string = "xpath_exists" + assertionXpathNotExists string = "xpath_not_exists" ) type MobileAction struct { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 322dfb7b..60b9cc3a 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -1,5 +1,7 @@ package hrp +import "fmt" + type AndroidAction struct { MobileAction Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` @@ -164,12 +166,31 @@ type StepAndroidValidation struct { step *TStep } -func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg string) *StepAndroidValidation { +func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: "UI", - Assert: "xpath_exists", - Expect: expectedXpath, - Message: msg, + Check: "UI", + Assert: assertionXpathExists, + Expect: expectedXpath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("xpath [%s] not found", expectedXpath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: "UI", + Assert: assertionXpathNotExists, + Expect: expectedXpath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("xpath [%s] should not exist", expectedXpath) } s.step.Validators = append(s.step.Validators, v) return s diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go new file mode 100644 index 00000000..13d3cba9 --- /dev/null +++ b/hrp/step_android_ui_test.go @@ -0,0 +1,28 @@ +package hrp + +import ( + "fmt" + "testing" +) + +func TestAndroidAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("android ui action"), + TestSteps: []IStep{ + NewStep("launch douyin"). + Android().Serial("xxx").Click("抖音"). + Validate(). + AssertXpathExists("首页", "首页 tab 不存在"). + AssertXpathExists("消息", "消息 tab 不存在"), + NewStep("swipe up and down"). + Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), + }, + } + tCase := testCase.ToTCase() + fmt.Println(tCase) + + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } +} diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 1db74798..3e7b3adc 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -242,45 +242,61 @@ type StepIOSValidation struct { step *TStep } -func (s *StepIOSValidation) AssertNameExists(expectedName string, msg string) *StepIOSValidation { +func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: "name_exists", - Expect: expectedName, - Message: msg, + Check: "UI", + Assert: assertionNameExists, + Expect: expectedName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] not found", expectedName) } s.step.Validators = append(s.step.Validators, v) return s } -func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg string) *StepIOSValidation { +func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: "name_not_exists", - Expect: expectedName, - Message: msg, + Check: "UI", + Assert: assertionNameNotExists, + Expect: expectedName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] should not exist", expectedName) } s.step.Validators = append(s.step.Validators, v) return s } -func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg string) *StepIOSValidation { +func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: "xpath_exists", - Expect: expectedXpath, - Message: msg, + Check: "UI", + Assert: assertionXpathExists, + Expect: expectedXpath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("xpath [%s] not found", expectedXpath) } s.step.Validators = append(s.step.Validators, v) return s } -func (s *StepIOSValidation) AssertXpathNotExists(expectedXpath string, msg string) *StepIOSValidation { +func (s *StepIOSValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: "xpath_not_exists", - Expect: expectedXpath, - Message: msg, + Check: "UI", + Assert: assertionXpathNotExists, + Expect: expectedXpath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("xpath [%s] should not exist", expectedXpath) } s.step.Validators = append(s.step.Validators, v) return s @@ -673,13 +689,13 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* var result bool switch validator.Assert { - case "xpath_exists": + case assertionXpathExists: result = w.assertXpath(expected, true) - case "xpath_not_exists": + case assertionXpathNotExists: result = w.assertXpath(expected, false) - case "name_exists": + case assertionNameExists: result = w.assertName(expected, true) - case "name_not_exists": + case assertionNameNotExists: result = w.assertName(expected, false) } if result { diff --git a/hrp/step_ui_test.go b/hrp/step_ios_ui_test.go similarity index 72% rename from hrp/step_ui_test.go rename to hrp/step_ios_ui_test.go index 1ec259ae..7a836cdd 100644 --- a/hrp/step_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -5,28 +5,6 @@ import ( "testing" ) -func TestAndroidAction(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("android ui action"), - TestSteps: []IStep{ - NewStep("launch douyin"). - Android().Serial("xxx").Click("抖音"). - Validate(). - AssertXpathExists("首页", "首页 tab 不存在"). - AssertXpathExists("消息", "消息 tab 不存在"), - NewStep("swipe up and down"). - Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), - }, - } - tCase := testCase.ToTCase() - fmt.Println(tCase) - - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } -} - func TestIOSSettingsAction(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on Settings"), @@ -34,17 +12,18 @@ func TestIOSSettingsAction(t *testing.T) { NewStep("launch Settings"). IOS().Home().Click("//*[@label='设置']"). Validate(). - AssertNameExists("飞行模式", "「飞行模式」不存在"). - AssertNameNotExists("飞行模式2", "「飞行模式2」不存在"), + AssertNameExists("飞行模式"). + AssertNameNotExists("飞行模式2"), NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSSearchApp(t *testing.T) { @@ -54,16 +33,17 @@ func TestIOSSearchApp(t *testing.T) { NewStep("进入 App 资源库 搜索框"). IOS().Home().SwipeLeft().Times(2).Click("dewey-search-field"). Validate(). - AssertNameExists("取消", "「取消」不存在"), + AssertNameExists("取消"), NewStep("搜索抖音"). IOS().Input("抖音\n"), }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSAppLaunch(t *testing.T) { @@ -80,11 +60,12 @@ func TestIOSAppLaunch(t *testing.T) { IOS().AppLaunchUnattached("com.ss.iphone.article.News"), }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSWeixinLive(t *testing.T) { @@ -103,18 +84,19 @@ func TestIOSWeixinLive(t *testing.T) { Click("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 Click([]float64{0.5, 0.3}). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 Validate(). - AssertNameExists("直播", "「直播」不存在"), + AssertNameExists("直播"), NewStep("向上滑动 5 次"). IOS(). SwipeUp().Times(3).ScreenShot(). // 上划 3 次,截图保存 SwipeUp().Times(2).ScreenShot(), // 再上划 2 次,截图保存 }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSCameraPhotoCapture(t *testing.T) { @@ -131,11 +113,12 @@ func TestIOSCameraPhotoCapture(t *testing.T) { IOS().Click("PhotoCapture"), }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSCameraVideoCapture(t *testing.T) { @@ -160,11 +143,12 @@ func TestIOSCameraVideoCapture(t *testing.T) { Click("VideoCapture"), // 停止录像 }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } func TestIOSDouyinAction(t *testing.T) { @@ -180,9 +164,10 @@ func TestIOSDouyinAction(t *testing.T) { IOS().SwipeUp().Times(3).SwipeDown(), }, } + fmt.Println(testCase) - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } + // err := NewRunner(t).Run(testCase) + // if err != nil { + // t.Fatal(err) + // } } From df3b56c18c26a8d5cc7a1ebff9e2c0e6b9209247 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 15 Aug 2022 21:57:03 +0800 Subject: [PATCH 024/169] change: upgrade gwda to 0.4.0 --- go.mod | 2 +- go.sum | 4 ++-- hrp/step_ios_ui.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a0a6d705..6eb56b61 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 - github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588 + github.com/electricbubble/gwda v0.4.0 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 github.com/go-errors/errors v1.0.1 diff --git a/go.sum b/go.sum index 68b84e77..09756429 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= -github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588 h1:CAq1AvRKkG8/PX6LCLwYpERq/TIXkVNTXq4MJEWGlG0= -github.com/electricbubble/gwda v0.3.1-0.20220731124432-07f451783588/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= +github.com/electricbubble/gwda v0.4.0 h1:+Sbi8WRM8sXh0cXpmY97GiWuR1CNQ/v2tsMUyWxlnV4= +github.com/electricbubble/gwda v0.4.0/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 3e7b3adc..68dc425e 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -501,7 +501,7 @@ func (w *wdaClient) screenShot() error { if err := builtin.EnsureFolderExists(screenshotsDir); err != nil { return errors.Wrap(err, "create screenshots failed") } - path := filepath.Join(screenshotsDir, fmt.Sprintf("%d", time.Now().UnixMilli())+"."+format) + path := filepath.Join(screenshotsDir, fmt.Sprintf("%d", time.Now().Unix())+"."+format) file, err := os.Create(path) if err != nil { return errors.Wrap(err, "create screenshot image file failed") From ec79d78192fc2887c4fc28ca306c9cb243646102 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 15 Aug 2022 22:34:17 +0800 Subject: [PATCH 025/169] change: assert name/xpath exists --- hrp/step.go | 9 +++++---- hrp/step_android_ui.go | 8 ++++---- hrp/step_ios_ui.go | 41 ++++++++++++++++++++++------------------- hrp/step_ios_ui_test.go | 16 ++++++++-------- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index dcc2c7ff..4af50755 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -40,10 +40,11 @@ const ( uiInput MobileMethod = "input" // UI validation - assertionNameExists string = "name_exists" - assertionNameNotExists string = "name_not_exists" - assertionXpathExists string = "xpath_exists" - assertionXpathNotExists string = "xpath_not_exists" + uiSelectorName string = "ui_name" + uiSelectorXpath string = "ui_xpath" + uiSelectorOCR string = "ui_ocr" + assertionExists string = "exists" + assertionNotExists string = "not_exists" ) type MobileAction struct { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 60b9cc3a..d3fecb26 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -168,8 +168,8 @@ type StepAndroidValidation struct { func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: "UI", - Assert: assertionXpathExists, + Check: uiSelectorXpath, + Assert: assertionExists, Expect: expectedXpath, } if len(msg) > 0 { @@ -183,8 +183,8 @@ func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg ...s func (s *StepAndroidValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: "UI", - Assert: assertionXpathNotExists, + Check: uiSelectorXpath, + Assert: assertionNotExists, Expect: expectedXpath, } if len(msg) > 0 { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 68dc425e..a4987341 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -244,8 +244,8 @@ type StepIOSValidation struct { func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: assertionNameExists, + Check: uiSelectorName, + Assert: assertionExists, Expect: expectedName, } if len(msg) > 0 { @@ -259,8 +259,8 @@ func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: assertionNameNotExists, + Check: uiSelectorName, + Assert: assertionNotExists, Expect: expectedName, } if len(msg) > 0 { @@ -274,8 +274,8 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: assertionXpathExists, + Check: uiSelectorXpath, + Assert: assertionExists, Expect: expectedXpath, } if len(msg) > 0 { @@ -289,8 +289,8 @@ func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg ...strin func (s *StepIOSValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: "UI", - Assert: assertionXpathNotExists, + Check: uiSelectorXpath, + Assert: assertionNotExists, Expect: expectedXpath, } if len(msg) > 0 { @@ -675,7 +675,7 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* } // parse check value - if validator.Check != "UI" { + if !strings.HasPrefix(validator.Check, "ui_") { validataResult.CheckResult = "skip" log.Warn().Interface("validator", validator).Msg("skip validator") validateResults = append(validateResults, validataResult) @@ -687,17 +687,20 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* return nil, errors.New("validator expect should be string") } - var result bool - switch validator.Assert { - case assertionXpathExists: - result = w.assertXpath(expected, true) - case assertionXpathNotExists: - result = w.assertXpath(expected, false) - case assertionNameExists: - result = w.assertName(expected, true) - case assertionNameNotExists: - result = w.assertName(expected, false) + var exists bool + if validator.Assert == assertionExists { + exists = true + } else { + exists = false } + var result bool + switch validator.Check { + case uiSelectorName: + result = w.assertName(expected, exists) + case uiSelectorXpath: + result = w.assertXpath(expected, exists) + } + if result { log.Info(). Str("assert", validator.Assert). diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 7a836cdd..0ed9120f 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -20,10 +20,10 @@ func TestIOSSettingsAction(t *testing.T) { } fmt.Println(testCase) - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSSearchApp(t *testing.T) { @@ -93,10 +93,10 @@ func TestIOSWeixinLive(t *testing.T) { } fmt.Println(testCase) - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSCameraPhotoCapture(t *testing.T) { From 3b73571713ac400a17c63f9882047bf4fcf83da7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 23 Aug 2022 20:45:34 +0800 Subject: [PATCH 026/169] feat: init device with optional serial, port, mjpeg port --- docs/CHANGELOG.md | 4 +++ go.mod | 1 + go.sum | 4 +-- hrp/config.go | 25 +++++++++++++++++- hrp/runner.go | 10 +++++++- hrp/step.go | 4 +-- hrp/step_android_ui.go | 2 +- hrp/step_ios_ui.go | 57 +++++++++++++++++++---------------------- hrp/step_ios_ui_test.go | 3 ++- hrp/step_request.go | 4 +-- 10 files changed, 74 insertions(+), 40 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index afb0344d..411982f1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## v4.2.1 (2022-08-23) + +- feat: support iOS UI automation with WebDriverAgent + ## v4.2.0 (2022-08-21) **go version** diff --git a/go.mod b/go.mod index 6eb56b61..1853a9af 100644 --- a/go.mod +++ b/go.mod @@ -38,3 +38,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin +replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992 diff --git a/go.sum b/go.sum index 09756429..294e362f 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992 h1:z5dL+1EechBejcrY+zpeGUTuvUDnP6bTWroyhdqwzas= +github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992/go.mod h1:0kmE3KaUs6RECo+wkeSbmfJjQb/anVphrf3mvFvssJ0= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= @@ -101,8 +103,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= -github.com/electricbubble/gwda v0.4.0 h1:+Sbi8WRM8sXh0cXpmY97GiWuR1CNQ/v2tsMUyWxlnV4= -github.com/electricbubble/gwda v0.4.0/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/hrp/config.go b/hrp/config.go index 3ee1264a..103ea986 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -29,6 +29,7 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"` + IOS []*IOSConfig `json:"ios,omitempty" yaml:"ios,omitempty"` Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -90,12 +91,34 @@ func (c *TConfig) SetWeight(weight int) *TConfig { return c } -func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) { +func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { c.WebSocketSetting = &WebSocketConfig{ ReconnectionTimes: times, ReconnectionInterval: interval, MaxMessageSize: size, } + return c +} + +func (c *TConfig) SetIOS(device WDADevice) *TConfig { + // each device can have its own settings + if device.UDID != "" { + c.IOS = append(c.IOS, &IOSConfig{ + WDADevice: device, + }) + return c + } + + // device UDID is not specified ,settings will be shared + iosConfig := &IOSConfig{ + WDADevice: device, + } + if len(c.IOS) == 0 { + c.IOS = append(c.IOS, iosConfig) + } else { + c.IOS[0] = iosConfig + } + return c } type ThinkTimeConfig struct { diff --git a/hrp/runner.go b/hrp/runner.go index d4e4f9b1..0a7002f9 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -12,12 +12,12 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/httprunner/funplugin" "github.com/jinzhu/copier" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/net/http2" - "github.com/httprunner/funplugin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -384,6 +384,14 @@ func (r *testCaseRunner) parseConfig() error { } r.parametersIterator = parametersIterator + // init iOS WDA clients + for _, iosDeviceConfig := range r.parsedConfig.IOS { + _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig.WDADevice) + if err != nil { + return errors.Wrap(err, "init iOS WDA client failed") + } + } + return nil } diff --git a/hrp/step.go b/hrp/step.go index 4af50755..63390399 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -75,8 +75,8 @@ type TStep struct { Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"` ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocket *WebSocketAction `json:"websocket,omitempty" yaml:"websocket,omitempty"` - Android *AndroidAction `json:"android,omitempty" yaml:"android,omitempty"` - IOS *IOSAction `json:"ios,omitempty" yaml:"ios,omitempty"` + Android *AndroidStep `json:"android,omitempty" yaml:"android,omitempty"` + IOS *IOSStep `json:"ios,omitempty" yaml:"ios,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index d3fecb26..14eb76d8 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -2,7 +2,7 @@ package hrp import "fmt" -type AndroidAction struct { +type AndroidStep struct { MobileAction Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index a4987341..016745d0 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -32,9 +32,19 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) -type IOSAction struct { +type IOSConfig struct { + WDADevice +} + +type WDADevice struct { + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` +} + +type IOSStep struct { + WDADevice MobileAction - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } @@ -318,7 +328,7 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { +func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err error) { defer func() { if err != nil { return @@ -336,13 +346,24 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { }() // avoid duplicate init - if udid == "" && len(r.wdaClients) == 1 { + if device.UDID == "" && len(r.wdaClients) == 1 { for _, v := range r.wdaClients { return v, nil } } - targetDevice, err := getAttachedIOSDevice(udid) + // init wda device + var options []gwda.DeviceOptions + if device.UDID != "" { + options = append(options, gwda.WithSerialNumber(device.UDID)) + } + if device.Port != 0 { + options = append(options, gwda.WithPort(device.Port)) + } + if device.MjpegPort != 0 { + options = append(options, gwda.WithMjpegPort(device.MjpegPort)) + } + targetDevice, err := gwda.NewDevice(options...) if err != nil { return nil, err } @@ -395,30 +416,6 @@ func (r *HRPRunner) InitWDAClient(udid string) (client *wdaClient, err error) { return client, nil } -func getAttachedIOSDevice(udid string) (*gwda.Device, error) { - // get all attached deivces - devices, err := gwda.DeviceList() - if err != nil { - return nil, errors.Wrap(err, "failed to get attached ios devices list") - } - if len(devices) == 0 { - return nil, errors.New("no ios devices attached") - } - - if udid == "" { - return &devices[0], nil - } - - // find device by udid - for _, device := range devices { - if device.SerialNumber() == udid { - return &device, nil - } - } - - return nil, fmt.Errorf("device %s is not attached", udid) -} - func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, @@ -428,7 +425,7 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // init wdaClient driver - wdaClient, err := r.hrpRunner.InitWDAClient(step.IOS.UDID) + wdaClient, err := r.hrpRunner.InitWDAClient(step.IOS.WDADevice) if err != nil { return } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 0ed9120f..78972278 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -70,7 +70,8 @@ func TestIOSAppLaunch(t *testing.T) { func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ - Config: NewConfig("ios ui action on 微信直播"), + Config: NewConfig("ios ui action on 微信直播"). + SetIOS(WDADevice{Port: 8700, MjpegPort: 8800}), TestSteps: []IStep{ NewStep("启动微信"). IOS(). diff --git a/hrp/step_request.go b/hrp/step_request.go index 9c44497b..48a70e4d 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -764,7 +764,7 @@ func (s *StepRequest) WebSocket() *StepWebSocket { // Android creates a new android action func (s *StepRequest) Android() *StepAndroid { - s.step.Android = &AndroidAction{} + s.step.Android = &AndroidStep{} return &StepAndroid{ step: s.step, } @@ -772,7 +772,7 @@ func (s *StepRequest) Android() *StepAndroid { // IOS creates a new ios action func (s *StepRequest) IOS() *StepIOS { - s.step.IOS = &IOSAction{} + s.step.IOS = &IOSStep{} return &StepIOS{ step: s.step, } From df50b8ff87d8efc40226bef96549c8ece76ca92c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 24 Aug 2022 19:00:59 +0800 Subject: [PATCH 027/169] change: update gwda dependency --- go.mod | 2 +- go.sum | 4 ++-- hrp/step_ios_ui.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1853a9af..1f645d7a 100644 --- a/go.mod +++ b/go.mod @@ -38,4 +38,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992 +replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 diff --git a/go.sum b/go.sum index 294e362f..e425df94 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992 h1:z5dL+1EechBejcrY+zpeGUTuvUDnP6bTWroyhdqwzas= -github.com/debugtalk/gwda v0.0.0-20220823102718-fae698f66992/go.mod h1:0kmE3KaUs6RECo+wkeSbmfJjQb/anVphrf3mvFvssJ0= +github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 h1:DjPOXlkeCsxtFzieys2RjYEn6OCoAPQNiLmG2eeSVgw= +github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 016745d0..fdc8f0e3 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -353,7 +353,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro } // init wda device - var options []gwda.DeviceOptions + var options []gwda.DeviceOption if device.UDID != "" { options = append(options, gwda.WithSerialNumber(device.UDID)) } From 624b70ea83d8c874f764346cfff28049820b7c86 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 25 Aug 2022 20:14:56 +0800 Subject: [PATCH 028/169] change: set debug logging for gwda --- hrp/step_ios_ui.go | 5 +++-- hrp/step_ios_ui_test.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index fdc8f0e3..5c24fa85 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -383,6 +383,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro } // init WDA driver + gwda.SetDebug(true) capabilities := gwda.NewCapabilities() capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) driver, err := gwda.NewUSBDriver(capabilities, *targetDevice) @@ -728,7 +729,7 @@ func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { } else { // name selector = gwda.BySelector{ - Name: param, + LinkText: gwda.NewElementAttribute().WithName(param), } } @@ -737,7 +738,7 @@ func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { func (w *wdaClient) assertName(name string, exists bool) bool { selector := gwda.BySelector{ - Name: name, + LinkText: gwda.NewElementAttribute().WithName(name), } _, err := w.Driver.FindElement(selector) return exists == (err == nil) diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 78972278..9ce227bb 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -48,7 +48,8 @@ func TestIOSSearchApp(t *testing.T) { func TestIOSAppLaunch(t *testing.T) { testCase := &TestCase{ - Config: NewConfig("启动 & 关闭 App"), + Config: NewConfig("启动 & 关闭 App"). + SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), TestSteps: []IStep{ NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), @@ -71,7 +72,7 @@ func TestIOSAppLaunch(t *testing.T) { func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 微信直播"). - SetIOS(WDADevice{Port: 8700, MjpegPort: 8800}), + SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), TestSteps: []IStep{ NewStep("启动微信"). IOS(). From 4c624b40cafe893b4dd015961c505ea59734568f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 25 Aug 2022 22:34:08 +0800 Subject: [PATCH 029/169] feat: specify screenshot filename --- hrp/step_ios_ui.go | 49 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5c24fa85..c3fb4538 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -5,6 +5,7 @@ import ( "image" "image/jpeg" "image/png" + "net/http" "os" "path/filepath" "strings" @@ -408,16 +409,20 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro // cache wda client r.wdaClients = make(map[string]*wdaClient) client = &wdaClient{ + ID: time.Now().Unix(), Device: targetDevice, Driver: driver, WindowSize: windowSize, + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, } r.wdaClients[targetDevice.SerialNumber()] = client return client, nil } -func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { +func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, StepType: stepTypeIOS, @@ -426,7 +431,7 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // init wdaClient driver - wdaClient, err := r.hrpRunner.InitWDAClient(step.IOS.WDADevice) + wdaClient, err := s.hrpRunner.InitWDAClient(step.IOS.WDADevice) if err != nil { return } @@ -453,7 +458,7 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro // take snapshot log.Info().Str("name", step.Name).Msg("take snapshot before validation") - err = wdaClient.screenShot() + err = wdaClient.screenShot(fmt.Sprintf("validate_%s", step.Name)) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } @@ -473,13 +478,15 @@ func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err erro var errActionNotImplemented = errors.New("UI action not implemented") type wdaClient struct { + ID int64 Device *gwda.Device Driver gwda.WebDriver WindowSize gwda.Size + httpClient *http.Client } // screenShot takes screenshot and saves image file to $CWD/screenshots/ folder -func (w *wdaClient) screenShot() error { +func (w *wdaClient) screenShot(path ...string) error { // gidevice 和 gwda 均可实现截图功能,但 gidevice 的截图性能更优 // gwda 通过 wda 请求获取(分辨率、响应时间均由 wda 决定) // gidevice 直接通过 Apple 允许的底层通信获取 @@ -497,10 +504,19 @@ func (w *wdaClient) screenShot() error { dir, _ := os.Getwd() screenshotsDir := filepath.Join(dir, "screenshots") if err := builtin.EnsureFolderExists(screenshotsDir); err != nil { - return errors.Wrap(err, "create screenshots failed") + return errors.Wrap(err, "create screenshots directory failed") } - path := filepath.Join(screenshotsDir, fmt.Sprintf("%d", time.Now().Unix())+"."+format) - file, err := os.Create(path) + + var filaName string + if len(path) > 0 && path[0] != "" { + filaName = fmt.Sprintf("%d_%s", time.Now().Unix(), path[0]) + } else { + filaName = fmt.Sprintf("%d", time.Now().Unix()) + } + screenshotPath := filepath.Join(screenshotsDir, + fmt.Sprintf("%d_%s.%s", w.ID, filaName, format)) + + file, err := os.Create(screenshotPath) if err != nil { return errors.Wrap(err, "create screenshot image file failed") } @@ -520,7 +536,7 @@ func (w *wdaClient) screenShot() error { return errors.Wrap(err, "encode screenshot image failed") } - log.Info().Str("path", path).Msg("screenshot generated") + log.Info().Str("path", screenshotPath).Msg("screenshot generated") return nil } @@ -642,6 +658,9 @@ func (w *wdaClient) doAction(action MobileAction) error { case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") + if param, ok := action.Params.(string); ok { + return w.screenShot(fmt.Sprintf("screenshot_%s", param)) + } return w.screenShot() case ctlStartCamera: // start camera, alias for app_launch com.apple.camera @@ -736,6 +755,20 @@ func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { return w.Driver.FindElement(selector) } +func (w *wdaClient) locateByOCR(text string) (point gwda.Point, err error) { + // raw, err := w.Device.GIDevice().Screenshot() + // if err != nil { + // return errors.Wrap(err, "screenshot by WDA failed") + // } + + // url := "https://hubble.bytedance.net/video/api/v1/algorithm/ocr" + + // req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode())) + + // w.httpClient.Do(req) + return +} + func (w *wdaClient) assertName(name string, exists bool) bool { selector := gwda.BySelector{ LinkText: gwda.NewElementAttribute().WithName(name), From 597d56337b51323f593559e7809854e45863ea9d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 27 Aug 2022 00:02:13 +0800 Subject: [PATCH 030/169] feat: save screenshot file --- hrp/step_ios_ui.go | 130 +++++++++++---------------------------------- 1 file changed, 32 insertions(+), 98 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index c3fb4538..81e96d46 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -2,20 +2,14 @@ package hrp import ( "fmt" - "image" - "image/jpeg" - "image/png" "net/http" - "os" - "path/filepath" "strings" "time" "github.com/electricbubble/gwda" + gwdaExt "github.com/electricbubble/gwda-ext-opencv" "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) const ( @@ -335,7 +329,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro return } // check if WDA is healthy - ok, e := client.Driver.IsWdaHealthy() + ok, e := client.DriverExt.IsWdaHealthy() if err != nil { err = errors.Wrap(e, "check WDA health failed") return @@ -391,7 +385,11 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } - settings, err := driver.SetAppiumSettings(map[string]interface{}{ + driverExt, err := gwdaExt.Extend(driver, 0.95) + if err != nil { + return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") + } + settings, err := driverExt.SetAppiumSettings(map[string]interface{}{ "snapshotMaxDepth": snapshotMaxDepth, "acceptAlertButtonSelector": acceptAlertButtonSelector, }) @@ -401,7 +399,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") // get device window size - windowSize, err := driver.WindowSize() + windowSize, err := driverExt.WindowSize() if err != nil { return nil, errors.Wrap(err, "failed to get windows size") } @@ -411,7 +409,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro client = &wdaClient{ ID: time.Now().Unix(), Device: targetDevice, - Driver: driver, + DriverExt: driverExt, WindowSize: windowSize, httpClient: &http.Client{ Timeout: 10 * time.Second, @@ -457,11 +455,11 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // take snapshot - log.Info().Str("name", step.Name).Msg("take snapshot before validation") - err = wdaClient.screenShot(fmt.Sprintf("validate_%s", step.Name)) + screenshotPath, err := wdaClient.DriverExt.ScreenShot(fmt.Sprintf("validate_%s", step.Name)) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } + log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") // validate validateResults, err := wdaClient.doValidation(step.Validators) @@ -480,66 +478,11 @@ var errActionNotImplemented = errors.New("UI action not implemented") type wdaClient struct { ID int64 Device *gwda.Device - Driver gwda.WebDriver + DriverExt *gwdaExt.DriverExt WindowSize gwda.Size httpClient *http.Client } -// screenShot takes screenshot and saves image file to $CWD/screenshots/ folder -func (w *wdaClient) screenShot(path ...string) error { - // gidevice 和 gwda 均可实现截图功能,但 gidevice 的截图性能更优 - // gwda 通过 wda 请求获取(分辨率、响应时间均由 wda 决定) - // gidevice 直接通过 Apple 允许的底层通信获取 - // raw, err := w.Driver.Screenshot() - raw, err := w.Device.GIDevice().Screenshot() - if err != nil { - return errors.Wrap(err, "screenshot by WDA failed") - } - - img, format, err := image.Decode(raw) - if err != nil { - return errors.Wrap(err, "decode screenshot image failed") - } - - dir, _ := os.Getwd() - screenshotsDir := filepath.Join(dir, "screenshots") - if err := builtin.EnsureFolderExists(screenshotsDir); err != nil { - return errors.Wrap(err, "create screenshots directory failed") - } - - var filaName string - if len(path) > 0 && path[0] != "" { - filaName = fmt.Sprintf("%d_%s", time.Now().Unix(), path[0]) - } else { - filaName = fmt.Sprintf("%d", time.Now().Unix()) - } - screenshotPath := filepath.Join(screenshotsDir, - fmt.Sprintf("%d_%s.%s", w.ID, filaName, format)) - - file, err := os.Create(screenshotPath) - if err != nil { - return errors.Wrap(err, "create screenshot image file failed") - } - defer func() { - _ = file.Close() - }() - - switch format { - case "png": - err = png.Encode(file, img) - case "jpeg": - err = jpeg.Encode(file, img, nil) - default: - return fmt.Errorf("unsupported image format: %s", format) - } - if err != nil { - return errors.Wrap(err, "encode screenshot image failed") - } - - log.Info().Str("path", screenshotPath).Msg("screenshot generated") - return nil -} - func (w *wdaClient) doAction(action MobileAction) error { log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") @@ -549,17 +492,17 @@ func (w *wdaClient) doAction(action MobileAction) error { return errActionNotImplemented case appLaunch: if bundleId, ok := action.Params.(string); ok { - return w.Driver.AppLaunch(bundleId) + return w.DriverExt.AppLaunch(bundleId) } return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params) case appLaunchUnattached: if bundleId, ok := action.Params.(string); ok { - return w.Driver.AppLaunchUnattached(bundleId) + return w.DriverExt.AppLaunchUnattached(bundleId) } return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params) case appTerminate: if bundleId, ok := action.Params.(string); ok { - success, err := w.Driver.AppTerminate(bundleId) + success, err := w.DriverExt.AppTerminate(bundleId) if err != nil { return errors.Wrap(err, "failed to terminate app") } @@ -570,7 +513,7 @@ func (w *wdaClient) doAction(action MobileAction) error { } return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) case uiHome: - return w.Driver.Homescreen() + return w.DriverExt.Homescreen() case uiClick: // click on coordinate if location, ok := action.Params.([]int); ok { @@ -578,7 +521,7 @@ func (w *wdaClient) doAction(action MobileAction) error { if len(location) != 2 { return fmt.Errorf("invalid click location params: %v", location) } - return w.Driver.Tap(location[0], location[1]) + return w.DriverExt.WebDriver.Tap(location[0], location[1]) } if location, ok := action.Params.([]float64); ok { // relative x,y of window size @@ -587,7 +530,7 @@ func (w *wdaClient) doAction(action MobileAction) error { } x := location[0] * float64(w.WindowSize.Width) y := location[1] * float64(w.WindowSize.Height) - return w.Driver.TapFloat(x, y) + return w.DriverExt.TapFloat(x, y) } // click on name or xpath if param, ok := action.Params.(string); ok { @@ -642,13 +585,13 @@ func (w *wdaClient) doAction(action MobileAction) error { } else { return fmt.Errorf("invalid swipe params: %v", action.Params) } - return w.Driver.Swipe(fromX, fromY, toX, toY) + return w.DriverExt.WebDriver.Swipe(fromX, fromY, toX, toY) case uiInput: // input text on current active element // append \n to send text with enter // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) - return w.Driver.SendKeys(param) + return w.DriverExt.SendKeys(param) case ctlSleep: if param, ok := action.Params.(int); ok { time.Sleep(time.Duration(param) * time.Second) @@ -658,16 +601,21 @@ func (w *wdaClient) doAction(action MobileAction) error { case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") + var screenshotPath string + var err error if param, ok := action.Params.(string); ok { - return w.screenShot(fmt.Sprintf("screenshot_%s", param)) + screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%s", param)) + } else { + screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) } - return w.screenShot() + log.Info().Str("path", screenshotPath).Msg("take screenshot") + return err case ctlStartCamera: // start camera, alias for app_launch com.apple.camera - return w.Driver.AppLaunch("com.apple.camera") + return w.DriverExt.AppLaunch("com.apple.camera") case ctlStopCamera: // stop camera, alias for app_terminate com.apple.camera - success, err := w.Driver.AppTerminate("com.apple.camera") + success, err := w.DriverExt.AppTerminate("com.apple.camera") if err != nil { return errors.Wrap(err, "failed to terminate camera") } @@ -752,28 +700,14 @@ func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { } } - return w.Driver.FindElement(selector) -} - -func (w *wdaClient) locateByOCR(text string) (point gwda.Point, err error) { - // raw, err := w.Device.GIDevice().Screenshot() - // if err != nil { - // return errors.Wrap(err, "screenshot by WDA failed") - // } - - // url := "https://hubble.bytedance.net/video/api/v1/algorithm/ocr" - - // req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode())) - - // w.httpClient.Do(req) - return + return w.DriverExt.FindElement(selector) } func (w *wdaClient) assertName(name string, exists bool) bool { selector := gwda.BySelector{ LinkText: gwda.NewElementAttribute().WithName(name), } - _, err := w.Driver.FindElement(selector) + _, err := w.DriverExt.FindElement(selector) return exists == (err == nil) } @@ -781,6 +715,6 @@ func (w *wdaClient) assertXpath(xpath string, exists bool) bool { selector := gwda.BySelector{ XPath: xpath, } - _, err := w.Driver.FindElement(selector) + _, err := w.DriverExt.FindElement(selector) return exists == (err == nil) } From 3222b211567665a75cc7f4a63048d8ab4aeabfb2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 27 Aug 2022 00:25:48 +0800 Subject: [PATCH 031/169] change: update gwda-ext --- go.mod | 3 ++- go.sum | 14 ++++++++++++-- hrp/step_ios_ui.go | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1f645d7a..46cbfb1a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/andybalholm/brotli v1.0.4 + github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009 github.com/denisbrodbeck/machineid v1.0.1 github.com/electricbubble/gwda v0.4.0 github.com/fatih/color v1.13.0 @@ -24,7 +25,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.11.0 - github.com/rs/zerolog v1.26.1 + github.com/rs/zerolog v1.27.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index e425df94..31c6c4f1 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= @@ -95,6 +96,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 h1:DjPOXlkeCsxtFzieys2RjYEn6OCoAPQNiLmG2eeSVgw= github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= +github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009 h1:JYD/5UFNWfaDOY4GIGboszuW7yKHXgiepo/balYm684= +github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009/go.mod h1:R10UCNr8u2xpS377k0YeutGShr0Nq5S6eEALQ6WGyu8= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= @@ -103,6 +106,8 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= +github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= +github.com/electricbubble/opencv-helper v0.0.3/go.mod h1:VHB21p5xsIjXUsUleWSaKGJosRsRAO7cuJoZKf7uCcc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -337,8 +342,9 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= @@ -389,6 +395,7 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ 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.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -423,8 +430,9 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -511,6 +519,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +gocv.io/x/gocv v0.27.0 h1:3X8I74ULsWHd4m7DQRv2Nqx5VkKscfUFnKgLNodiboI= +gocv.io/x/gocv v0.27.0/go.mod h1:n4LnYjykU6y9gn48yZf4eLCdtuSb77XxSkW6g0wGf/A= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 81e96d46..e7c30dd5 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -6,8 +6,8 @@ import ( "strings" "time" + gwdaExt "github.com/debugtalk/gwda-ext" "github.com/electricbubble/gwda" - gwdaExt "github.com/electricbubble/gwda-ext-opencv" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) From d3c1c080301b2b0cf5ed53a6f8a06bfb414af686 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 00:41:45 +0800 Subject: [PATCH 032/169] change: tap & swipe --- hrp/step.go | 7 ++- hrp/step_android_ui.go | 16 ++---- hrp/step_ios_ui.go | 123 ++++++++++++++-------------------------- hrp/step_ios_ui_test.go | 18 +++--- 4 files changed, 61 insertions(+), 103 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 63390399..507182a4 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -33,9 +33,10 @@ const ( // UI handling uiHome MobileMethod = "home" - uiClick MobileMethod = "click" - uiDoubleClick MobileMethod = "double_click" - uiLongClick MobileMethod = "long_click" + uiTapXY MobileMethod = "tap_xy" + uiTap MobileMethod = "tap" + uiDoubleTapXY MobileMethod = "double_tap_xy" + uiDoubleTap MobileMethod = "double_tap" uiSwipe MobileMethod = "swipe" uiInput MobileMethod = "input" diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 14eb76d8..8dac1687 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -66,25 +66,17 @@ func (s *StepAndroid) StopRecording() *StepAndroid { return &StepAndroid{step: s.step} } -func (s *StepAndroid) Click(params interface{}) *StepAndroid { +func (s *StepAndroid) Tap(params interface{}) *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiClick, + Method: uiTap, Params: params, }) return &StepAndroid{step: s.step} } -func (s *StepAndroid) DoubleClick(params interface{}) *StepAndroid { +func (s *StepAndroid) DoubleTap(params interface{}) *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiDoubleClick, - Params: params, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) LongClick(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiLongClick, + Method: uiDoubleTap, Params: params, }) return &StepAndroid{step: s.step} diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index e7c30dd5..6b9358b8 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -2,12 +2,11 @@ package hrp import ( "fmt" - "net/http" "strings" "time" - gwdaExt "github.com/debugtalk/gwda-ext" "github.com/electricbubble/gwda" + "github.com/httprunner/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -93,25 +92,36 @@ func (s *StepIOS) Home() *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) Click(params interface{}) *StepIOS { +// TapXY taps the point {X,Y}, X & Y is percentage of coordinates +func (s *StepIOS) TapXY(x, y float64) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiClick, + Method: uiTapXY, + Params: []float64{x, y}, + }) + return &StepIOS{step: s.step} +} + +// Tap taps on the target element +func (s *StepIOS) Tap(params string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiTap, Params: params, }) return &StepIOS{step: s.step} } -func (s *StepIOS) DoubleClick(params interface{}) *StepIOS { +// DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates +func (s *StepIOS) DoubleTapXY(x, y float64) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiDoubleClick, - Params: params, + Method: uiDoubleTapXY, + Params: []float64{x, y}, }) return &StepIOS{step: s.step} } -func (s *StepIOS) LongClick(params interface{}) *StepIOS { +func (s *StepIOS) DoubleTap(params string) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiLongClick, + Method: uiDoubleTap, Params: params, }) return &StepIOS{step: s.step} @@ -385,7 +395,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } - driverExt, err := gwdaExt.Extend(driver, 0.95) + driverExt, err := uixt.Extend(driver, 0.95) if err != nil { return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") } @@ -407,13 +417,9 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro // cache wda client r.wdaClients = make(map[string]*wdaClient) client = &wdaClient{ - ID: time.Now().Unix(), Device: targetDevice, DriverExt: driverExt, WindowSize: windowSize, - httpClient: &http.Client{ - Timeout: 10 * time.Second, - }, } r.wdaClients[targetDevice.SerialNumber()] = client @@ -476,11 +482,9 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro var errActionNotImplemented = errors.New("UI action not implemented") type wdaClient struct { - ID int64 Device *gwda.Device - DriverExt *gwdaExt.DriverExt + DriverExt *uixt.DriverExt WindowSize gwda.Size - httpClient *http.Client } func (w *wdaClient) doAction(action MobileAction) error { @@ -514,78 +518,39 @@ func (w *wdaClient) doAction(action MobileAction) error { return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) case uiHome: return w.DriverExt.Homescreen() - case uiClick: - // click on coordinate - if location, ok := action.Params.([]int); ok { - // absolute x,y - if len(location) != 2 { - return fmt.Errorf("invalid click location params: %v", location) - } - return w.DriverExt.WebDriver.Tap(location[0], location[1]) - } + case uiTapXY: if location, ok := action.Params.([]float64); ok { - // relative x,y of window size + // relative x,y of window size: [0.5, 0.5] if len(location) != 2 { - return fmt.Errorf("invalid click location params: %v", location) + return fmt.Errorf("invalid tap location params: %v", location) } - x := location[0] * float64(w.WindowSize.Width) - y := location[1] * float64(w.WindowSize.Height) - return w.DriverExt.TapFloat(x, y) + return w.DriverExt.TapXY(location[0], location[1]) } - // click on name or xpath + return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) + case uiTap: if param, ok := action.Params.(string); ok { - ele, err := w.findElement(param) - if err != nil { - return errors.Wrap(err, "failed to find element") - } - return ele.Click() + return w.DriverExt.Tap(param) } - return fmt.Errorf("invalid click params: %v", action.Params) - case uiDoubleClick: - // double click on name or xpath + return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) + case uiDoubleTapXY: + if location, ok := action.Params.([]float64); ok { + // relative x,y of window size: [0.5, 0.5] + if len(location) != 2 { + return fmt.Errorf("invalid tap location params: %v", location) + } + return w.DriverExt.DoubleTapXY(location[0], location[1]) + } + return fmt.Errorf("invalid %s params: %v", uiDoubleTapXY, action.Params) + case uiDoubleTap: if param, ok := action.Params.(string); ok { - ele, err := w.findElement(param) - if err != nil { - return errors.Wrap(err, "failed to find element") - } - return ele.DoubleTap() + return w.DriverExt.DoubleTap(param) } - return fmt.Errorf("invalid click params: %v", action.Params) - case uiLongClick: - // long click 2s on name or xpath - if param, ok := action.Params.(string); ok { - ele, err := w.findElement(param) - if err != nil { - return errors.Wrap(err, "failed to find element") - } - return ele.TouchAndHold(2) - } - return fmt.Errorf("invalid click params: %v", action.Params) + return fmt.Errorf("invalid %s params: %v", uiDoubleTap, action.Params) case uiSwipe: - width := w.WindowSize.Width - height := w.WindowSize.Height - - var fromX, fromY, toX, toY int - if direction, ok := action.Params.(string); ok { - switch direction { - case "up": - fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4 - case "down": - fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 - case "left": - fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2 - case "right": - fromX, fromY, toX, toY = width*1/4, height/2, width*3/4, height/2 - } - } else if params, ok := action.Params.([]int); ok { - if len(params) != 4 { - return fmt.Errorf("invalid swipe params: %v", params) - } - fromX, fromY, toX, toY = params[0], params[1], params[2], params[3] - } else { - return fmt.Errorf("invalid swipe params: %v", action.Params) + if param, ok := action.Params.(string); ok { + return w.DriverExt.SwipeTo(param) } - return w.DriverExt.WebDriver.Swipe(fromX, fromY, toX, toY) + return fmt.Errorf("invalid %s params: %v", uiSwipe, action.Params) case uiInput: // input text on current active element // append \n to send text with enter diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 9ce227bb..246ede90 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -10,7 +10,7 @@ func TestIOSSettingsAction(t *testing.T) { Config: NewConfig("ios ui action on Settings"), TestSteps: []IStep{ NewStep("launch Settings"). - IOS().Home().Click("//*[@label='设置']"). + IOS().Home().Tap("//*[@label='设置']"). Validate(). AssertNameExists("飞行模式"). AssertNameNotExists("飞行模式2"), @@ -31,7 +31,7 @@ func TestIOSSearchApp(t *testing.T) { Config: NewConfig("ios ui action on Search App 资源库"), TestSteps: []IStep{ NewStep("进入 App 资源库 搜索框"). - IOS().Home().SwipeLeft().Times(2).Click("dewey-search-field"). + IOS().Home().SwipeLeft().Times(2).Tap("dewey-search-field"). Validate(). AssertNameExists("取消"), NewStep("搜索抖音"). @@ -78,13 +78,13 @@ func TestIOSWeixinLive(t *testing.T) { IOS(). Home(). AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 - Click("微信"). + Tap("微信"). Validate(). AssertNameExists("通讯录", "微信启动失败,「通讯录」不存在"), NewStep("进入直播页"). IOS(). - Click("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 - Click([]float64{0.5, 0.3}). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 + Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 + TapXY(0.5, 0.3). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 Validate(). AssertNameExists("直播"), NewStep("向上滑动 5 次"). @@ -112,7 +112,7 @@ func TestIOSCameraPhotoCapture(t *testing.T) { Validate(). AssertNameExists("PhotoCapture", "拍照按钮不存在"), NewStep("start recording"). - IOS().Click("PhotoCapture"), + IOS().Tap("PhotoCapture"), }, } fmt.Println(testCase) @@ -140,9 +140,9 @@ func TestIOSCameraVideoCapture(t *testing.T) { AssertNameExists("VideoCapture", "拍摄按钮不存在"), NewStep("start recording"). IOS(). - Click("VideoCapture"). // 开始录像 + Tap("VideoCapture"). // 开始录像 Sleep(5). - Click("VideoCapture"), // 停止录像 + Tap("VideoCapture"), // 停止录像 }, } fmt.Println(testCase) @@ -158,7 +158,7 @@ func TestIOSDouyinAction(t *testing.T) { Config: NewConfig("ios ui action on 抖音"), TestSteps: []IStep{ NewStep("launch douyin"). - IOS().Home().Click("//*[@label='抖音']"). + IOS().Home().Tap("//*[@label='抖音']"). Validate(). AssertNameExists("首页", "首页 tab 不存在"). AssertNameExists("消息", "消息 tab 不存在"), From 598ae509deca1ca8dac06d211783fb7415e1e5ef Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 09:45:49 +0800 Subject: [PATCH 033/169] feat: move uixt to hrp internal --- go.mod | 3 +- go.sum | 6 +- hrp/internal/uixt/README.md | 41 ++++ hrp/internal/uixt/drag.go | 29 +++ hrp/internal/uixt/drag_test.go | 23 ++ hrp/internal/uixt/ext.go | 335 ++++++++++++++++++++++++++++++ hrp/internal/uixt/ext_ocr.go | 137 ++++++++++++ hrp/internal/uixt/ext_ocr_test.go | 21 ++ hrp/internal/uixt/gesture.go | 44 ++++ hrp/internal/uixt/gesture_test.go | 28 +++ hrp/internal/uixt/swipe.go | 135 ++++++++++++ hrp/internal/uixt/swipe_test.go | 33 +++ hrp/internal/uixt/tap.go | 88 ++++++++ hrp/internal/uixt/tap_test.go | 48 +++++ hrp/internal/uixt/touch.go | 33 +++ hrp/internal/uixt/touch_test.go | 55 +++++ hrp/step_ios_ui.go | 20 +- 17 files changed, 1057 insertions(+), 22 deletions(-) create mode 100644 hrp/internal/uixt/README.md create mode 100644 hrp/internal/uixt/drag.go create mode 100644 hrp/internal/uixt/drag_test.go create mode 100644 hrp/internal/uixt/ext.go create mode 100644 hrp/internal/uixt/ext_ocr.go create mode 100644 hrp/internal/uixt/ext_ocr_test.go create mode 100644 hrp/internal/uixt/gesture.go create mode 100644 hrp/internal/uixt/gesture_test.go create mode 100644 hrp/internal/uixt/swipe.go create mode 100644 hrp/internal/uixt/swipe_test.go create mode 100644 hrp/internal/uixt/tap.go create mode 100644 hrp/internal/uixt/tap_test.go create mode 100644 hrp/internal/uixt/touch.go create mode 100644 hrp/internal/uixt/touch_test.go diff --git a/go.mod b/go.mod index 46cbfb1a..592963da 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.16 require ( github.com/andybalholm/brotli v1.0.4 - github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009 github.com/denisbrodbeck/machineid v1.0.1 github.com/electricbubble/gwda v0.4.0 + github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 github.com/go-errors/errors v1.0.1 @@ -31,6 +31,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + gocv.io/x/gocv v0.31.0 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 google.golang.org/grpc v1.45.0 diff --git a/go.sum b/go.sum index 31c6c4f1..0d59b3cb 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 h1:DjPOXlkeCsxtFzieys2RjYEn6OCoAPQNiLmG2eeSVgw= github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= -github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009 h1:JYD/5UFNWfaDOY4GIGboszuW7yKHXgiepo/balYm684= -github.com/debugtalk/gwda-ext v0.0.0-20220826161333-0588d8320009/go.mod h1:R10UCNr8u2xpS377k0YeutGShr0Nq5S6eEALQ6WGyu8= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= @@ -272,6 +270,7 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/httprunner/funplugin v0.5.0 h1:Laoe8URu71qeyST9wvRtGSkDWc8Y3T1IrnvFSTHmO84= github.com/httprunner/funplugin v0.5.0/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= +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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= @@ -519,8 +518,9 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -gocv.io/x/gocv v0.27.0 h1:3X8I74ULsWHd4m7DQRv2Nqx5VkKscfUFnKgLNodiboI= gocv.io/x/gocv v0.27.0/go.mod h1:n4LnYjykU6y9gn48yZf4eLCdtuSb77XxSkW6g0wGf/A= +gocv.io/x/gocv v0.31.0 h1:BHDtK8v+YPvoSPQTTiZB2fM/7BLg6511JqkruY2z6LQ= +gocv.io/x/gocv v0.31.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/hrp/internal/uixt/README.md b/hrp/internal/uixt/README.md new file mode 100644 index 00000000..e9191b29 --- /dev/null +++ b/hrp/internal/uixt/README.md @@ -0,0 +1,41 @@ +# uixt + +From v4.3.0,HttpRunner will support mobile UI automation testing: + +- iOS: based on [appium/WebDriverAgent], with client library [electricbubble/gwda] in golang +- Android: based on UiAutomation + +Some UI recognition algorithms are also introduced for both iOS and Android: + +- OpenCV: based on [OpenCV 4], with golang bindings [hybridgroup/gocv] and helper utils [electricbubble/gwda-ext-opencv] +- OCR: based on OCR API service from [volcengine], other API service may be extended + +## Dependencies + +### OpenCV + +[OpenCV 4] should be pre-installed. + +You can install OpenCV 4.6.0 using Homebrew on macOS. + +```bash +$ brew install opencv +``` + +You can get more installation introduction on [hybridgroup/gocv]. + +### OCR + +OCR API is a paid service, you need to pre-purchase and configure the account key. + +## Thanks + +This uixt module is initially forked from [electricbubble/gwda-ext-opencv] and made a lot of changes. + + +[electricbubble/gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv +[appium/WebDriverAgent]: https://github.com/appium/WebDriverAgent +[electricbubble/gwda]: https://github.com/electricbubble/gwda +[OpenCV 4]: https://opencv.org/ +[hybridgroup/gocv]: https://github.com/hybridgroup/gocv +[volcengine]: https://www.volcengine.com/product/text-recognition diff --git a/hrp/internal/uixt/drag.go b/hrp/internal/uixt/drag.go new file mode 100644 index 00000000..b4c5ea56 --- /dev/null +++ b/hrp/internal/uixt/drag.go @@ -0,0 +1,29 @@ +package uixt + +func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) { + return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...) +} + +func (dExt *DriverExt) DragFloat(pathname string, toX, toY float64, pressForDuration ...float64) (err error) { + return dExt.DragOffsetFloat(pathname, toX, toY, 0.5, 0.5, pressForDuration...) +} + +func (dExt *DriverExt) DragOffset(pathname string, toX, toY int, xOffset, yOffset float64, pressForDuration ...float64) (err error) { + return dExt.DragOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset, pressForDuration...) +} + +func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64, pressForDuration ...float64) (err error) { + if len(pressForDuration) == 0 { + pressForDuration = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + return dExt.WebDriver.DragFloat(fromX, fromY, toX, toY, pressForDuration[0]) +} diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go new file mode 100644 index 00000000..018d4fd7 --- /dev/null +++ b/hrp/internal/uixt/drag_test.go @@ -0,0 +1,23 @@ +package uixt + +import ( + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExt_Drag(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" + + // err = driverExt.Drag(pathSearch, 300, 500, 2) + // checkErr(t, err) + + err = driverExt.DragOffset(pathSearch, 300, 500, 2.1, 0.5, 2) + checkErr(t, err) +} diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go new file mode 100644 index 00000000..4b24ccb8 --- /dev/null +++ b/hrp/internal/uixt/ext.go @@ -0,0 +1,335 @@ +package uixt + +import ( + "bytes" + "fmt" + "image" + "image/jpeg" + "image/png" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/electricbubble/gwda" + cvHelper "github.com/electricbubble/opencv-helper" + "github.com/pkg/errors" +) + +// TemplateMatchMode is the type of the template matching operation. +type TemplateMatchMode int + +const ( + // TmSqdiff maps to TM_SQDIFF + TmSqdiff TemplateMatchMode = iota + // TmSqdiffNormed maps to TM_SQDIFF_NORMED + TmSqdiffNormed + // TmCcorr maps to TM_CCORR + TmCcorr + // TmCcorrNormed maps to TM_CCORR_NORMED + TmCcorrNormed + // TmCcoeff maps to TM_CCOEFF + TmCcoeff + // TmCcoeffNormed maps to TM_CCOEFF_NORMED + TmCcoeffNormed +) + +type DebugMode int + +const ( + // DmOff no output + DmOff DebugMode = iota + // DmEachMatch output matched and mismatched values + DmEachMatch + // DmNotMatch output only values that do not match + DmNotMatch +) + +type DriverExt struct { + gwda.WebDriver + windowSize gwda.Size + scale float64 + MatchMode TemplateMatchMode + Threshold float64 + frame *bytes.Buffer + doneMjpegStream chan bool +} + +// Extend 获得扩展后的 Driver, +// 并指定匹配阀值, +// 获取当前设备的 Scale, +// 默认匹配模式为 TmCcoeffNormed, +// 默认关闭 OpenCV 匹配值计算后的输出 +func Extend(driver gwda.WebDriver, threshold float64, matchMode ...TemplateMatchMode) (dExt *DriverExt, err error) { + dExt = &DriverExt{WebDriver: driver} + dExt.doneMjpegStream = make(chan bool, 1) + + if dExt.scale, err = dExt.Scale(); err != nil { + return &DriverExt{}, err + } + + // get device window size + dExt.windowSize, err = dExt.WebDriver.WindowSize() + if err != nil { + return nil, errors.Wrap(err, "failed to get windows size") + } + + if len(matchMode) == 0 { + matchMode = []TemplateMatchMode{TmCcoeffNormed} + } + dExt.MatchMode = matchMode[0] + cvHelper.Debug(cvHelper.DebugMode(DmOff)) + dExt.Threshold = threshold + return dExt, nil +} + +func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) { + newExt = new(DriverExt) + newExt.WebDriver = dExt.WebDriver + newExt.scale = dExt.scale + newExt.MatchMode = dExt.MatchMode + newExt.Threshold = threshold + return +} + +func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) { + newExt = new(DriverExt) + newExt.WebDriver = dExt.WebDriver + newExt.scale = dExt.scale + newExt.MatchMode = matchMode + newExt.Threshold = dExt.Threshold + return +} + +func (dExt *DriverExt) Debug(dm DebugMode) { + cvHelper.Debug(cvHelper.DebugMode(dm)) +} + +func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { + if httpClient == nil { + return errors.New(`'httpClient' can't be nil`) + } + + var req *http.Request + if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil { + return err + } + + var resp *http.Response + if resp, err = httpClient.Do(req); err != nil { + return err + } + // defer func() { _ = resp.Body.Close() }() + + var boundary string + if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { + return err + } else { + boundary = strings.Trim(param["boundary"], "-") + } + + mjpegReader := multipart.NewReader(resp.Body, boundary) + + go func() { + for { + select { + case <-dExt.doneMjpegStream: + _ = resp.Body.Close() + return + default: + var part *multipart.Part + if part, err = mjpegReader.NextPart(); err != nil { + dExt.frame = nil + continue + } + + raw := new(bytes.Buffer) + if _, err = raw.ReadFrom(part); err != nil { + dExt.frame = nil + continue + } + dExt.frame = raw + } + } + }() + + return +} + +func (dExt *DriverExt) CloseMjpegStream() { + dExt.doneMjpegStream <- true +} + +func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { + // 优先使用 MJPEG 流进行截图,性能最优 + // 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口 + if dExt.frame != nil { + return dExt.frame, nil + } + if raw, err = dExt.WebDriver.Screenshot(); err != nil { + return nil, err + } + return +} + +// saveScreenShot saves image file to $CWD/screenshots/ folder +func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { + img, format, err := image.Decode(raw) + if err != nil { + return "", errors.Wrap(err, "decode screenshot image failed") + } + + dir, _ := os.Getwd() + screenshotsDir := filepath.Join(dir, "screenshots") + if err = os.MkdirAll(screenshotsDir, os.ModePerm); err != nil { + return "", errors.Wrap(err, "create screenshots directory failed") + } + screenshotPath := filepath.Join(screenshotsDir, + fmt.Sprintf("%s.%s", fileName, format)) + + file, err := os.Create(screenshotPath) + if err != nil { + return "", errors.Wrap(err, "create screenshot image file failed") + } + defer func() { + _ = file.Close() + }() + + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + default: + return "", fmt.Errorf("unsupported image format: %s", format) + } + if err != nil { + return "", errors.Wrap(err, "encode screenshot image failed") + } + + return screenshotPath, nil +} + +// ScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder +func (dExt *DriverExt) ScreenShot(fileName string) (string, error) { + raw, err := dExt.takeScreenShot() + if err != nil { + return "", errors.Wrap(err, "screenshot by WDA failed") + } + + return dExt.saveScreenShot(raw, fileName) +} + +// func (sExt *DriverExt) findImgRect(search string) (rect image.Rectangle, err error) { +// pathSource := filepath.Join(sExt.pathname, cvHelper.GenFilename()) +// if err = sExt.driver.ScreenshotToDisk(pathSource); err != nil { +// return image.Rectangle{}, err +// } +// +// if rect, err = cvHelper.FindImageRectFromDisk(pathSource, search, float32(sExt.Threshold), cvHelper.TemplateMatchMode(sExt.MatchMode)); err != nil { +// return image.Rectangle{}, err +// } +// return +// } + +func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { + var bufSource, bufSearch *bytes.Buffer + if bufSearch, err = getBufFromDisk(search); err != nil { + return nil, err + } + if bufSource, err = dExt.takeScreenShot(); err != nil { + return nil, err + } + + if rects, err = cvHelper.FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.Threshold), cvHelper.TemplateMatchMode(dExt.MatchMode)); err != nil { + return nil, err + } + return +} + +func getBufFromDisk(name string) (*bytes.Buffer, error) { + var f *os.File + var err error + if f, err = os.Open(name); err != nil { + return nil, err + } + var all []byte + if all, err = ioutil.ReadAll(f); err != nil { + return nil, err + } + return bytes.NewBuffer(all), nil +} + +// isPathExists returns true if path exists, whether path is file or dir +func isPathExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} + +func (dExt *DriverExt) FindUIElement(param string) (ele gwda.WebElement, err error) { + var selector gwda.BySelector + if strings.HasPrefix(param, "/") { + // xpath + selector = gwda.BySelector{ + XPath: param, + } + } else { + // name + selector = gwda.BySelector{ + LinkText: gwda.NewElementAttribute().WithName(param), + } + } + + return dExt.WebDriver.FindElement(selector) +} + +func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height float64, err error) { + // click on text, using OCR + if !isPathExists(search) { + return dExt.FindTextByOCR(search) + } + // click on image, using opencv + return dExt.FindImageRectInUIKit(search) +} + +func (dExt *DriverExt) FindImageRectInUIKit(search string) (x, y, width, height float64, err error) { + var bufSource, bufSearch *bytes.Buffer + if bufSearch, err = getBufFromDisk(search); err != nil { + return 0, 0, 0, 0, err + } + if bufSource, err = dExt.takeScreenShot(); err != nil { + return 0, 0, 0, 0, err + } + + var rect image.Rectangle + if rect, err = cvHelper.FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.Threshold), cvHelper.TemplateMatchMode(dExt.MatchMode)); err != nil { + return 0, 0, 0, 0, err + } + + // if rect, err = dExt.findImgRect(search); err != nil { + // return 0, 0, 0, 0, err + // } + x, y, width, height = dExt.MappingToRectInUIKit(rect) + return +} + +func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { + x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale + width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale + return +} + +func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error { + return dExt.PerformAppiumTouchActions(touchActions) +} + +func (dExt *DriverExt) PerformActions(actions *gwda.W3CActions) error { + return dExt.PerformW3CActions(actions) +} + +// IsExist diff --git a/hrp/internal/uixt/ext_ocr.go b/hrp/internal/uixt/ext_ocr.go new file mode 100644 index 00000000..78581b3e --- /dev/null +++ b/hrp/internal/uixt/ext_ocr.go @@ -0,0 +1,137 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "image" + "io/ioutil" + "mime/multipart" + "net/http" + "time" +) + +var client = &http.Client{ + Timeout: time.Second * 10, +} + +type Point struct { + X float32 `json:"x"` + Y float32 `json:"y"` +} + +type OCRResult struct { + Text string `json:"text"` + Points []Point `json:"points"` +} + +type ResponseOCR struct { + Code int `json:"code"` + Message string `json:"message"` + OCRResult []OCRResult `json:"ocrResult"` +} + +type veDEMOCRService struct{} + +func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + bodyWriter.WriteField("withDet", "true") + // bodyWriter.WriteField("timestampOnly", "true") + + formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png") + if err != nil { + return nil, fmt.Errorf("create form file error: %v", err) + } + _, err = formWriter.Write(imageBuf) + if err != nil { + return nil, fmt.Errorf("write form error: %v", err) + } + + err = bodyWriter.Close() + if err != nil { + return nil, fmt.Errorf("close body writer error: %v", err) + } + + url, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9odWJibGUuYnl0ZWRhbmNlLm5ldC92aWRlby9hcGkvdjEvYWxnb3JpdGhtL29jcg==") + req, err := http.NewRequest("POST", string(url), bodyBuf) + if err != nil { + return nil, fmt.Errorf("construct request error: %v", err) + } + + req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("http reqeust OCR server error: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response status code: %d", resp.StatusCode) + } + + results, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response body error: %v", err) + } + + var ocrResult ResponseOCR + err = json.Unmarshal(results, &ocrResult) + if err != nil { + return nil, fmt.Errorf("json unmarshal response body error: %v", err) + } + + return ocrResult.OCRResult, nil +} + +func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rectangle, err error) { + ocrResults, err := s.getOCRResult(imageBuf) + if err != nil { + return + } + + for _, ocrResult := range ocrResults { + if ocrResult.Text != text { + continue + } + + // only find the first matched one + rect = image.Rectangle{ + // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 + Min: image.Point{ + X: int(ocrResult.Points[0].X), + Y: int(ocrResult.Points[0].Y), + }, + Max: image.Point{ + X: int(ocrResult.Points[2].X), + Y: int(ocrResult.Points[2].Y), + }, + } + return + } + + return image.Rectangle{}, fmt.Errorf("text %s not found", text) +} + +type OCRService interface { + FindText(text string, imageBuf []byte) (rect image.Rectangle, err error) +} + +func (dExt *DriverExt) FindTextByOCR(search string) (x, y, width, height float64, err error) { + var bufSource *bytes.Buffer + if bufSource, err = dExt.takeScreenShot(); err != nil { + err = fmt.Errorf("screenshot error: %v", err) + return + } + + service := &veDEMOCRService{} + rect, err := service.FindText(search, bufSource.Bytes()) + if err != nil { + err = fmt.Errorf("find text failed: %v", err) + return + } + + x, y, width, height = dExt.MappingToRectInUIKit(rect) + return +} diff --git a/hrp/internal/uixt/ext_ocr_test.go b/hrp/internal/uixt/ext_ocr_test.go new file mode 100644 index 00000000..53e1dee3 --- /dev/null +++ b/hrp/internal/uixt/ext_ocr_test.go @@ -0,0 +1,21 @@ +package uixt + +import ( + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExtOCR(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + x, y, width, height, err := driverExt.FindTextByOCR("抖音") + checkErr(t, err) + + t.Logf("x: %v, y: %v, width: %v, height: %v", x, y, width, height) + driver.TapFloat(x, y-20) +} diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go new file mode 100644 index 00000000..75462167 --- /dev/null +++ b/hrp/internal/uixt/gesture.go @@ -0,0 +1,44 @@ +package uixt + +import ( + "image" + "sort" + + "github.com/electricbubble/gwda" +) + +func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) { + var rects []image.Rectangle + if rects, err = dExt.FindAllImageRect(pathname); err != nil { + return err + } + + sort.Slice(rects, func(i, j int) bool { + if rects[i].Min.Y < rects[j].Min.Y { + return true + } else if rects[i].Min.Y == rects[j].Min.Y { + if rects[i].Min.X < rects[j].Min.X { + return true + } + } + return false + }) + + touchActions := gwda.NewTouchActions(len(password)*2 + 1) + for i := range password { + x, y, width, height := dExt.MappingToRectInUIKit(rects[password[i]]) + x = x + width*0.5 + y = y + height*0.5 + + if i == 0 { + touchActions.Press(gwda.NewTouchActionPress().WithXYFloat(x, y)). + Wait(0.2) + } else { + touchActions.MoveTo(gwda.NewTouchActionMoveTo().WithXYFloat(x, y)). + Wait(0.2) + } + } + touchActions.Release() + + return dExt.PerformTouchActions(touchActions) +} diff --git a/hrp/internal/uixt/gesture_test.go b/hrp/internal/uixt/gesture_test.go new file mode 100644 index 00000000..8308d132 --- /dev/null +++ b/hrp/internal/uixt/gesture_test.go @@ -0,0 +1,28 @@ +package uixt + +import ( + "strconv" + "strings" + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExt_GesturePassword(t *testing.T) { + split := strings.Split("6304258", "") + password := make([]int, len(split)) + for i := range split { + password[i], _ = strconv.Atoi(split[i]) + } + + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" + + err = driverExt.GesturePassword(pathSearch, password...) + checkErr(t, err) +} diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go new file mode 100644 index 00000000..44b9d36c --- /dev/null +++ b/hrp/internal/uixt/swipe.go @@ -0,0 +1,135 @@ +package uixt + +func (dExt *DriverExt) SwipeTo(direction string) (err error) { + width := dExt.windowSize.Width + height := dExt.windowSize.Height + + var fromX, fromY, toX, toY int + switch direction { + case "up": + fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4 + case "down": + fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 + case "left": + fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2 + case "right": + fromX, fromY, toX, toY = width*1/4, height/2, width*3/4, height/2 + } + return dExt.WebDriver.Swipe(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) Swipe(pathname string, toX, toY int) (err error) { + return dExt.SwipeFloat(pathname, float64(toX), float64(toY)) +} + +func (dExt *DriverExt) SwipeFloat(pathname string, toX, toY float64) (err error) { + return dExt.SwipeOffsetFloat(pathname, toX, toY, 0.5, 0.5) +} + +func (dExt *DriverExt) SwipeOffset(pathname string, toX, toY int, xOffset, yOffset float64) (err error) { + return dExt.SwipeOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset) +} + +func (dExt *DriverExt) SwipeOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64) (err error) { + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) SwipeUp(pathname string, distance ...float64) (err error) { + return dExt.SwipeUpOffset(pathname, 0.5, 0.9, distance...) +} + +func (dExt *DriverExt) SwipeUpOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { + if len(distance) == 0 { + distance = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := (y + height) - height*(1.0-yOffset) + + toX := fromX + toY := fromY - height*distance[0] + + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) SwipeDown(pathname string, distance ...float64) (err error) { + return dExt.SwipeDownOffset(pathname, 0.5, 0.1, distance...) +} + +func (dExt *DriverExt) SwipeDownOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { + if len(distance) == 0 { + distance = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + toX := fromX + toY := fromY + height*distance[0] + + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) SwipeLeft(pathname string, distance ...float64) (err error) { + return dExt.SwipeLeftOffset(pathname, 0.9, 0.5, distance...) +} + +func (dExt *DriverExt) SwipeLeftOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { + if len(distance) == 0 { + distance = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + toX := fromX - width*distance[0] + toY := fromY + + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) SwipeRight(pathname string, distance ...float64) (err error) { + return dExt.SwipeRightOffset(pathname, 0.1, 0.5, distance...) +} + +func (dExt *DriverExt) SwipeRightOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { + if len(distance) == 0 { + distance = []float64{1.0} + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + fromX := x + width*xOffset + fromY := y + height*yOffset + + toX := fromX + width*distance[0] + toY := fromY + + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go new file mode 100644 index 00000000..f8e0650c --- /dev/null +++ b/hrp/internal/uixt/swipe_test.go @@ -0,0 +1,33 @@ +package uixt + +import ( + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExt_Swipe(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" + + // gwda.SetDebug(true) + + err = driverExt.Swipe(pathSearch, 300, 500) + checkErr(t, err) + + err = driverExt.SwipeFloat(pathSearch, 300.9, 500) + checkErr(t, err) + + err = driverExt.SwipeOffset(pathSearch, 300, 500, 0.2, 0.5) + checkErr(t, err) + + driverExt.Debug(DmNotMatch) + + err = driverExt.OnlyOnceThreshold(0.92).SwipeOffsetFloat(pathSearch, 300.9, 499.1, 0.2, 0.5) + checkErr(t, err) +} diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go new file mode 100644 index 00000000..d2c11290 --- /dev/null +++ b/hrp/internal/uixt/tap.go @@ -0,0 +1,88 @@ +package uixt + +import ( + "fmt" + + "github.com/electricbubble/gwda" +) + +func (dExt *DriverExt) TapXY(x, y float64) error { + // tap on coordinate: [x, y] should be relative + if x > 1 || y > 1 { + return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) + } + + x = x * float64(dExt.windowSize.Width) + y = y * float64(dExt.windowSize.Height) + return dExt.WebDriver.TapFloat(x, y) +} + +func (dExt *DriverExt) Tap(param string) error { + return dExt.TapOffset(param, 0.5, 0.5) +} + +func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64) (err error) { + // click on element, find by name attribute + ele, err := dExt.FindUIElement(param) + if err == nil { + return ele.Click() + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + return err + } + + return dExt.WebDriver.TapFloat(x+width*xOffset, y+height*yOffset) +} + +func (dExt *DriverExt) DoubleTapXY(x, y float64) error { + // double tap on coordinate: [x, y] should be relative + if x > 1 || y > 1 { + return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) + } + + x = x * float64(dExt.windowSize.Width) + y = y * float64(dExt.windowSize.Height) + return dExt.WebDriver.DoubleTapFloat(x, y) +} + +func (dExt *DriverExt) DoubleTap(param string) (err error) { + return dExt.DoubleTapOffset(param, 0.5, 0.5) +} + +func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64) (err error) { + // click on element, find by name attribute + ele, err := dExt.FindUIElement(param) + if err == nil { + return ele.DoubleTap() + } + + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + return err + } + + return dExt.WebDriver.DoubleTapFloat(x+width*xOffset, y+height*yOffset) +} + +// TapWithNumber sends one or more taps +func (dExt *DriverExt) TapWithNumber(param string, numberOfTaps int) (err error) { + return dExt.TapWithNumberOffset(param, numberOfTaps, 0.5, 0.5) +} + +func (dExt *DriverExt) TapWithNumberOffset(param string, numberOfTaps int, xOffset, yOffset float64) (err error) { + if numberOfTaps <= 0 { + numberOfTaps = 1 + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + return err + } + + x = x + width*xOffset + y = y + height*yOffset + + touchActions := gwda.NewTouchActions().Tap(gwda.NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps)) + return dExt.PerformTouchActions(touchActions) +} diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go new file mode 100644 index 00000000..f519af3b --- /dev/null +++ b/hrp/internal/uixt/tap_test.go @@ -0,0 +1,48 @@ +package uixt + +import ( + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExt_TapWithNumber(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" + + // gwda.SetDebug(true) + + err = driverExt.TapWithNumber(pathSearch, 3) + checkErr(t, err) + + err = driverExt.TapWithNumberOffset(pathSearch, 3, 0.5, 0.75) + checkErr(t, err) +} + +func TestDriverExt_TapXY(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + err = driverExt.TapXY(0.4, 0.5) + checkErr(t, err) +} + +func TestDriverExt_TapWithOCR(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + // 需要点击文字上方的图标 + err = driverExt.TapOffset("抖音", 0.5, -1) + checkErr(t, err) +} diff --git a/hrp/internal/uixt/touch.go b/hrp/internal/uixt/touch.go new file mode 100644 index 00000000..6c06ae81 --- /dev/null +++ b/hrp/internal/uixt/touch.go @@ -0,0 +1,33 @@ +package uixt + +func (dExt *DriverExt) ForceTouch(pathname string, pressure float64, duration ...float64) (err error) { + return dExt.ForceTouchOffset(pathname, pressure, 0.5, 0.5, duration...) +} + +func (dExt *DriverExt) ForceTouchOffset(pathname string, pressure, xOffset, yOffset float64, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + return dExt.ForceTouchFloat(x+width*xOffset, y+height*yOffset, pressure, duration[0]) +} + +func (dExt *DriverExt) TouchAndHold(pathname string, duration ...float64) (err error) { + return dExt.TouchAndHoldOffset(pathname, 0.5, 0.5, duration...) +} + +func (dExt *DriverExt) TouchAndHoldOffset(pathname string, xOffset, yOffset float64, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + var x, y, width, height float64 + if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { + return err + } + + return dExt.TouchAndHoldFloat(x+width*xOffset, y+height*yOffset, duration[0]) +} diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go new file mode 100644 index 00000000..514e2fae --- /dev/null +++ b/hrp/internal/uixt/touch_test.go @@ -0,0 +1,55 @@ +package uixt + +import ( + "testing" + + "github.com/electricbubble/gwda" +) + +func TestDriverExt_ForceTouch(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" + + err = driverExt.ForceTouch(pathSearch, 0.5, 3) + checkErr(t, err) + + // err = driverExt.ForceTouchOffset(pathSearch, 0.5, 0.1, 0.9) + // checkErr(t, err) + + // err = driverExt.ForceTouchOffset(pathSearch, 0.2, 1.1, -1) + // checkErr(t, err) +} + +func TestDriverExt_TouchAndHold(t *testing.T) { + driver, err := gwda.NewUSBDriver(nil) + checkErr(t, err) + + driverExt, err := Extend(driver, 0.95) + checkErr(t, err) + + pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" + + // err = driverExt.TouchAndHold(pathSearch) + // checkErr(t, err) + + // err = driverExt.TouchAndHold(pathSearch, 3) + // checkErr(t, err) + + err = driverExt.TouchAndHoldOffset(pathSearch, 0.8, 0.1) + checkErr(t, err) +} + +func checkErr(t *testing.T, err error, msg ...string) { + if err != nil { + if len(msg) == 0 { + t.Fatal(err) + } else { + t.Fatal(msg, err) + } + } +} diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 6b9358b8..bf02c121 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -6,9 +6,10 @@ import ( "time" "github.com/electricbubble/gwda" - "github.com/httprunner/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) const ( @@ -651,23 +652,6 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* return } -func (w *wdaClient) findElement(param string) (ele gwda.WebElement, err error) { - var selector gwda.BySelector - if strings.HasPrefix(param, "/") { - // xpath - selector = gwda.BySelector{ - XPath: param, - } - } else { - // name - selector = gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithName(param), - } - } - - return w.DriverExt.FindElement(selector) -} - func (w *wdaClient) assertName(name string, exists bool) bool { selector := gwda.BySelector{ LinkText: gwda.NewElementAttribute().WithName(name), From ad730b354245f55ae6bd5f735fea054fe6417b93 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 10:31:16 +0800 Subject: [PATCH 034/169] change: remove xpath validation --- hrp/step.go | 1 - hrp/step_android_ui.go | 16 +++++++-------- hrp/step_android_ui_test.go | 6 +++--- hrp/step_ios_ui.go | 40 ------------------------------------- 4 files changed, 11 insertions(+), 52 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 507182a4..87c48953 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -42,7 +42,6 @@ const ( // UI validation uiSelectorName string = "ui_name" - uiSelectorXpath string = "ui_xpath" uiSelectorOCR string = "ui_ocr" assertionExists string = "exists" assertionNotExists string = "not_exists" diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 8dac1687..abc1a9be 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -158,31 +158,31 @@ type StepAndroidValidation struct { step *TStep } -func (s *StepAndroidValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepAndroidValidation { +func (s *StepAndroidValidation) AssertNameExists(expectedName string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: uiSelectorXpath, + Check: uiSelectorName, Assert: assertionExists, - Expect: expectedXpath, + Expect: expectedName, } if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("xpath [%s] not found", expectedXpath) + v.Message = fmt.Sprintf("[%s] not found", expectedName) } s.step.Validators = append(s.step.Validators, v) return s } -func (s *StepAndroidValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepAndroidValidation { +func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: uiSelectorXpath, + Check: uiSelectorName, Assert: assertionNotExists, - Expect: expectedXpath, + Expect: expectedName, } if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("xpath [%s] should not exist", expectedXpath) + v.Message = fmt.Sprintf("[%s] should not exist", expectedName) } s.step.Validators = append(s.step.Validators, v) return s diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go index 13d3cba9..7ca077f9 100644 --- a/hrp/step_android_ui_test.go +++ b/hrp/step_android_ui_test.go @@ -10,10 +10,10 @@ func TestAndroidAction(t *testing.T) { Config: NewConfig("android ui action"), TestSteps: []IStep{ NewStep("launch douyin"). - Android().Serial("xxx").Click("抖音"). + Android().Serial("xxx").Tap("抖音"). Validate(). - AssertXpathExists("首页", "首页 tab 不存在"). - AssertXpathExists("消息", "消息 tab 不存在"), + AssertNameExists("首页", "首页 tab 不存在"). + AssertNameExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), }, diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index bf02c121..6c458c6a 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -288,36 +288,6 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri return s } -func (s *StepIOSValidation) AssertXpathExists(expectedXpath string, msg ...string) *StepIOSValidation { - v := Validator{ - Check: uiSelectorXpath, - Assert: assertionExists, - Expect: expectedXpath, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("xpath [%s] not found", expectedXpath) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepIOSValidation) AssertXpathNotExists(expectedXpath string, msg ...string) *StepIOSValidation { - v := Validator{ - Check: uiSelectorXpath, - Assert: assertionNotExists, - Expect: expectedXpath, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("xpath [%s] should not exist", expectedXpath) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - func (s *StepIOSValidation) Name() string { return s.step.Name } @@ -628,8 +598,6 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* switch validator.Check { case uiSelectorName: result = w.assertName(expected, exists) - case uiSelectorXpath: - result = w.assertXpath(expected, exists) } if result { @@ -659,11 +627,3 @@ func (w *wdaClient) assertName(name string, exists bool) bool { _, err := w.DriverExt.FindElement(selector) return exists == (err == nil) } - -func (w *wdaClient) assertXpath(xpath string, exists bool) bool { - selector := gwda.BySelector{ - XPath: xpath, - } - _, err := w.DriverExt.FindElement(selector) - return exists == (err == nil) -} From 939c153d370e3dcfefde3f18d672084a6bc71a7c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 12:04:21 +0800 Subject: [PATCH 035/169] feat: validate with label --- hrp/step.go | 1 + hrp/step_ios_ui.go | 40 ++++++++++++++++++++++++++++++++++++++++ hrp/step_ios_ui_test.go | 20 ++++++++++---------- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/hrp/step.go b/hrp/step.go index 87c48953..713b3417 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -42,6 +42,7 @@ const ( // UI validation uiSelectorName string = "ui_name" + uiSelectorLabel string = "ui_label" uiSelectorOCR string = "ui_ocr" assertionExists string = "exists" assertionNotExists string = "not_exists" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 6c458c6a..fb1f0a88 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -288,6 +288,36 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri return s } +func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorLabel, + Assert: assertionExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] not found", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorLabel, + Assert: assertionNotExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] should not exist", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepIOSValidation) Name() string { return s.step.Name } @@ -598,6 +628,8 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* switch validator.Check { case uiSelectorName: result = w.assertName(expected, exists) + case uiSelectorLabel: + result = w.assertLabel(expected, exists) } if result { @@ -627,3 +659,11 @@ func (w *wdaClient) assertName(name string, exists bool) bool { _, err := w.DriverExt.FindElement(selector) return exists == (err == nil) } + +func (w *wdaClient) assertLabel(label string, exists bool) bool { + selector := gwda.BySelector{ + LinkText: gwda.NewElementAttribute().WithLabel(label), + } + _, err := w.DriverExt.FindElement(selector) + return exists == (err == nil) +} diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 246ede90..2b6abd61 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -12,8 +12,8 @@ func TestIOSSettingsAction(t *testing.T) { NewStep("launch Settings"). IOS().Home().Tap("//*[@label='设置']"). Validate(). - AssertNameExists("飞行模式"). - AssertNameNotExists("飞行模式2"), + AssertLabelExists("飞行模式"). + AssertLabelNotExists("飞行模式2"), NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), }, @@ -33,7 +33,7 @@ func TestIOSSearchApp(t *testing.T) { NewStep("进入 App 资源库 搜索框"). IOS().Home().SwipeLeft().Times(2).Tap("dewey-search-field"). Validate(). - AssertNameExists("取消"), + AssertLabelExists("取消"), NewStep("搜索抖音"). IOS().Input("抖音\n"), }, @@ -80,13 +80,13 @@ func TestIOSWeixinLive(t *testing.T) { AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 Tap("微信"). Validate(). - AssertNameExists("通讯录", "微信启动失败,「通讯录」不存在"), + AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), NewStep("进入直播页"). IOS(). Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 TapXY(0.5, 0.3). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 Validate(). - AssertNameExists("直播"), + AssertLabelExists("直播"), NewStep("向上滑动 5 次"). IOS(). SwipeUp().Times(3).ScreenShot(). // 上划 3 次,截图保存 @@ -110,7 +110,7 @@ func TestIOSCameraPhotoCapture(t *testing.T) { StopCamera(). StartCamera(). Validate(). - AssertNameExists("PhotoCapture", "拍照按钮不存在"), + AssertLabelExists("PhotoCapture", "拍照按钮不存在"), NewStep("start recording"). IOS().Tap("PhotoCapture"), }, @@ -132,12 +132,12 @@ func TestIOSCameraVideoCapture(t *testing.T) { StopCamera(). StartCamera(). Validate(). - AssertNameExists("PhotoCapture", "录像按钮不存在"), + AssertLabelExists("PhotoCapture", "录像按钮不存在"), NewStep("switch to video capture"). IOS(). SwipeRight(). Validate(). - AssertNameExists("VideoCapture", "拍摄按钮不存在"), + AssertLabelExists("VideoCapture", "拍摄按钮不存在"), NewStep("start recording"). IOS(). Tap("VideoCapture"). // 开始录像 @@ -160,8 +160,8 @@ func TestIOSDouyinAction(t *testing.T) { NewStep("launch douyin"). IOS().Home().Tap("//*[@label='抖音']"). Validate(). - AssertNameExists("首页", "首页 tab 不存在"). - AssertNameExists("消息", "消息 tab 不存在"), + AssertLabelExists("首页", "首页 tab 不存在"). + AssertLabelExists("消息", "消息 tab 不存在"), NewStep("swipe up and down"). IOS().SwipeUp().Times(3).SwipeDown(), }, From 2cd10989f0490805dd067fc52d1f5b39bb673fac Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 12:18:45 +0800 Subject: [PATCH 036/169] feat: validate with ocr text --- hrp/step_ios_ui.go | 37 +++++++++++++++++++++++++++++++++++++ hrp/step_ios_ui_test.go | 4 +++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index fb1f0a88..beebd782 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -318,6 +318,36 @@ func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...st return s } +func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorOCR, + Assert: assertionExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] not found", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorOCR, + Assert: assertionNotExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("[%s] should not exist", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepIOSValidation) Name() string { return s.step.Name } @@ -630,6 +660,8 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* result = w.assertName(expected, exists) case uiSelectorLabel: result = w.assertLabel(expected, exists) + case uiSelectorOCR: + result = w.assertOCR(expected, exists) } if result { @@ -667,3 +699,8 @@ func (w *wdaClient) assertLabel(label string, exists bool) bool { _, err := w.DriverExt.FindElement(selector) return exists == (err == nil) } + +func (w *wdaClient) assertOCR(text string, exists bool) bool { + _, _, _, _, err := w.DriverExt.FindTextByOCR(text) + return exists == (err == nil) +} diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 2b6abd61..6711fed6 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -10,9 +10,11 @@ func TestIOSSettingsAction(t *testing.T) { Config: NewConfig("ios ui action on Settings"), TestSteps: []IStep{ NewStep("launch Settings"). - IOS().Home().Tap("//*[@label='设置']"). + IOS().Home().Tap("设置"). Validate(). + AssertNameExists("飞行模式"). AssertLabelExists("飞行模式"). + AssertOCRExists("飞行模式"). AssertLabelNotExists("飞行模式2"), NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), From 5effb3897c3ce1832826af0b86e93455c185fa1c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 14:55:34 +0800 Subject: [PATCH 037/169] refactor: relocate code --- go.mod | 2 +- go.sum | 4 +- hrp/internal/uixt/ext.go | 21 ++++- hrp/internal/uixt/init.go | 72 ++++++++++++++++ hrp/runner.go | 2 +- hrp/step_ios_ui.go | 170 ++++++++------------------------------ hrp/step_ios_ui_test.go | 5 +- 7 files changed, 133 insertions(+), 143 deletions(-) create mode 100644 hrp/internal/uixt/init.go diff --git a/go.mod b/go.mod index 592963da..bd615864 100644 --- a/go.mod +++ b/go.mod @@ -40,4 +40,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 +replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7 diff --git a/go.sum b/go.sum index 0d59b3cb..d0059fe4 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7 h1:DjPOXlkeCsxtFzieys2RjYEn6OCoAPQNiLmG2eeSVgw= -github.com/debugtalk/gwda v0.0.0-20220824022606-02ad6ca51de7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= +github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7 h1:pAvqLivdxSqCttO6lbEzg/zjxJO6oOQayfPKqBVD3t0= +github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 4b24ccb8..cb8c57a3 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -332,4 +332,23 @@ func (dExt *DriverExt) PerformActions(actions *gwda.W3CActions) error { return dExt.PerformW3CActions(actions) } -// IsExist +func (dExt *DriverExt) IsNameExist(name string) bool { + selector := gwda.BySelector{ + LinkText: gwda.NewElementAttribute().WithName(name), + } + _, err := dExt.FindElement(selector) + return err == nil +} + +func (dExt *DriverExt) IsLabelExist(label string) bool { + selector := gwda.BySelector{ + LinkText: gwda.NewElementAttribute().WithLabel(label), + } + _, err := dExt.FindElement(selector) + return err == nil +} + +func (dExt *DriverExt) IsOCRExist(text string) bool { + _, _, _, _, err := dExt.FindTextByOCR(text) + return err == nil +} diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/init.go new file mode 100644 index 00000000..64c0c011 --- /dev/null +++ b/hrp/internal/uixt/init.go @@ -0,0 +1,72 @@ +package uixt + +import ( + "github.com/electricbubble/gwda" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +const ( + // Changes the value of maximum depth for traversing elements source tree. + // It may help to prevent out of memory or timeout errors while getting the elements source tree, + // but it might restrict the depth of source tree. + // A part of elements source tree might be lost if the value was too small. Defaults to 50 + snapshotMaxDepth = 10 + // Allows to customize accept/dismiss alert button selector. + // It helps you to handle an arbitrary element as accept button in accept alert command. + // The selector should be a valid class chain expression, where the search root is the alert element itself. + // The default button location algorithm is used if the provided selector is wrong or does not match any element. + // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] + acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" + dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" +) + +func InitWDAClient(udid string, port, mjpeg_port int) (*DriverExt, error) { + // init wda device + var options []gwda.DeviceOption + if udid != "" { + options = append(options, gwda.WithSerialNumber(udid)) + } + if port != 0 { + options = append(options, gwda.WithPort(port)) + } + if mjpeg_port != 0 { + options = append(options, gwda.WithMjpegPort(mjpeg_port)) + } + targetDevice, err := gwda.NewDevice(options...) + if err != nil { + return nil, err + } + + // switch to iOS springboard before init WDA session + // aviod getting stuck when some super app is activate such as douyin or wexin + log.Info().Msg("switch to iOS springboard") + bundleID := "com.apple.springboard" + _, err = targetDevice.GIDevice().AppLaunch(bundleID) + if err != nil { + return nil, errors.Wrap(err, "launch springboard failed") + } + + // init WDA driver + gwda.SetDebug(true) + capabilities := gwda.NewCapabilities() + capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) + driver, err := gwda.NewUSBDriver(capabilities, *targetDevice) + if err != nil { + return nil, errors.Wrap(err, "failed to init WDA driver") + } + driverExt, err := Extend(driver, 0.95) + if err != nil { + return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") + } + settings, err := driverExt.SetAppiumSettings(map[string]interface{}{ + "snapshotMaxDepth": snapshotMaxDepth, + "acceptAlertButtonSelector": acceptAlertButtonSelector, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set appium WDA settings") + } + log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + + return driverExt, nil +} diff --git a/hrp/runner.go b/hrp/runner.go index 0a7002f9..690fc232 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -71,7 +71,7 @@ type HRPRunner struct { httpClient *http.Client http2Client *http.Client wsDialer *websocket.Dialer - wdaClients map[string]*wdaClient // wda client used for iOS UI automation, key is udid + wdaClients map[string]*uiDriver // wda client used for iOS UI automation, key is udid } // SetClientTransport configures transport of http client for high concurrency load testing diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index beebd782..823968cd 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -5,28 +5,12 @@ import ( "strings" "time" - "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -const ( - // Changes the value of maximum depth for traversing elements source tree. - // It may help to prevent out of memory or timeout errors while getting the elements source tree, - // but it might restrict the depth of source tree. - // A part of elements source tree might be lost if the value was too small. Defaults to 50 - snapshotMaxDepth = 10 - // Allows to customize accept/dismiss alert button selector. - // It helps you to handle an arbitrary element as accept button in accept alert command. - // The selector should be a valid class chain expression, where the search root is the alert element itself. - // The default button location algorithm is used if the provided selector is wrong or does not match any element. - // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] - acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" - dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" -) - type IOSConfig struct { WDADevice } @@ -364,23 +348,7 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err error) { - defer func() { - if err != nil { - return - } - // check if WDA is healthy - ok, e := client.DriverExt.IsWdaHealthy() - if err != nil { - err = errors.Wrap(e, "check WDA health failed") - return - } - if !ok { - err = errors.New("WDA is not healthy") - return - } - }() - +func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error) { // avoid duplicate init if device.UDID == "" && len(r.wdaClients) == 1 { for _, v := range r.wdaClients { @@ -388,71 +356,26 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *wdaClient, err erro } } - // init wda device - var options []gwda.DeviceOption + // avoid duplicate init if device.UDID != "" { - options = append(options, gwda.WithSerialNumber(device.UDID)) + if client, ok := r.wdaClients[device.UDID]; ok { + return client, nil + } } - if device.Port != 0 { - options = append(options, gwda.WithPort(device.Port)) - } - if device.MjpegPort != 0 { - options = append(options, gwda.WithMjpegPort(device.MjpegPort)) - } - targetDevice, err := gwda.NewDevice(options...) + + driverExt, err := uixt.InitWDAClient(device.UDID, device.Port, device.MjpegPort) if err != nil { return nil, err } - - // avoid duplicate init - if client, ok := r.wdaClients[targetDevice.SerialNumber()]; ok { - return client, nil - } - - // switch to iOS springboard before init WDA session - // aviod getting stuck when some super app is activate such as douyin or wexin - log.Info().Msg("switch to iOS springboard") - bundleID := "com.apple.springboard" - _, err = targetDevice.GIDevice().AppLaunch(bundleID) - if err != nil { - return nil, errors.Wrap(err, "launch springboard failed") - } - - // init WDA driver - gwda.SetDebug(true) - capabilities := gwda.NewCapabilities() - capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) - driver, err := gwda.NewUSBDriver(capabilities, *targetDevice) - if err != nil { - return nil, errors.Wrap(err, "failed to init WDA driver") - } - driverExt, err := uixt.Extend(driver, 0.95) - if err != nil { - return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") - } - settings, err := driverExt.SetAppiumSettings(map[string]interface{}{ - "snapshotMaxDepth": snapshotMaxDepth, - "acceptAlertButtonSelector": acceptAlertButtonSelector, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to set appium WDA settings") - } - log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") - - // get device window size - windowSize, err := driverExt.WindowSize() - if err != nil { - return nil, errors.Wrap(err, "failed to get windows size") + client = &uiDriver{ + DriverExt: *driverExt, } // cache wda client - r.wdaClients = make(map[string]*wdaClient) - client = &wdaClient{ - Device: targetDevice, - DriverExt: driverExt, - WindowSize: windowSize, + if r.wdaClients == nil { + r.wdaClients = make(map[string]*uiDriver) } - r.wdaClients[targetDevice.SerialNumber()] = client + r.wdaClients[device.UDID] = client return client, nil } @@ -512,13 +435,11 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro var errActionNotImplemented = errors.New("UI action not implemented") -type wdaClient struct { - Device *gwda.Device - DriverExt *uixt.DriverExt - WindowSize gwda.Size +type uiDriver struct { + uixt.DriverExt } -func (w *wdaClient) doAction(action MobileAction) error { +func (ud *uiDriver) doAction(action MobileAction) error { log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") switch action.Method { @@ -527,17 +448,17 @@ func (w *wdaClient) doAction(action MobileAction) error { return errActionNotImplemented case appLaunch: if bundleId, ok := action.Params.(string); ok { - return w.DriverExt.AppLaunch(bundleId) + return ud.AppLaunch(bundleId) } return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params) case appLaunchUnattached: if bundleId, ok := action.Params.(string); ok { - return w.DriverExt.AppLaunchUnattached(bundleId) + return ud.AppLaunchUnattached(bundleId) } return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params) case appTerminate: if bundleId, ok := action.Params.(string); ok { - success, err := w.DriverExt.AppTerminate(bundleId) + success, err := ud.AppTerminate(bundleId) if err != nil { return errors.Wrap(err, "failed to terminate app") } @@ -548,19 +469,19 @@ func (w *wdaClient) doAction(action MobileAction) error { } return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) case uiHome: - return w.DriverExt.Homescreen() + return ud.Homescreen() case uiTapXY: if location, ok := action.Params.([]float64); ok { // relative x,y of window size: [0.5, 0.5] if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return w.DriverExt.TapXY(location[0], location[1]) + return ud.TapXY(location[0], location[1]) } return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) case uiTap: if param, ok := action.Params.(string); ok { - return w.DriverExt.Tap(param) + return ud.Tap(param) } return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) case uiDoubleTapXY: @@ -569,17 +490,17 @@ func (w *wdaClient) doAction(action MobileAction) error { if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return w.DriverExt.DoubleTapXY(location[0], location[1]) + return ud.DoubleTapXY(location[0], location[1]) } return fmt.Errorf("invalid %s params: %v", uiDoubleTapXY, action.Params) case uiDoubleTap: if param, ok := action.Params.(string); ok { - return w.DriverExt.DoubleTap(param) + return ud.DoubleTap(param) } return fmt.Errorf("invalid %s params: %v", uiDoubleTap, action.Params) case uiSwipe: if param, ok := action.Params.(string); ok { - return w.DriverExt.SwipeTo(param) + return ud.SwipeTo(param) } return fmt.Errorf("invalid %s params: %v", uiSwipe, action.Params) case uiInput: @@ -587,7 +508,7 @@ func (w *wdaClient) doAction(action MobileAction) error { // append \n to send text with enter // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) - return w.DriverExt.SendKeys(param) + return ud.SendKeys(param) case ctlSleep: if param, ok := action.Params.(int); ok { time.Sleep(time.Duration(param) * time.Second) @@ -600,18 +521,18 @@ func (w *wdaClient) doAction(action MobileAction) error { var screenshotPath string var err error if param, ok := action.Params.(string); ok { - screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%s", param)) + screenshotPath, err = ud.ScreenShot(fmt.Sprintf("screenshot_%s", param)) } else { - screenshotPath, err = w.DriverExt.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) + screenshotPath, err = ud.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) } log.Info().Str("path", screenshotPath).Msg("take screenshot") return err case ctlStartCamera: // start camera, alias for app_launch com.apple.camera - return w.DriverExt.AppLaunch("com.apple.camera") + return ud.AppLaunch("com.apple.camera") case ctlStopCamera: // stop camera, alias for app_terminate com.apple.camera - success, err := w.DriverExt.AppTerminate("com.apple.camera") + success, err := ud.AppTerminate("com.apple.camera") if err != nil { return errors.Wrap(err, "failed to terminate camera") } @@ -623,7 +544,7 @@ func (w *wdaClient) doAction(action MobileAction) error { return nil } -func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []*ValidationResult, err error) { +func (ud *uiDriver) doValidation(iValidators []interface{}) (validateResults []*ValidationResult, err error) { for _, iValidator := range iValidators { validator, ok := iValidator.(Validator) if !ok { @@ -657,11 +578,11 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* var result bool switch validator.Check { case uiSelectorName: - result = w.assertName(expected, exists) + result = (ud.IsNameExist(expected) == exists) case uiSelectorLabel: - result = w.assertLabel(expected, exists) + result = (ud.IsLabelExist(expected) == exists) case uiSelectorOCR: - result = w.assertOCR(expected, exists) + result = (ud.IsOCRExist(expected) == exists) } if result { @@ -678,29 +599,8 @@ func (w *wdaClient) doValidation(iValidators []interface{}) (validateResults []* Str("msg", validator.Message). Msg("validate UI failed") validateResults = append(validateResults, validataResult) - err = errors.New("step validation failed") + return validateResults, errors.New("step validation failed") } } - return -} - -func (w *wdaClient) assertName(name string, exists bool) bool { - selector := gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithName(name), - } - _, err := w.DriverExt.FindElement(selector) - return exists == (err == nil) -} - -func (w *wdaClient) assertLabel(label string, exists bool) bool { - selector := gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithLabel(label), - } - _, err := w.DriverExt.FindElement(selector) - return exists == (err == nil) -} - -func (w *wdaClient) assertOCR(text string, exists bool) bool { - _, _, _, _, err := w.DriverExt.FindTextByOCR(text) - return exists == (err == nil) + return validateResults, nil } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 6711fed6..c9e8732c 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -13,9 +13,8 @@ func TestIOSSettingsAction(t *testing.T) { IOS().Home().Tap("设置"). Validate(). AssertNameExists("飞行模式"). - AssertLabelExists("飞行模式"). - AssertOCRExists("飞行模式"). - AssertLabelNotExists("飞行模式2"), + AssertLabelExists("蓝牙"). + AssertOCRExists("个人热点"), NewStep("swipe up and down"). IOS().SwipeUp().SwipeUp().SwipeDown(), }, From 0ff59bebb534e210aeea38e8f44e6bf939d14711 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 23:07:31 +0800 Subject: [PATCH 038/169] feat: tap by ocr and cv --- hrp/internal/uixt/ext.go | 4 ++-- hrp/internal/uixt/ext_ocr.go | 4 ++-- hrp/internal/uixt/tap.go | 18 ++++++++++++++++++ hrp/step.go | 2 ++ hrp/step_ios_ui.go | 28 ++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index cb8c57a3..7c9bb098 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -297,9 +297,9 @@ func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height flo return dExt.FindImageRectInUIKit(search) } -func (dExt *DriverExt) FindImageRectInUIKit(search string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { var bufSource, bufSearch *bytes.Buffer - if bufSearch, err = getBufFromDisk(search); err != nil { + if bufSearch, err = getBufFromDisk(imagePath); err != nil { return 0, 0, 0, 0, err } if bufSource, err = dExt.takeScreenShot(); err != nil { diff --git a/hrp/internal/uixt/ext_ocr.go b/hrp/internal/uixt/ext_ocr.go index 78581b3e..dc267111 100644 --- a/hrp/internal/uixt/ext_ocr.go +++ b/hrp/internal/uixt/ext_ocr.go @@ -118,7 +118,7 @@ type OCRService interface { FindText(text string, imageBuf []byte) (rect image.Rectangle, err error) } -func (dExt *DriverExt) FindTextByOCR(search string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("screenshot error: %v", err) @@ -126,7 +126,7 @@ func (dExt *DriverExt) FindTextByOCR(search string) (x, y, width, height float64 } service := &veDEMOCRService{} - rect, err := service.FindText(search, bufSource.Bytes()) + rect, err := service.FindText(ocrText, bufSource.Bytes()) if err != nil { err = fmt.Errorf("find text failed: %v", err) return diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index d2c11290..5b7d1d42 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -17,6 +17,24 @@ func (dExt *DriverExt) TapXY(x, y float64) error { return dExt.WebDriver.TapFloat(x, y) } +func (dExt *DriverExt) TapByOCR(ocrText string) error { + x, y, width, height, err := dExt.FindTextByOCR(ocrText) + if err != nil { + return err + } + + return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) +} + +func (dExt *DriverExt) TapByCV(imagePath string) error { + x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath) + if err != nil { + return err + } + + return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) +} + func (dExt *DriverExt) Tap(param string) error { return dExt.TapOffset(param, 0.5, 0.5) } diff --git a/hrp/step.go b/hrp/step.go index 713b3417..d0b99aa4 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -34,6 +34,8 @@ const ( // UI handling uiHome MobileMethod = "home" uiTapXY MobileMethod = "tap_xy" + uiTapByOCR MobileMethod = "tap_ocr" + uiTapByCV MobileMethod = "tap_cv" uiTap MobileMethod = "tap" uiDoubleTapXY MobileMethod = "double_tap_xy" uiDoubleTap MobileMethod = "double_tap" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 823968cd..370234dd 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -95,6 +95,24 @@ func (s *StepIOS) Tap(params string) *StepIOS { return &StepIOS{step: s.step} } +// Tap taps on the target element by OCR recognition +func (s *StepIOS) TapByOCR(ocrText string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiTapByOCR, + Params: ocrText, + }) + return &StepIOS{step: s.step} +} + +// Tap taps on the target element by CV recognition +func (s *StepIOS) TapByCV(imagePath string) *StepIOS { + s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ + Method: uiTapByCV, + Params: imagePath, + }) + return &StepIOS{step: s.step} +} + // DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates func (s *StepIOS) DoubleTapXY(x, y float64) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ @@ -484,6 +502,16 @@ func (ud *uiDriver) doAction(action MobileAction) error { return ud.Tap(param) } return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) + case uiTapByOCR: + if ocrText, ok := action.Params.(string); ok { + return ud.TapByOCR(ocrText) + } + return fmt.Errorf("invalid %s params: %v", uiTapByOCR, action.Params) + case uiTapByCV: + if imagePath, ok := action.Params.(string); ok { + return ud.TapByCV(imagePath) + } + return fmt.Errorf("invalid %s params: %v", uiTapByCV, action.Params) case uiDoubleTapXY: if location, ok := action.Params.([]float64); ok { // relative x,y of window size: [0.5, 0.5] From f2b25df87e19c338e049a676587fa05f394a42d6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 28 Aug 2022 23:17:12 +0800 Subject: [PATCH 039/169] feat: validate image exists --- hrp/internal/uixt/ext.go | 5 +++ hrp/internal/uixt/{ext_ocr.go => ocr.go} | 0 .../uixt/{ext_ocr_test.go => ocr_test.go} | 0 hrp/step.go | 1 + hrp/step_ios_ui.go | 44 ++++++++++++++++--- 5 files changed, 44 insertions(+), 6 deletions(-) rename hrp/internal/uixt/{ext_ocr.go => ocr.go} (100%) rename hrp/internal/uixt/{ext_ocr_test.go => ocr_test.go} (100%) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 7c9bb098..5855491e 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -352,3 +352,8 @@ func (dExt *DriverExt) IsOCRExist(text string) bool { _, _, _, _, err := dExt.FindTextByOCR(text) return err == nil } + +func (dExt *DriverExt) IsImageExist(text string) bool { + _, _, _, _, err := dExt.FindImageRectInUIKit(text) + return err == nil +} diff --git a/hrp/internal/uixt/ext_ocr.go b/hrp/internal/uixt/ocr.go similarity index 100% rename from hrp/internal/uixt/ext_ocr.go rename to hrp/internal/uixt/ocr.go diff --git a/hrp/internal/uixt/ext_ocr_test.go b/hrp/internal/uixt/ocr_test.go similarity index 100% rename from hrp/internal/uixt/ext_ocr_test.go rename to hrp/internal/uixt/ocr_test.go diff --git a/hrp/step.go b/hrp/step.go index d0b99aa4..709d6647 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -46,6 +46,7 @@ const ( uiSelectorName string = "ui_name" uiSelectorLabel string = "ui_label" uiSelectorOCR string = "ui_ocr" + uiSelectorImage string = "ui_image" assertionExists string = "exists" assertionNotExists string = "not_exists" ) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 370234dd..331ff537 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -269,7 +269,7 @@ func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] not found", expectedName) + v.Message = fmt.Sprintf("attribute name [%s] not found", expectedName) } s.step.Validators = append(s.step.Validators, v) return s @@ -284,7 +284,7 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] should not exist", expectedName) + v.Message = fmt.Sprintf("attribute name [%s] should not exist", expectedName) } s.step.Validators = append(s.step.Validators, v) return s @@ -299,7 +299,7 @@ func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...strin if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] not found", expectedLabel) + v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel) } s.step.Validators = append(s.step.Validators, v) return s @@ -314,7 +314,7 @@ func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...st if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] should not exist", expectedLabel) + v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel) } s.step.Validators = append(s.step.Validators, v) return s @@ -329,7 +329,7 @@ func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] not found", expectedText) + v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText) } s.step.Validators = append(s.step.Validators, v) return s @@ -344,7 +344,37 @@ func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...strin if len(msg) > 0 { v.Message = msg[0] } else { - v.Message = fmt.Sprintf("[%s] should not exist", expectedText) + v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorImage, + Assert: assertionExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepIOSValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepIOSValidation { + v := Validator{ + Check: uiSelectorImage, + Assert: assertionNotExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath) } s.step.Validators = append(s.step.Validators, v) return s @@ -611,6 +641,8 @@ func (ud *uiDriver) doValidation(iValidators []interface{}) (validateResults []* result = (ud.IsLabelExist(expected) == exists) case uiSelectorOCR: result = (ud.IsOCRExist(expected) == exists) + case uiSelectorImage: + result = (ud.IsImageExist(expected) == exists) } if result { From 90a4ee59f999644005d0202b3bc4cc85e86e4941 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 16:38:15 +0800 Subject: [PATCH 040/169] feat: support ignoreNotFoundError, ignore error if target element not found --- hrp/internal/uixt/tap.go | 23 ++++++++++++------ hrp/internal/uixt/tap_test.go | 2 +- hrp/step.go | 17 +++++++++++++ hrp/step_ios_ui.go | 46 +++++++++++++++++++++++------------ hrp/step_ios_ui_test.go | 2 +- 5 files changed, 66 insertions(+), 24 deletions(-) diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index 5b7d1d42..ca8176c1 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -17,37 +17,46 @@ func (dExt *DriverExt) TapXY(x, y float64) error { return dExt.WebDriver.TapFloat(x, y) } -func (dExt *DriverExt) TapByOCR(ocrText string) error { +func (dExt *DriverExt) TapByOCR(ocrText string, ignoreNotFoundError bool) error { x, y, width, height, err := dExt.FindTextByOCR(ocrText) if err != nil { + if ignoreNotFoundError { + return nil + } return err } return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) } -func (dExt *DriverExt) TapByCV(imagePath string) error { +func (dExt *DriverExt) TapByCV(imagePath string, ignoreNotFoundError bool) error { x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath) if err != nil { + if ignoreNotFoundError { + return nil + } return err } return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) } -func (dExt *DriverExt) Tap(param string) error { - return dExt.TapOffset(param, 0.5, 0.5) +func (dExt *DriverExt) Tap(param string, ignoreNotFoundError bool) error { + return dExt.TapOffset(param, 0.5, 0.5, ignoreNotFoundError) } -func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64) (err error) { +func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, ignoreNotFoundError bool) (err error) { // click on element, find by name attribute ele, err := dExt.FindUIElement(param) if err == nil { return ele.Click() } - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil { + x, y, width, height, err := dExt.FindUIRectInUIKit(param) + if err != nil { + if ignoreNotFoundError { + return nil + } return err } diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index f519af3b..174e86d6 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -43,6 +43,6 @@ func TestDriverExt_TapWithOCR(t *testing.T) { checkErr(t, err) // 需要点击文字上方的图标 - err = driverExt.TapOffset("抖音", 0.5, -1) + err = driverExt.TapOffset("抖音", 0.5, -1, false) checkErr(t, err) } diff --git a/hrp/step.go b/hrp/step.go index 709d6647..8e4715b7 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -54,6 +54,23 @@ const ( type MobileAction struct { Method MobileMethod `json:"method" yaml:"method"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` + + timeout int // TODO: wait timeout in seconds for mobile action + ignoreNotFoundError bool // ignore error if target element not found +} + +type ActionOption func(o *MobileAction) + +func WithTimeout(timeout int) ActionOption { + return func(o *MobileAction) { + o.timeout = timeout + } +} + +func WithIgnoreNotFoundError(ignoreError bool) ActionOption { + return func(o *MobileAction) { + o.ignoreNotFoundError = ignoreError + } } type StepResult struct { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 331ff537..cc6bacb3 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -87,29 +87,41 @@ func (s *StepIOS) TapXY(x, y float64) *StepIOS { } // Tap taps on the target element -func (s *StepIOS) Tap(params string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) Tap(params string, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiTap, Params: params, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } // Tap taps on the target element by OCR recognition -func (s *StepIOS) TapByOCR(ocrText string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) TapByOCR(ocrText string, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiTapByOCR, Params: ocrText, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } // Tap taps on the target element by CV recognition -func (s *StepIOS) TapByCV(imagePath string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) TapByCV(imagePath string, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiTapByCV, Params: imagePath, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -122,11 +134,15 @@ func (s *StepIOS) DoubleTapXY(x, y float64) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) DoubleTap(params string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) DoubleTap(params string, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiDoubleTap, Params: params, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -529,17 +545,17 @@ func (ud *uiDriver) doAction(action MobileAction) error { return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) case uiTap: if param, ok := action.Params.(string); ok { - return ud.Tap(param) + return ud.Tap(param, action.ignoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) case uiTapByOCR: if ocrText, ok := action.Params.(string); ok { - return ud.TapByOCR(ocrText) + return ud.TapByOCR(ocrText, action.ignoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByOCR, action.Params) case uiTapByCV: if imagePath, ok := action.Params.(string); ok { - return ud.TapByCV(imagePath) + return ud.TapByCV(imagePath, action.ignoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByCV, action.Params) case uiDoubleTapXY: diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index c9e8732c..9f867a22 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -85,7 +85,7 @@ func TestIOSWeixinLive(t *testing.T) { NewStep("进入直播页"). IOS(). Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 - TapXY(0.5, 0.3). // 基于坐标位置点击「直播」;TODO:通过 OCR 识别「直播」 + TapByOCR("直播"). // 通过 OCR 识别「直播」 Validate(). AssertLabelExists("直播"), NewStep("向上滑动 5 次"). From c18050266672f82d4e90d33538f7c4184cbfbf20 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 18:17:53 +0800 Subject: [PATCH 041/169] change: InitWDAClient with options --- hrp/internal/uixt/drag_test.go | 7 +------ hrp/internal/uixt/gesture_test.go | 7 +------ hrp/internal/uixt/init.go | 12 +----------- hrp/internal/uixt/ocr_test.go | 9 ++------- hrp/internal/uixt/tap_test.go | 17 +++-------------- hrp/internal/uixt/touch_test.go | 12 ++---------- hrp/step_ios_ui.go | 7 ++++++- 7 files changed, 16 insertions(+), 55 deletions(-) diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go index 018d4fd7..09b554b1 100644 --- a/hrp/internal/uixt/drag_test.go +++ b/hrp/internal/uixt/drag_test.go @@ -2,15 +2,10 @@ package uixt import ( "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExt_Drag(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" diff --git a/hrp/internal/uixt/gesture_test.go b/hrp/internal/uixt/gesture_test.go index 8308d132..075023bd 100644 --- a/hrp/internal/uixt/gesture_test.go +++ b/hrp/internal/uixt/gesture_test.go @@ -4,8 +4,6 @@ import ( "strconv" "strings" "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExt_GesturePassword(t *testing.T) { @@ -15,10 +13,7 @@ func TestDriverExt_GesturePassword(t *testing.T) { password[i], _ = strconv.Atoi(split[i]) } - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/init.go index 64c0c011..5acd9bb7 100644 --- a/hrp/internal/uixt/init.go +++ b/hrp/internal/uixt/init.go @@ -21,18 +21,8 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) -func InitWDAClient(udid string, port, mjpeg_port int) (*DriverExt, error) { +func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { // init wda device - var options []gwda.DeviceOption - if udid != "" { - options = append(options, gwda.WithSerialNumber(udid)) - } - if port != 0 { - options = append(options, gwda.WithPort(port)) - } - if mjpeg_port != 0 { - options = append(options, gwda.WithMjpegPort(mjpeg_port)) - } targetDevice, err := gwda.NewDevice(options...) if err != nil { return nil, err diff --git a/hrp/internal/uixt/ocr_test.go b/hrp/internal/uixt/ocr_test.go index 53e1dee3..c334a1dd 100644 --- a/hrp/internal/uixt/ocr_test.go +++ b/hrp/internal/uixt/ocr_test.go @@ -2,20 +2,15 @@ package uixt import ( "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExtOCR(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) x, y, width, height, err := driverExt.FindTextByOCR("抖音") checkErr(t, err) t.Logf("x: %v, y: %v, width: %v, height: %v", x, y, width, height) - driver.TapFloat(x, y-20) + driverExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5-20) } diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 174e86d6..a25f8946 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -2,15 +2,10 @@ package uixt import ( "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExt_TapWithNumber(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -25,10 +20,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) { } func TestDriverExt_TapXY(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) err = driverExt.TapXY(0.4, 0.5) @@ -36,10 +28,7 @@ func TestDriverExt_TapXY(t *testing.T) { } func TestDriverExt_TapWithOCR(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) // 需要点击文字上方的图标 diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index 514e2fae..a814b12b 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -2,15 +2,10 @@ package uixt import ( "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExt_ForceTouch(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" @@ -26,10 +21,7 @@ func TestDriverExt_ForceTouch(t *testing.T) { } func TestDriverExt_TouchAndHold(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index cc6bacb3..23bcf153 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -427,7 +428,11 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error } } - driverExt, err := uixt.InitWDAClient(device.UDID, device.Port, device.MjpegPort) + driverExt, err := uixt.InitWDAClient( + gwda.WithSerialNumber(device.UDID), + gwda.WithPort(device.Port), + gwda.WithMjpegPort(device.MjpegPort), + ) if err != nil { return nil, err } From 6d418ff7956009a986a2bfe813193e94fdc2030c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 20:15:29 +0800 Subject: [PATCH 042/169] feat: SwipeUntil --- hrp/internal/uixt/ocr.go | 5 +++- hrp/internal/uixt/swipe.go | 22 ++++++++++++++++ hrp/internal/uixt/swipe_test.go | 46 ++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/hrp/internal/uixt/ocr.go b/hrp/internal/uixt/ocr.go index dc267111..fbc77b96 100644 --- a/hrp/internal/uixt/ocr.go +++ b/hrp/internal/uixt/ocr.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "strings" "time" ) @@ -92,10 +93,12 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rec } for _, ocrResult := range ocrResults { - if ocrResult.Text != text { + // not contains text + if !strings.Contains(ocrResult.Text, text) { continue } + // contains text // only find the first matched one rect = image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 44b9d36c..93b0fccd 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -1,5 +1,11 @@ package uixt +import ( + "fmt" + + "github.com/rs/zerolog/log" +) + func (dExt *DriverExt) SwipeTo(direction string) (err error) { width := dExt.windowSize.Width height := dExt.windowSize.Height @@ -18,6 +24,22 @@ func (dExt *DriverExt) SwipeTo(direction string) (err error) { return dExt.WebDriver.Swipe(fromX, fromY, toX, toY) } +type Condition func(driver *DriverExt) error + +func (dExt *DriverExt) SwipeUntil(direction string, condition Condition, maxTimes int) error { + for i := 0; i < maxTimes; i++ { + err := condition(dExt) + if err == nil { + return nil + } + err = dExt.SwipeTo(direction) + if err != nil { + log.Error().Err(err).Msgf("swipe %s failed", direction) + } + } + return fmt.Errorf("swipe %s %d times, run condition failed", direction, maxTimes) +} + func (dExt *DriverExt) Swipe(pathname string, toX, toY int) (err error) { return dExt.SwipeFloat(pathname, float64(toX), float64(toY)) } diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index f8e0650c..fb570dc1 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -2,15 +2,10 @@ package uixt import ( "testing" - - "github.com/electricbubble/gwda" ) func TestDriverExt_Swipe(t *testing.T) { - driver, err := gwda.NewUSBDriver(nil) - checkErr(t, err) - - driverExt, err := Extend(driver, 0.95) + driverExt, err := InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -31,3 +26,42 @@ func TestDriverExt_Swipe(t *testing.T) { err = driverExt.OnlyOnceThreshold(0.92).SwipeOffsetFloat(pathSearch, 300.9, 499.1, 0.2, 0.5) checkErr(t, err) } + +func TestSwipeUntil(t *testing.T) { + driverExt, err := InitWDAClient() + checkErr(t, err) + + var x, y, width, height float64 + findApp := func(d *DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR("抖音") + return err + } + + driverExt.Homescreen() + + // swipe to first screen + for i := 0; i < 5; i++ { + driverExt.SwipeTo("right") + } + + // swipe until app found + err = driverExt.SwipeUntil("left", findApp, 10) + checkErr(t, err) + + // click app, launch douyin + driverExt.TapFloat(x+width*0.5, y+height*0.5-20) + + findLive := func(d *DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR("点击进入直播间") + return err + } + + // swipe until live room found + err = driverExt.SwipeUntil("up", findLive, 20) + checkErr(t, err) + + // enter live room + driverExt.TapFloat(x+width*0.5, y+height*0.5) +} From 9b547524d2e214b1392ff9a0daef1c23a9abfdf1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 20:36:34 +0800 Subject: [PATCH 043/169] feat: add FoundAction to SwipeUntil --- hrp/internal/uixt/swipe.go | 19 +++++++++++-------- hrp/internal/uixt/swipe_test.go | 18 ++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 93b0fccd..afae7550 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -24,20 +24,23 @@ func (dExt *DriverExt) SwipeTo(direction string) (err error) { return dExt.WebDriver.Swipe(fromX, fromY, toX, toY) } -type Condition func(driver *DriverExt) error +// FindCondition indicates the condition to find a UI element +type FindCondition func(driver *DriverExt) error -func (dExt *DriverExt) SwipeUntil(direction string, condition Condition, maxTimes int) error { +// FoundAction indicates the action to do after a UI element is found +type FoundAction func(driver *DriverExt) error + +func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, action FoundAction, maxTimes int) error { for i := 0; i < maxTimes; i++ { - err := condition(dExt) - if err == nil { - return nil + if err := condition(dExt); err == nil { + // do action after found + return action(dExt) } - err = dExt.SwipeTo(direction) - if err != nil { + if err := dExt.SwipeTo(direction); err != nil { log.Error().Err(err).Msgf("swipe %s failed", direction) } } - return fmt.Errorf("swipe %s %d times, run condition failed", direction, maxTimes) + return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } func (dExt *DriverExt) Swipe(pathname string, toX, toY int) (err error) { diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index fb570dc1..77d5d39e 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -37,6 +37,10 @@ func TestSwipeUntil(t *testing.T) { x, y, width, height, err = d.FindTextByOCR("抖音") return err } + foundAppAction := func(d *DriverExt) error { + // click app, launch douyin + return d.TapFloat(x+width*0.5, y+height*0.5-20) + } driverExt.Homescreen() @@ -46,22 +50,20 @@ func TestSwipeUntil(t *testing.T) { } // swipe until app found - err = driverExt.SwipeUntil("left", findApp, 10) + err = driverExt.SwipeUntil("left", findApp, foundAppAction, 10) checkErr(t, err) - // click app, launch douyin - driverExt.TapFloat(x+width*0.5, y+height*0.5-20) - findLive := func(d *DriverExt) error { var err error x, y, width, height, err = d.FindTextByOCR("点击进入直播间") return err } + foundLiveAction := func(d *DriverExt) error { + // enter live room + return d.TapFloat(x+width*0.5, y+height*0.5) + } // swipe until live room found - err = driverExt.SwipeUntil("up", findLive, 20) + err = driverExt.SwipeUntil("up", findLive, foundLiveAction, 20) checkErr(t, err) - - // enter live room - driverExt.TapFloat(x+width*0.5, y+height*0.5) } From 70aa8fa937561350bac762b62278dfaf60d43cd2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 21:21:55 +0800 Subject: [PATCH 044/169] feat: SwipeToTapApp, SwipeToTapText --- examples/uitest/demo_weixin_test.go | 45 ++++++++++++++++ hrp/step.go | 11 ++++ hrp/step_ios_ui.go | 84 ++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 examples/uitest/demo_weixin_test.go diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go new file mode 100644 index 00000000..0dce08a4 --- /dev/null +++ b/examples/uitest/demo_weixin_test.go @@ -0,0 +1,45 @@ +package uitest + +import ( + "fmt" + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSWeixinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动微信"). + IOS(). + Home(). + AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 + SwipeToTapApp("微信", hrp.WithMaxRetryTimes(5)). + Validate(). + AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), + hrp.NewStep("进入直播页"). + IOS(). + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号"). // 通过 OCR 识别「视频号」 + Validate(). + AssertLabelExists("视频号"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(false)), + hrp.NewStep("在推荐页上划,直到出现「轻触进入直播间」"). + IOS(). + SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10)), + hrp.NewStep("向上滑动,等待 60s"). + IOS(). + SwipeUp().Sleep(60).ScreenShot(). // 上划 1 次,等待 60s,截图保存 + SwipeUp().Times(60).ScreenShot(), // 再上划 1 次,等待 60s,截图保存 + }, + } + fmt.Println(testCase) + + err := hrp.NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/step.go b/hrp/step.go index 8e4715b7..34cdb879 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -49,18 +49,29 @@ const ( uiSelectorImage string = "ui_image" assertionExists string = "exists" assertionNotExists string = "not_exists" + + // custom actions + swipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap + swipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap ) type MobileAction struct { Method MobileMethod `json:"method" yaml:"method"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` + maxRetryTimes int // max retry times timeout int // TODO: wait timeout in seconds for mobile action ignoreNotFoundError bool // ignore error if target element not found } type ActionOption func(o *MobileAction) +func WithMaxRetryTimes(maxRetryTimes int) ActionOption { + return func(o *MobileAction) { + o.maxRetryTimes = maxRetryTimes + } +} + func WithTimeout(timeout int) ActionOption { return func(o *MobileAction) { o.timeout = timeout diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 23bcf153..6b83ceb4 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -187,6 +187,38 @@ func (s *StepIOS) SwipeRight() *StepIOS { return &StepIOS{step: s.step} } +func (s *StepIOS) SwipeToTapApp(appName string, options ...ActionOption) *StepIOS { + action := MobileAction{ + Method: swipeToTapApp, + Params: appName, + } + for _, option := range options { + option(&action) + } + // default to retry 5 times + if action.maxRetryTimes == 0 { + action.maxRetryTimes = 5 + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeToTapText(text string, options ...ActionOption) *StepIOS { + action := MobileAction{ + Method: swipeToTapText, + Params: text, + } + for _, option := range options { + option(&action) + } + // default to retry 10 times + if action.maxRetryTimes == 0 { + action.maxRetryTimes = 10 + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + func (s *StepIOS) Input(text string) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ Method: uiInput, @@ -519,12 +551,60 @@ func (ud *uiDriver) doAction(action MobileAction) error { if bundleId, ok := action.Params.(string); ok { return ud.AppLaunch(bundleId) } - return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params) + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + appLaunch, action.Params) case appLaunchUnattached: if bundleId, ok := action.Params.(string); ok { return ud.AppLaunchUnattached(bundleId) } - return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params) + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + appLaunchUnattached, action.Params) + case swipeToTapApp: + if appName, ok := action.Params.(string); ok { + var x, y, width, height float64 + findApp := func(d *uixt.DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(appName) + return err + } + foundAppAction := func(d *uixt.DriverExt) error { + // click app to launch + return d.TapFloat(x+width*0.5, y+height*0.5-20) + } + + // go to home screen + if err := ud.WebDriver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + ud.SwipeTo("right") + } + + // swipe next screen until app found + return ud.SwipeUntil("left", findApp, foundAppAction, action.maxRetryTimes) + } + return fmt.Errorf("invalid %s params, should be app name(string), got %v", + swipeToTapApp, action.Params) + case swipeToTapText: + if text, ok := action.Params.(string); ok { + var x, y, width, height float64 + findText := func(d *uixt.DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(text) + return err + } + foundTextAction := func(d *uixt.DriverExt) error { + // tap text + return d.TapFloat(x+width*0.5, y+height*0.5) + } + + // swipe until live room found + return ud.SwipeUntil("up", findText, foundTextAction, 20) + } + return fmt.Errorf("invalid %s params, should be app text(string), got %v", + swipeToTapText, action.Params) case appTerminate: if bundleId, ok := action.Params.(string); ok { success, err := ud.AppTerminate(bundleId) From cf5761836a896b15d916a1af368d4107a3f68a76 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 22:31:45 +0800 Subject: [PATCH 045/169] fix: get ocr exact text in first priority --- examples/uitest/demo_weixin_test.go | 12 +++++------- hrp/internal/uixt/ocr.go | 18 +++++++++++++++--- hrp/step_ios_ui.go | 17 ++++++++++++----- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 0dce08a4..26611ae7 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -21,19 +21,17 @@ func TestIOSWeixinLive(t *testing.T) { hrp.NewStep("进入直播页"). IOS(). Tap("发现"). // 进入「发现页」 - TapByOCR("视频号"). // 通过 OCR 识别「视频号」 - Validate(). - AssertLabelExists("视频号"), + TapByOCR("视频号"), // 通过 OCR 识别「视频号」 hrp.NewStep("处理青少年弹窗"). IOS(). - TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(false)), + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), hrp.NewStep("在推荐页上划,直到出现「轻触进入直播间」"). IOS(). SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10)), - hrp.NewStep("向上滑动,等待 60s"). + hrp.NewStep("向上滑动,等待 10s"). IOS(). - SwipeUp().Sleep(60).ScreenShot(). // 上划 1 次,等待 60s,截图保存 - SwipeUp().Times(60).ScreenShot(), // 再上划 1 次,等待 60s,截图保存 + SwipeUp().Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp().Times(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 }, } fmt.Println(testCase) diff --git a/hrp/internal/uixt/ocr.go b/hrp/internal/uixt/ocr.go index fbc77b96..d261e3de 100644 --- a/hrp/internal/uixt/ocr.go +++ b/hrp/internal/uixt/ocr.go @@ -92,14 +92,13 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rec return } + var rects []image.Rectangle for _, ocrResult := range ocrResults { // not contains text if !strings.Contains(ocrResult.Text, text) { continue } - // contains text - // only find the first matched one rect = image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 Min: image.Point{ @@ -111,7 +110,20 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rec Y: int(ocrResult.Points[2].Y), }, } - return + + // contains text while not match exactly + if ocrResult.Text != text { + rects = append(rects, rect) + continue + } + + // match exactly + return rect, nil + } + + // only find the first matched one + if len(rects) > 0 { + return rects[0], nil } return image.Rectangle{}, fmt.Errorf("text %s not found", text) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 6b83ceb4..5f1126ed 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -460,11 +460,18 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error } } - driverExt, err := uixt.InitWDAClient( - gwda.WithSerialNumber(device.UDID), - gwda.WithPort(device.Port), - gwda.WithMjpegPort(device.MjpegPort), - ) + var deviceOptions []gwda.DeviceOption + if device.UDID != "" { + deviceOptions = append(deviceOptions, gwda.WithSerialNumber(device.UDID)) + } + if device.Port != 0 { + deviceOptions = append(deviceOptions, gwda.WithPort(device.Port)) + } + if device.MjpegPort != 0 { + deviceOptions = append(deviceOptions, gwda.WithMjpegPort(device.MjpegPort)) + } + + driverExt, err := uixt.InitWDAClient(deviceOptions...) if err != nil { return nil, err } From 5fba76e6e302e5b590b3f0f8ace044edf318aee7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 23:16:46 +0800 Subject: [PATCH 046/169] change: convert StepResult Attachments to interface, for screenshots saving --- examples/uitest/demo_weixin_test.go | 11 +++++--- hrp/boomer.go | 11 ++++---- hrp/step.go | 2 +- hrp/step_ios_ui.go | 43 +++++++++++++++++++++++------ hrp/step_request.go | 2 +- hrp/step_testcase.go | 2 +- hrp/step_websocket.go | 2 +- hrp/summary_test.go | 2 +- 8 files changed, 53 insertions(+), 22 deletions(-) diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 26611ae7..70d2180a 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -31,13 +31,16 @@ func TestIOSWeixinLive(t *testing.T) { hrp.NewStep("向上滑动,等待 10s"). IOS(). SwipeUp().Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 - SwipeUp().Times(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + SwipeUp().Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 }, } fmt.Println(testCase) - err := hrp.NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) + runner := hrp.NewRunner(t) + sessionRunner, _ := runner.NewSessionRunner(testCase) + if err := sessionRunner.Start(nil); err != nil { + t.Fatal() } + summary := sessionRunner.GetSummary() + fmt.Println(summary) } diff --git a/hrp/boomer.go b/hrp/boomer.go index e8ea55fc..c7d56bb6 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -10,12 +10,13 @@ import ( "time" "github.com/httprunner/funplugin" + "github.com/rs/zerolog/log" + "golang.org/x/net/context" + "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" - "github.com/rs/zerolog/log" - "golang.org/x/net/context" ) func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *HRPBoomer { @@ -252,7 +253,6 @@ func (b *HRPBoomer) rebalanceRunner(profile *boomer.Profile) { log.Info().Interface("profile", profile).Msg("rebalance tasks successfully") } - func (b *HRPBoomer) PollTasks(ctx context.Context) { for { select { @@ -261,7 +261,7 @@ func (b *HRPBoomer) PollTasks(ctx context.Context) { if len(b.Boomer.GetTasksChan()) > 0 { continue } - //Todo: 过滤掉已经传输过的task + // Todo: 过滤掉已经传输过的task if task.TestCasesBytes != nil { // init boomer with profile b.initWorker(task.Profile) @@ -382,7 +382,8 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend if result.Success { b.RecordSuccess(string(result.StepType), result.Name, result.Elapsed, result.ContentSize) } else { - b.RecordFailure(string(result.StepType), result.Name, result.Elapsed, result.Attachment) + exception, _ := result.Attachments.(string) + b.RecordFailure(string(result.StepType), result.Name, result.Elapsed, exception) } } } diff --git a/hrp/step.go b/hrp/step.go index 34cdb879..8636d0c3 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -93,7 +93,7 @@ type StepResult struct { Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables - Attachment string `json:"attachment,omitempty" yaml:"attachment,omitempty"` // step error information + Attachments interface{} `json:"attachments,omitempty" yaml:"attachments,omitempty"` // store extra step information, such as error message or screenshots } // TStep represents teststep data structure. diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5f1126ed..ea6a0092 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -495,6 +495,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro Success: false, ContentSize: 0, } + screenshots := make([]string, 0) // init wdaClient driver wdaClient, err := s.hrpRunner.InitWDAClient(step.IOS.WDADevice) @@ -502,6 +503,29 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro return } + defer func() { + attachments := make(map[string]interface{}) + if err != nil { + attachments["error"] = err.Error() + } + + // save attachments + screenshots = append(screenshots, wdaClient.screenShots...) + attachments["screenshots"] = screenshots + stepResult.Attachments = attachments + + // update summary + s.summary.Records = append(s.summary.Records, stepResult) + s.summary.Stat.Total += 1 + if stepResult.Success { + s.summary.Stat.Successes += 1 + } else { + s.summary.Stat.Failures += 1 + // update summary result to failed + s.summary.Success = false + } + }() + // prepare actions var actions []MobileAction if step.IOS.Actions == nil { @@ -523,11 +547,14 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // take snapshot - screenshotPath, err := wdaClient.DriverExt.ScreenShot(fmt.Sprintf("validate_%s", step.Name)) + screenshotPath, err := wdaClient.DriverExt.ScreenShot( + fmt.Sprintf("validate_%d", time.Now().Unix())) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") + } else { + log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") + screenshots = append(screenshots, screenshotPath) } - log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") // validate validateResults, err := wdaClient.doValidation(step.Validators) @@ -545,6 +572,8 @@ var errActionNotImplemented = errors.New("UI action not implemented") type uiDriver struct { uixt.DriverExt + + screenShots []string // save screenshots path } func (ud *uiDriver) doAction(action MobileAction) error { @@ -684,14 +713,12 @@ func (ud *uiDriver) doAction(action MobileAction) error { case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") - var screenshotPath string - var err error - if param, ok := action.Params.(string); ok { - screenshotPath, err = ud.ScreenShot(fmt.Sprintf("screenshot_%s", param)) - } else { - screenshotPath, err = ud.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) + screenshotPath, err := ud.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) + if err != nil { + return errors.Wrap(err, "take screenshot failed") } log.Info().Str("path", screenshotPath).Msg("take screenshot") + ud.screenShots = append(ud.screenShots, screenshotPath) return err case ctlStartCamera: // start camera, alias for app_launch com.apple.camera diff --git a/hrp/step_request.go b/hrp/step_request.go index 48a70e4d..47a9a242 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -295,7 +295,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err defer func() { // update testcase summary if err != nil { - stepResult.Attachment = err.Error() + stepResult.Attachments = err.Error() } // update summary r.summary.Records = append(r.summary.Records, stepResult) diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index 09440e5a..0bcc1ae3 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -54,7 +54,7 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe defer func() { // update testcase summary if err != nil { - stepResult.Attachment = err.Error() + stepResult.Attachments = err.Error() } }() diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 90feec2d..5da949f6 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -258,7 +258,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er defer func() { // update testcase summary if err != nil { - stepResult.Attachment = err.Error() + stepResult.Attachments = err.Error() } // update summary r.summary.Records = append(r.summary.Records, stepResult) diff --git a/hrp/summary_test.go b/hrp/summary_test.go index 32343e0e..034ded16 100644 --- a/hrp/summary_test.go +++ b/hrp/summary_test.go @@ -12,7 +12,7 @@ func TestGenHTMLReport(t *testing.T) { StepType: stepTypeRequest, Success: false, ContentSize: 0, - Attachment: "err", + Attachments: "err", } caseSummary1.Records = []*StepResult{stepResult1, stepResult2, nil} summary.appendCaseSummary(caseSummary1) From 94727e8a34eb89f3ff7912dce31dcb0ed9c58744 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 1 Sep 2022 21:06:49 +0800 Subject: [PATCH 047/169] bump version to v4.3.0 --- hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 15a2b33b..1ddc0f60 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.2.0 \ No newline at end of file +v4.3.0 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index fe4e9b1c..416ff278 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.2.0" +__version__ = "v4.3.0" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 18786b00..33947c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.2.0" +version = "v4.3.0" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 843b72100f596adaf136d10885d3a4fdf232282b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 1 Sep 2022 21:10:55 +0800 Subject: [PATCH 048/169] change: use startTime to associate screenshots name --- hrp/step_ios_ui.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index ea6a0092..6cce8217 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -502,6 +502,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro if err != nil { return } + wdaClient.startTime = s.startTime defer func() { attachments := make(map[string]interface{}) @@ -548,7 +549,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro // take snapshot screenshotPath, err := wdaClient.DriverExt.ScreenShot( - fmt.Sprintf("validate_%d", time.Now().Unix())) + fmt.Sprintf("%d_validate_%d", wdaClient.startTime.Unix(), time.Now().Unix())) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } else { @@ -573,7 +574,8 @@ var errActionNotImplemented = errors.New("UI action not implemented") type uiDriver struct { uixt.DriverExt - screenShots []string // save screenshots path + startTime time.Time // used to associate screenshots name + screenShots []string // save screenshots path } func (ud *uiDriver) doAction(action MobileAction) error { @@ -713,7 +715,8 @@ func (ud *uiDriver) doAction(action MobileAction) error { case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") - screenshotPath, err := ud.ScreenShot(fmt.Sprintf("screenshot_%d", time.Now().Unix())) + screenshotPath, err := ud.ScreenShot(fmt.Sprintf("%d_screenshot_%d", + ud.startTime.Unix(), time.Now().Unix())) if err != nil { return errors.Wrap(err, "take screenshot failed") } From 47ac7dba0bf619dd923a3f74fa8166e009632013 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 1 Sep 2022 23:27:14 +0800 Subject: [PATCH 049/169] feat: dump TestCase to json/yaml file --- examples/uitest/demo_weixin_test.go | 8 +++++++- hrp/testcase.go | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 70d2180a..cd5cbda3 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -35,11 +35,17 @@ func TestIOSWeixinLive(t *testing.T) { }, } fmt.Println(testCase) + if err := testCase.Dump2JSON("demo_weixin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_weixin_live.yaml"); err != nil { + t.Fatal(err) + } runner := hrp.NewRunner(t) sessionRunner, _ := runner.NewSessionRunner(testCase) if err := sessionRunner.Start(nil); err != nil { - t.Fatal() + t.Fatal(err) } summary := sessionRunner.GetSummary() fmt.Println(summary) diff --git a/hrp/testcase.go b/hrp/testcase.go index e1cd4533..4bf3315b 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -7,11 +7,11 @@ import ( "path/filepath" "strings" + "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/mitchellh/mapstructure" ) // ITestCase represents interface for testcases, @@ -51,6 +51,24 @@ func (tc *TestCase) ToTCase() *TCase { return tCase } +func (tc *TestCase) Dump2JSON(targetPath string) error { + tCase := tc.ToTCase() + err := builtin.Dump2JSON(tCase, targetPath) + if err != nil { + return errors.Wrap(err, "dump testcase to json failed") + } + return nil +} + +func (tc *TestCase) Dump2YAML(targetPath string) error { + tCase := tc.ToTCase() + err := builtin.Dump2YAML(tCase, targetPath) + if err != nil { + return errors.Wrap(err, "dump testcase to yaml failed") + } + return nil +} + // TestCasePath implements ITestCase interface. type TestCasePath string From 4406083d443f606288d5d5de408fe7823d3fe8e0 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 1 Sep 2022 23:54:27 +0800 Subject: [PATCH 050/169] fix: hrp run ios UI tests --- examples/uitest/demo_weixin_live.json | 102 ++++++++++++++++++++++++++ examples/uitest/demo_weixin_test.go | 2 +- hrp/step.go | 14 ++-- hrp/step_ios_ui.go | 26 +++---- hrp/testcase.go | 8 ++ 5 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 examples/uitest/demo_weixin_live.json diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json new file mode 100644 index 00000000..a789f0ee --- /dev/null +++ b/examples/uitest/demo_weixin_live.json @@ -0,0 +1,102 @@ +{ + "config": { + "name": "通过 feed 卡片进入微信直播间" + }, + "teststeps": [ + { + "name": "启动微信", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.tencent.xin" + }, + { + "method": "swipe_to_tap_app", + "params": "微信", + "max_retry_times": 5 + } + ] + }, + "validate": [ + { + "check": "ui_label", + "assert": "exists", + "expect": "通讯录", + "msg": "微信启动失败,「通讯录」不存在" + } + ] + }, + { + "name": "进入直播页", + "ios": { + "actions": [ + { + "method": "tap", + "params": "发现" + }, + { + "method": "tap_ocr", + "params": "视频号" + } + ] + } + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "在推荐页上划,直到出现「轻触进入直播间」", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": "轻触进入直播间", + "max_retry_times": 10 + } + ] + } + }, + { + "name": "向上滑动,等待 10s", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": "up" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index cd5cbda3..05200abe 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -34,7 +34,7 @@ func TestIOSWeixinLive(t *testing.T) { SwipeUp().Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 }, } - fmt.Println(testCase) + if err := testCase.Dump2JSON("demo_weixin_live.json"); err != nil { t.Fatal(err) } diff --git a/hrp/step.go b/hrp/step.go index 8636d0c3..2a219bb7 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -56,31 +56,31 @@ const ( ) type MobileAction struct { - Method MobileMethod `json:"method" yaml:"method"` + Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` - maxRetryTimes int // max retry times - timeout int // TODO: wait timeout in seconds for mobile action - ignoreNotFoundError bool // ignore error if target element not found + MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action + IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found } type ActionOption func(o *MobileAction) func WithMaxRetryTimes(maxRetryTimes int) ActionOption { return func(o *MobileAction) { - o.maxRetryTimes = maxRetryTimes + o.MaxRetryTimes = maxRetryTimes } } func WithTimeout(timeout int) ActionOption { return func(o *MobileAction) { - o.timeout = timeout + o.Timeout = timeout } } func WithIgnoreNotFoundError(ignoreError bool) ActionOption { return func(o *MobileAction) { - o.ignoreNotFoundError = ignoreError + o.IgnoreNotFoundError = ignoreError } } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 6cce8217..d8cf5382 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -195,10 +195,6 @@ func (s *StepIOS) SwipeToTapApp(appName string, options ...ActionOption) *StepIO for _, option := range options { option(&action) } - // default to retry 5 times - if action.maxRetryTimes == 0 { - action.maxRetryTimes = 5 - } s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -211,10 +207,6 @@ func (s *StepIOS) SwipeToTapText(text string, options ...ActionOption) *StepIOS for _, option := range options { option(&action) } - // default to retry 10 times - if action.maxRetryTimes == 0 { - action.maxRetryTimes = 10 - } s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -620,8 +612,12 @@ func (ud *uiDriver) doAction(action MobileAction) error { ud.SwipeTo("right") } + // default to retry 5 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 5 + } // swipe next screen until app found - return ud.SwipeUntil("left", findApp, foundAppAction, action.maxRetryTimes) + return ud.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes) } return fmt.Errorf("invalid %s params, should be app name(string), got %v", swipeToTapApp, action.Params) @@ -638,8 +634,12 @@ func (ud *uiDriver) doAction(action MobileAction) error { return d.TapFloat(x+width*0.5, y+height*0.5) } + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } // swipe until live room found - return ud.SwipeUntil("up", findText, foundTextAction, 20) + return ud.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) } return fmt.Errorf("invalid %s params, should be app text(string), got %v", swipeToTapText, action.Params) @@ -668,17 +668,17 @@ func (ud *uiDriver) doAction(action MobileAction) error { return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) case uiTap: if param, ok := action.Params.(string); ok { - return ud.Tap(param, action.ignoreNotFoundError) + return ud.Tap(param, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) case uiTapByOCR: if ocrText, ok := action.Params.(string); ok { - return ud.TapByOCR(ocrText, action.ignoreNotFoundError) + return ud.TapByOCR(ocrText, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByOCR, action.Params) case uiTapByCV: if imagePath, ok := action.Params.(string); ok { - return ud.TapByCV(imagePath, action.ignoreNotFoundError) + return ud.TapByCV(imagePath, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByCV, action.Params) case uiDoubleTapXY: diff --git a/hrp/testcase.go b/hrp/testcase.go index 4bf3315b..6f4bd46b 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -260,6 +260,14 @@ func (tc *TCase) toTestCase() (*TestCase, error) { testCase.TestSteps = append(testCase.TestSteps, &StepWebSocket{ step: step, }) + } else if step.IOS != nil { + testCase.TestSteps = append(testCase.TestSteps, &StepIOS{ + step: step, + }) + } else if step.Android != nil { + testCase.TestSteps = append(testCase.TestSteps, &StepAndroid{ + step: step, + }) } else { log.Warn().Interface("step", step).Msg("[convertTestCase] unexpected step") } From abbd3fb20536fdaf6d607f9c0ee2889a0704fc98 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 2 Sep 2022 08:54:09 +0800 Subject: [PATCH 051/169] fix: marshal yaml with inline tag --- examples/uitest/demo_weixin_live.yaml | 49 +++++++++++++++++++++++++++ hrp/step_ios_ui.go | 6 ++-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 examples/uitest/demo_weixin_live.yaml diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml new file mode 100644 index 00000000..d5f3d795 --- /dev/null +++ b/examples/uitest/demo_weixin_live.yaml @@ -0,0 +1,49 @@ +config: + name: 通过 feed 卡片进入微信直播间 +teststeps: + - name: 启动微信 + ios: + actions: + - method: home + - method: app_terminate + params: com.tencent.xin + - method: swipe_to_tap_app + params: 微信 + max_retry_times: 5 + validate: + - check: ui_label + assert: exists + expect: 通讯录 + msg: 微信启动失败,「通讯录」不存在 + - name: 进入直播页 + ios: + actions: + - method: tap + params: 发现 + - method: tap_ocr + params: 视频号 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true + - name: 在推荐页上划,直到出现「轻触进入直播间」 + ios: + actions: + - method: swipe_to_tap_text + params: 轻触进入直播间 + max_retry_times: 10 + - name: 向上滑动,等待 10s + ios: + actions: + - method: swipe + params: up + - method: sleep + params: 10 + - method: screenshot + - method: swipe + params: up + - method: sleep + params: 10 + - method: screenshot diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index d8cf5382..ca5013e9 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -23,9 +23,9 @@ type WDADevice struct { } type IOSStep struct { - WDADevice - MobileAction - Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` + WDADevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + MobileAction `yaml:",inline"` + Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } // StepIOS implements IStep interface. From 84f9d2d532335b94066c6bd4f4da9acc31ffd227 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 18 Sep 2022 10:42:31 +0800 Subject: [PATCH 052/169] fix: sleep specified seconds --- hrp/internal/uixt/ext.go | 2 ++ hrp/step_ios_ui.go | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 5855491e..82d9c394 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -17,6 +17,7 @@ import ( "github.com/electricbubble/gwda" cvHelper "github.com/electricbubble/opencv-helper" "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) // TemplateMatchMode is the type of the template matching operation. @@ -170,6 +171,7 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { return dExt.frame, nil } if raw, err = dExt.WebDriver.Screenshot(); err != nil { + log.Error().Err(err).Msgf("screenshot failed: %v", err) return nil, err } return diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index ca5013e9..312da3d7 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -1,6 +1,7 @@ package hrp import ( + "encoding/json" "fmt" "strings" "time" @@ -241,7 +242,7 @@ func (s *StepIOS) Times(n int) *StepIOS { } // Sleep specify sleep seconds after last action -func (s *StepIOS) Sleep(n int) *StepIOS { +func (s *StepIOS) Sleep(n float64) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ Method: ctlSleep, Params: n, @@ -707,11 +708,15 @@ func (ud *uiDriver) doAction(action MobileAction) error { param := fmt.Sprintf("%v", action.Params) return ud.SendKeys(param) case ctlSleep: - if param, ok := action.Params.(int); ok { - time.Sleep(time.Duration(param) * time.Second) + if param, ok := action.Params.(json.Number); ok { + seconds, _ := param.Float64() + time.Sleep(time.Duration(seconds*1000) * time.Millisecond) + return nil + } else if param, ok := action.Params.(float64); ok { + time.Sleep(time.Duration(param*1000) * time.Millisecond) return nil } - return fmt.Errorf("invalid sleep params: %v", action.Params) + return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params) case ctlScreenShot: // take snapshot log.Info().Msg("take snapshot for current screen") From c3c0ee2f60305803a4862ad4da23e7f366d368ce Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 19 Sep 2022 20:38:26 +0800 Subject: [PATCH 053/169] feat: tap with identifier for logging --- examples/uitest/demo_weixin_test.go | 8 ++-- hrp/internal/uixt/tap.go | 34 ++++++++++----- hrp/internal/uixt/tap_test.go | 4 +- hrp/step.go | 13 ++++-- hrp/step_ios_ui.go | 68 +++++++++++++++++++---------- 5 files changed, 86 insertions(+), 41 deletions(-) diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 05200abe..1031aefa 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -9,7 +9,7 @@ import ( func TestIOSWeixinLive(t *testing.T) { testCase := &hrp.TestCase{ - Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), // .SetIOS(hrp.WDADevice{Port: 8700, MjpegPort: 8800}) TestSteps: []hrp.IStep{ hrp.NewStep("启动微信"). IOS(). @@ -27,11 +27,11 @@ func TestIOSWeixinLive(t *testing.T) { TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), hrp.NewStep("在推荐页上划,直到出现「轻触进入直播间」"). IOS(). - SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10)), + SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), hrp.NewStep("向上滑动,等待 10s"). IOS(). - SwipeUp().Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 - SwipeUp().Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 }, } diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index ca8176c1..9f072481 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -6,7 +6,20 @@ import ( "github.com/electricbubble/gwda" ) -func (dExt *DriverExt) TapXY(x, y float64) error { +func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { + if len(identifier) > 0 { + option := gwda.DataOption{ + "log": map[string]interface{}{ + "enable": true, + "data": identifier, + }, + } + return dExt.WebDriver.TapFloat(x, y, option) + } + return dExt.WebDriver.TapFloat(x, y) +} + +func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { // tap on coordinate: [x, y] should be relative if x > 1 || y > 1 { return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) @@ -14,10 +27,11 @@ func (dExt *DriverExt) TapXY(x, y float64) error { x = x * float64(dExt.windowSize.Width) y = y * float64(dExt.windowSize.Height) - return dExt.WebDriver.TapFloat(x, y) + + return dExt.tapFloat(x, y, identifier) } -func (dExt *DriverExt) TapByOCR(ocrText string, ignoreNotFoundError bool) error { +func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool) error { x, y, width, height, err := dExt.FindTextByOCR(ocrText) if err != nil { if ignoreNotFoundError { @@ -26,10 +40,10 @@ func (dExt *DriverExt) TapByOCR(ocrText string, ignoreNotFoundError bool) error return err } - return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) + return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) } -func (dExt *DriverExt) TapByCV(imagePath string, ignoreNotFoundError bool) error { +func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool) error { x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath) if err != nil { if ignoreNotFoundError { @@ -38,14 +52,14 @@ func (dExt *DriverExt) TapByCV(imagePath string, ignoreNotFoundError bool) error return err } - return dExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5) + return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) } -func (dExt *DriverExt) Tap(param string, ignoreNotFoundError bool) error { - return dExt.TapOffset(param, 0.5, 0.5, ignoreNotFoundError) +func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool) error { + return dExt.TapOffset(param, 0.5, 0.5, identifier, ignoreNotFoundError) } -func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, ignoreNotFoundError bool) (err error) { +func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identifier string, ignoreNotFoundError bool) (err error) { // click on element, find by name attribute ele, err := dExt.FindUIElement(param) if err == nil { @@ -60,7 +74,7 @@ func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, ignoreN return err } - return dExt.WebDriver.TapFloat(x+width*xOffset, y+height*yOffset) + return dExt.tapFloat(x+width*xOffset, y+height*yOffset, identifier) } func (dExt *DriverExt) DoubleTapXY(x, y float64) error { diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index a25f8946..9c8c2e09 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -23,7 +23,7 @@ func TestDriverExt_TapXY(t *testing.T) { driverExt, err := InitWDAClient() checkErr(t, err) - err = driverExt.TapXY(0.4, 0.5) + err = driverExt.TapXY(0.4, 0.5, "") checkErr(t, err) } @@ -32,6 +32,6 @@ func TestDriverExt_TapWithOCR(t *testing.T) { checkErr(t, err) // 需要点击文字上方的图标 - err = driverExt.TapOffset("抖音", 0.5, -1, false) + err = driverExt.TapOffset("抖音", 0.5, -1, "", false) checkErr(t, err) } diff --git a/hrp/step.go b/hrp/step.go index 2a219bb7..02a43262 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -59,13 +59,20 @@ type MobileAction struct { Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` - MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times - Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action - IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log + MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action + IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found } type ActionOption func(o *MobileAction) +func WithIdentifier(identifier string) ActionOption { + return func(o *MobileAction) { + o.Identifier = identifier + } +} + func WithMaxRetryTimes(maxRetryTimes int) ActionOption { return func(o *MobileAction) { o.MaxRetryTimes = maxRetryTimes diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 312da3d7..a8018462 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -80,11 +80,15 @@ func (s *StepIOS) Home() *StepIOS { } // TapXY taps the point {X,Y}, X & Y is percentage of coordinates -func (s *StepIOS) TapXY(x, y float64) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) TapXY(x, y float64, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiTapXY, Params: []float64{x, y}, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -148,43 +152,63 @@ func (s *StepIOS) DoubleTap(params string, options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) Swipe(sx, sy, ex, ey int) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) Swipe(sx, sy, ex, ey int, options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiSwipe, Params: []int{sx, sy, ex, ey}, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeUp() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) SwipeUp(options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiSwipe, Params: "up", - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeDown() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) SwipeDown(options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiSwipe, Params: "down", - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeLeft() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) SwipeLeft(options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiSwipe, Params: "left", - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeRight() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ +func (s *StepIOS) SwipeRight(options ...ActionOption) *StepIOS { + action := MobileAction{ Method: uiSwipe, Params: "right", - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } @@ -664,22 +688,22 @@ func (ud *uiDriver) doAction(action MobileAction) error { if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return ud.TapXY(location[0], location[1]) + return ud.TapXY(location[0], location[1], action.Identifier) } return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) case uiTap: if param, ok := action.Params.(string); ok { - return ud.Tap(param, action.IgnoreNotFoundError) + return ud.Tap(param, action.Identifier, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) case uiTapByOCR: if ocrText, ok := action.Params.(string); ok { - return ud.TapByOCR(ocrText, action.IgnoreNotFoundError) + return ud.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByOCR, action.Params) case uiTapByCV: if imagePath, ok := action.Params.(string); ok { - return ud.TapByCV(imagePath, action.IgnoreNotFoundError) + return ud.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError) } return fmt.Errorf("invalid %s params: %v", uiTapByCV, action.Params) case uiDoubleTapXY: From 0f1206427e1abd100e3c98a544efc6e2ea5eb042 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 13:09:08 +0800 Subject: [PATCH 054/169] feat: swipe with identifier for logging --- go.mod | 2 +- go.sum | 4 +- hrp/internal/uixt/drag.go | 5 +- hrp/internal/uixt/swipe.go | 173 +++++++++----------------------- hrp/internal/uixt/swipe_test.go | 25 +---- hrp/internal/uixt/tap.go | 10 +- hrp/step_ios_ui.go | 14 ++- 7 files changed, 73 insertions(+), 160 deletions(-) diff --git a/go.mod b/go.mod index bd615864..7a9e3ac1 100644 --- a/go.mod +++ b/go.mod @@ -40,4 +40,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7 +replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 diff --git a/go.sum b/go.sum index d0059fe4..c8001b5a 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7 h1:pAvqLivdxSqCttO6lbEzg/zjxJO6oOQayfPKqBVD3t0= -github.com/debugtalk/gwda v0.0.0-20220828065105-59203789a7e7/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= +github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 h1:n/O+tMRl7XmuP778Oy2wunq8QpftRS0rlBkKumaJSbc= +github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= diff --git a/hrp/internal/uixt/drag.go b/hrp/internal/uixt/drag.go index b4c5ea56..0fc7fa64 100644 --- a/hrp/internal/uixt/drag.go +++ b/hrp/internal/uixt/drag.go @@ -1,5 +1,7 @@ package uixt +import "github.com/electricbubble/gwda" + func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) { return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...) } @@ -25,5 +27,6 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs fromX := x + width*xOffset fromY := y + height*yOffset - return dExt.WebDriver.DragFloat(fromX, fromY, toX, toY, pressForDuration[0]) + return dExt.WebDriver.DragFloat(fromX, fromY, toX, toY, + gwda.WithPressDuration(pressForDuration[0])) } diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index afae7550..5454682a 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -3,25 +3,68 @@ package uixt import ( "fmt" + "github.com/electricbubble/gwda" "github.com/rs/zerolog/log" ) -func (dExt *DriverExt) SwipeTo(direction string) (err error) { +func assertRelative(p float64) bool { + return p >= 0 && p <= 1 +} + +// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY] +func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier ...string) error { width := dExt.windowSize.Width height := dExt.windowSize.Height - var fromX, fromY, toX, toY int + if !assertRelative(fromX) || !assertRelative(fromY) || + !assertRelative(toX) || !assertRelative(toY) { + return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1", + fromX, fromY, toX, toY) + } + + fromX = float64(width) * fromX + fromY = float64(height) * fromY + toX = float64(width) * toX + toY = float64(height) * toY + + if len(identifier) > 0 && identifier[0] != "" { + option := gwda.WithCustomOption("log", map[string]interface{}{ + "enable": true, + "data": identifier[0], + }) + dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY, option) + } + return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) +} + +func (dExt *DriverExt) SwipeTo(direction string, identifier ...string) (err error) { switch direction { case "up": - fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4 + return dExt.SwipeUp(identifier...) case "down": - fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4 + return dExt.SwipeDown(identifier...) case "left": - fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2 + return dExt.SwipeLeft(identifier...) case "right": - fromX, fromY, toX, toY = width*1/4, height/2, width*3/4, height/2 + return dExt.SwipeRight(identifier...) } - return dExt.WebDriver.Swipe(fromX, fromY, toX, toY) + return fmt.Errorf("unexpected direction: %s", direction) +} + +func (dExt *DriverExt) SwipeUp(identifier ...string) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, identifier...) +} + +func (dExt *DriverExt) SwipeDown(identifier ...string) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, identifier...) +} + +func (dExt *DriverExt) SwipeLeft(identifier ...string) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, identifier...) +} + +func (dExt *DriverExt) SwipeRight(identifier ...string) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, identifier...) } // FindCondition indicates the condition to find a UI element @@ -42,119 +85,3 @@ func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, act } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } - -func (dExt *DriverExt) Swipe(pathname string, toX, toY int) (err error) { - return dExt.SwipeFloat(pathname, float64(toX), float64(toY)) -} - -func (dExt *DriverExt) SwipeFloat(pathname string, toX, toY float64) (err error) { - return dExt.SwipeOffsetFloat(pathname, toX, toY, 0.5, 0.5) -} - -func (dExt *DriverExt) SwipeOffset(pathname string, toX, toY int, xOffset, yOffset float64) (err error) { - return dExt.SwipeOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset) -} - -func (dExt *DriverExt) SwipeOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64) (err error) { - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { - return err - } - - fromX := x + width*xOffset - fromY := y + height*yOffset - - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) -} - -func (dExt *DriverExt) SwipeUp(pathname string, distance ...float64) (err error) { - return dExt.SwipeUpOffset(pathname, 0.5, 0.9, distance...) -} - -func (dExt *DriverExt) SwipeUpOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { - if len(distance) == 0 { - distance = []float64{1.0} - } - - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { - return err - } - - fromX := x + width*xOffset - fromY := (y + height) - height*(1.0-yOffset) - - toX := fromX - toY := fromY - height*distance[0] - - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) -} - -func (dExt *DriverExt) SwipeDown(pathname string, distance ...float64) (err error) { - return dExt.SwipeDownOffset(pathname, 0.5, 0.1, distance...) -} - -func (dExt *DriverExt) SwipeDownOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { - if len(distance) == 0 { - distance = []float64{1.0} - } - - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { - return err - } - - fromX := x + width*xOffset - fromY := y + height*yOffset - - toX := fromX - toY := fromY + height*distance[0] - - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) -} - -func (dExt *DriverExt) SwipeLeft(pathname string, distance ...float64) (err error) { - return dExt.SwipeLeftOffset(pathname, 0.9, 0.5, distance...) -} - -func (dExt *DriverExt) SwipeLeftOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { - if len(distance) == 0 { - distance = []float64{1.0} - } - - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { - return err - } - - fromX := x + width*xOffset - fromY := y + height*yOffset - - toX := fromX - width*distance[0] - toY := fromY - - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) -} - -func (dExt *DriverExt) SwipeRight(pathname string, distance ...float64) (err error) { - return dExt.SwipeRightOffset(pathname, 0.1, 0.5, distance...) -} - -func (dExt *DriverExt) SwipeRightOffset(pathname string, xOffset, yOffset float64, distance ...float64) (err error) { - if len(distance) == 0 { - distance = []float64{1.0} - } - - var x, y, width, height float64 - if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil { - return err - } - - fromX := x + width*xOffset - fromY := y + height*yOffset - - toX := fromX + width*distance[0] - toY := fromY - - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) -} diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index 77d5d39e..d7a1c8e9 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -4,29 +4,6 @@ import ( "testing" ) -func TestDriverExt_Swipe(t *testing.T) { - driverExt, err := InitWDAClient() - checkErr(t, err) - - pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" - - // gwda.SetDebug(true) - - err = driverExt.Swipe(pathSearch, 300, 500) - checkErr(t, err) - - err = driverExt.SwipeFloat(pathSearch, 300.9, 500) - checkErr(t, err) - - err = driverExt.SwipeOffset(pathSearch, 300, 500, 0.2, 0.5) - checkErr(t, err) - - driverExt.Debug(DmNotMatch) - - err = driverExt.OnlyOnceThreshold(0.92).SwipeOffsetFloat(pathSearch, 300.9, 499.1, 0.2, 0.5) - checkErr(t, err) -} - func TestSwipeUntil(t *testing.T) { driverExt, err := InitWDAClient() checkErr(t, err) @@ -46,7 +23,7 @@ func TestSwipeUntil(t *testing.T) { // swipe to first screen for i := 0; i < 5; i++ { - driverExt.SwipeTo("right") + driverExt.SwipeRight() } // swipe until app found diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index 9f072481..9a522825 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -8,12 +8,10 @@ import ( func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { if len(identifier) > 0 { - option := gwda.DataOption{ - "log": map[string]interface{}{ - "enable": true, - "data": identifier, - }, - } + option := gwda.WithCustomOption("log", map[string]interface{}{ + "enable": true, + "data": identifier, + }) return dExt.WebDriver.TapFloat(x, y, option) } return dExt.WebDriver.TapFloat(x, y) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index a8018462..43997f05 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -634,7 +634,7 @@ func (ud *uiDriver) doAction(action MobileAction) error { // swipe to first screen for i := 0; i < 5; i++ { - ud.SwipeTo("right") + ud.SwipeRight() } // default to retry 5 times @@ -721,8 +721,16 @@ func (ud *uiDriver) doAction(action MobileAction) error { } return fmt.Errorf("invalid %s params: %v", uiDoubleTap, action.Params) case uiSwipe: - if param, ok := action.Params.(string); ok { - return ud.SwipeTo(param) + if positions, ok := action.Params.([]float64); ok { + // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] + if len(positions) != 4 { + return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions) + } + return ud.SwipeRelative( + positions[0], positions[1], positions[2], positions[3], action.Identifier) + } + if direction, ok := action.Params.(string); ok { + return ud.SwipeTo(direction, action.Identifier) } return fmt.Errorf("invalid %s params: %v", uiSwipe, action.Params) case uiInput: From a12b72bc106c3598c7114061146cabcf7a0e8dd2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 13:28:54 +0800 Subject: [PATCH 055/169] change: upgrade to go 1.18 --- go.mod | 80 +++- go.sum | 490 ++++++++++++------------ hrp/internal/boomer/ulimit.go | 1 - hrp/internal/boomer/ulimit_windows.go | 1 - hrp/internal/builtin/utils_unix.go | 1 - hrp/internal/builtin/utils_windows.go | 1 - hrp/internal/dial/traceroute_unix.go | 1 - hrp/internal/dial/traceroute_windows.go | 1 - 8 files changed, 303 insertions(+), 273 deletions(-) diff --git a/go.mod b/go.mod index 7a9e3ac1..9eaee6e2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/httprunner/httprunner/v4 -go 1.16 +go 1.18 require ( github.com/andybalholm/brotli v1.0.4 @@ -9,34 +9,82 @@ require ( github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 - github.com/go-errors/errors v1.0.1 - github.com/go-openapi/spec v0.20.6 + github.com/go-errors/errors v1.4.2 + github.com/go-openapi/spec v0.20.7 github.com/go-ping/ping v1.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.4.1 + github.com/gorilla/websocket v1.5.0 github.com/httprunner/funplugin v0.5.0 - github.com/jinzhu/copier v0.3.2 + github.com/jinzhu/copier v0.3.5 github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 github.com/maja42/goval v1.2.1 - github.com/miekg/dns v1.1.25 - github.com/mitchellh/mapstructure v1.4.1 + github.com/miekg/dns v1.1.50 + github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.11.0 - github.com/rs/zerolog v1.27.0 + github.com/prometheus/client_golang v1.13.0 + github.com/rs/zerolog v1.28.0 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/spf13/cobra v1.2.1 - github.com/stretchr/testify v1.7.0 + github.com/spf13/cobra v1.5.0 + github.com/stretchr/testify v1.8.0 + golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1 + golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + cloud.google.com/go/compute v1.7.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/electricbubble/gidevice v0.6.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/golang/protobuf v1.5.2 // indirect + github.com/hashicorp/go-hclog v1.3.0 // indirect + github.com/hashicorp/go-plugin v1.4.5 // indirect + 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/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // 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.16 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + 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/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.5.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect gocv.io/x/gocv v0.31.0 // indirect - golang.org/x/net v0.0.0-20220225172249-27dd8689420f - golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 - google.golang.org/grpc v1.45.0 - google.golang.org/protobuf v1.28.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + golang.org/x/mod v0.4.2 // indirect + golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect + golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.7 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + howett.net/plist v1.0.0 // indirect ) // replace github.com/httprunner/funplugin => ../funplugin diff --git a/go.sum b/go.sum index c8001b5a..e615a6ac 100644 --- a/go.sum +++ b/go.sum @@ -17,17 +17,33 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -37,15 +53,11 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -54,20 +66,15 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 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= @@ -76,20 +83,15 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -98,10 +100,6 @@ github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 h1:n/O+tMRl7XmuP778 github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= @@ -112,34 +110,29 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +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= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -147,25 +140,17 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 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-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 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/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -178,6 +163,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -196,7 +182,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -209,13 +195,16 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -227,6 +216,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -234,58 +226,43 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw= github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-hclog v1.3.0 h1:G0ACM8Z2WilWgPv3Vdzwm3V0BQu/kSmrkVtpe1fy9do= +github.com/hashicorp/go-hclog v1.3.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= +github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +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.0 h1:Laoe8URu71qeyST9wvRtGSkDWc8Y3T1IrnvFSTHmO84= github.com/httprunner/funplugin v0.5.0/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc= 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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= -github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= -github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= -github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -294,85 +271,56 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= -github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= -github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= -github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= -github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU= github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= -github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -380,38 +328,29 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 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.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -419,83 +358,58 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +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.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +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/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= @@ -504,9 +418,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -515,27 +426,15 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= gocv.io/x/gocv v0.27.0/go.mod h1:n4LnYjykU6y9gn48yZf4eLCdtuSb77XxSkW6g0wGf/A= gocv.io/x/gocv v0.31.0 h1:BHDtK8v+YPvoSPQTTiZB2fM/7BLg6511JqkruY2z6LQ= gocv.io/x/gocv v0.31.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 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= @@ -571,18 +470,15 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -591,8 +487,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -615,10 +509,18 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +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-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1 h1:TWZxd/th7FbRSMret2MVQdlI8uT49QEtwZdvJrxjEHU= +golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/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= @@ -630,8 +532,17 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -642,14 +553,13 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A= +golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -659,14 +569,9 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -699,19 +604,36 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/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= @@ -727,16 +649,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -744,10 +662,8 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -770,7 +686,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -779,15 +694,23 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -809,7 +732,24 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -858,10 +798,48 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51 h1:ucpgjuzWqWrj0NEwjUpsGTf2IGxyLtmuSk0oGgifjec= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -882,9 +860,21 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -898,18 +888,16 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba 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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -920,11 +908,11 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/hrp/internal/boomer/ulimit.go b/hrp/internal/boomer/ulimit.go index 40f0c0cc..bc62a218 100644 --- a/hrp/internal/boomer/ulimit.go +++ b/hrp/internal/boomer/ulimit.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package boomer diff --git a/hrp/internal/boomer/ulimit_windows.go b/hrp/internal/boomer/ulimit_windows.go index d02840dc..8641b111 100644 --- a/hrp/internal/boomer/ulimit_windows.go +++ b/hrp/internal/boomer/ulimit_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package boomer diff --git a/hrp/internal/builtin/utils_unix.go b/hrp/internal/builtin/utils_unix.go index 9c0feb2c..a23da061 100644 --- a/hrp/internal/builtin/utils_unix.go +++ b/hrp/internal/builtin/utils_unix.go @@ -1,5 +1,4 @@ //go:build darwin || linux -// +build darwin linux package builtin diff --git a/hrp/internal/builtin/utils_windows.go b/hrp/internal/builtin/utils_windows.go index 6c8ae13e..062b493b 100644 --- a/hrp/internal/builtin/utils_windows.go +++ b/hrp/internal/builtin/utils_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package builtin diff --git a/hrp/internal/dial/traceroute_unix.go b/hrp/internal/dial/traceroute_unix.go index b6621592..d9d309c8 100644 --- a/hrp/internal/dial/traceroute_unix.go +++ b/hrp/internal/dial/traceroute_unix.go @@ -1,5 +1,4 @@ //go:build darwin || linux -// +build darwin linux package dial diff --git a/hrp/internal/dial/traceroute_windows.go b/hrp/internal/dial/traceroute_windows.go index a1b4b37b..f8ce4743 100644 --- a/hrp/internal/dial/traceroute_windows.go +++ b/hrp/internal/dial/traceroute_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package dial From a742b575fab04c15941e11f1b266d0eb12ade448 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 15:37:10 +0800 Subject: [PATCH 056/169] change: make opencv as optional build tags --- hrp/internal/uixt/default.go | 29 ++++++ hrp/internal/uixt/ext.go | 138 ++-------------------------- hrp/internal/uixt/gesture.go | 3 +- hrp/internal/uixt/gesture_test.go | 3 +- hrp/internal/uixt/opencv.go | 146 ++++++++++++++++++++++++++++++ scripts/build.sh | 1 + 6 files changed, 187 insertions(+), 133 deletions(-) create mode 100644 hrp/internal/uixt/default.go create mode 100644 hrp/internal/uixt/opencv.go diff --git a/hrp/internal/uixt/default.go b/hrp/internal/uixt/default.go new file mode 100644 index 00000000..7506f686 --- /dev/null +++ b/hrp/internal/uixt/default.go @@ -0,0 +1,29 @@ +//go:build !opencv + +package uixt + +import ( + "image" + + "github.com/rs/zerolog/log" +) + +func (dExt *DriverExt) extendOpenCV(threshold float64, matchMode ...TemplateMatchMode) (err error) { + log.Fatal().Msg("opencv is not supported") + return +} + +func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { + log.Fatal().Msg("opencv is not supported") + return +} + +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { + log.Fatal().Msg("opencv is not supported") + return +} + +func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { + log.Fatal().Msg("opencv is not supported") + return +} diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 82d9c394..02462183 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -6,7 +6,6 @@ import ( "image" "image/jpeg" "image/png" - "io/ioutil" "mime" "mime/multipart" "net/http" @@ -15,7 +14,6 @@ import ( "strings" "github.com/electricbubble/gwda" - cvHelper "github.com/electricbubble/opencv-helper" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -23,40 +21,16 @@ import ( // TemplateMatchMode is the type of the template matching operation. type TemplateMatchMode int -const ( - // TmSqdiff maps to TM_SQDIFF - TmSqdiff TemplateMatchMode = iota - // TmSqdiffNormed maps to TM_SQDIFF_NORMED - TmSqdiffNormed - // TmCcorr maps to TM_CCORR - TmCcorr - // TmCcorrNormed maps to TM_CCORR_NORMED - TmCcorrNormed - // TmCcoeff maps to TM_CCOEFF - TmCcoeff - // TmCcoeffNormed maps to TM_CCOEFF_NORMED - TmCcoeffNormed -) - -type DebugMode int - -const ( - // DmOff no output - DmOff DebugMode = iota - // DmEachMatch output matched and mismatched values - DmEachMatch - // DmNotMatch output only values that do not match - DmNotMatch -) - type DriverExt struct { gwda.WebDriver windowSize gwda.Size - scale float64 - MatchMode TemplateMatchMode - Threshold float64 frame *bytes.Buffer doneMjpegStream chan bool + + // OpenCV + scale float64 + matchMode TemplateMatchMode + threshold float64 } // Extend 获得扩展后的 Driver, @@ -68,45 +42,14 @@ func Extend(driver gwda.WebDriver, threshold float64, matchMode ...TemplateMatch dExt = &DriverExt{WebDriver: driver} dExt.doneMjpegStream = make(chan bool, 1) - if dExt.scale, err = dExt.Scale(); err != nil { - return &DriverExt{}, err - } - // get device window size dExt.windowSize, err = dExt.WebDriver.WindowSize() if err != nil { return nil, errors.Wrap(err, "failed to get windows size") } - if len(matchMode) == 0 { - matchMode = []TemplateMatchMode{TmCcoeffNormed} - } - dExt.MatchMode = matchMode[0] - cvHelper.Debug(cvHelper.DebugMode(DmOff)) - dExt.Threshold = threshold - return dExt, nil -} - -func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) { - newExt = new(DriverExt) - newExt.WebDriver = dExt.WebDriver - newExt.scale = dExt.scale - newExt.MatchMode = dExt.MatchMode - newExt.Threshold = threshold - return -} - -func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) { - newExt = new(DriverExt) - newExt.WebDriver = dExt.WebDriver - newExt.scale = dExt.scale - newExt.MatchMode = matchMode - newExt.Threshold = dExt.Threshold - return -} - -func (dExt *DriverExt) Debug(dm DebugMode) { - cvHelper.Debug(cvHelper.DebugMode(dm)) + err = dExt.extendOpenCV(threshold, matchMode...) + return dExt, err } func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { @@ -225,46 +168,6 @@ func (dExt *DriverExt) ScreenShot(fileName string) (string, error) { return dExt.saveScreenShot(raw, fileName) } -// func (sExt *DriverExt) findImgRect(search string) (rect image.Rectangle, err error) { -// pathSource := filepath.Join(sExt.pathname, cvHelper.GenFilename()) -// if err = sExt.driver.ScreenshotToDisk(pathSource); err != nil { -// return image.Rectangle{}, err -// } -// -// if rect, err = cvHelper.FindImageRectFromDisk(pathSource, search, float32(sExt.Threshold), cvHelper.TemplateMatchMode(sExt.MatchMode)); err != nil { -// return image.Rectangle{}, err -// } -// return -// } - -func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { - var bufSource, bufSearch *bytes.Buffer - if bufSearch, err = getBufFromDisk(search); err != nil { - return nil, err - } - if bufSource, err = dExt.takeScreenShot(); err != nil { - return nil, err - } - - if rects, err = cvHelper.FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.Threshold), cvHelper.TemplateMatchMode(dExt.MatchMode)); err != nil { - return nil, err - } - return -} - -func getBufFromDisk(name string) (*bytes.Buffer, error) { - var f *os.File - var err error - if f, err = os.Open(name); err != nil { - return nil, err - } - var all []byte - if all, err = ioutil.ReadAll(f); err != nil { - return nil, err - } - return bytes.NewBuffer(all), nil -} - // isPathExists returns true if path exists, whether path is file or dir func isPathExists(path string) bool { if _, err := os.Stat(path); os.IsNotExist(err) { @@ -299,33 +202,6 @@ func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height flo return dExt.FindImageRectInUIKit(search) } -func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { - var bufSource, bufSearch *bytes.Buffer - if bufSearch, err = getBufFromDisk(imagePath); err != nil { - return 0, 0, 0, 0, err - } - if bufSource, err = dExt.takeScreenShot(); err != nil { - return 0, 0, 0, 0, err - } - - var rect image.Rectangle - if rect, err = cvHelper.FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.Threshold), cvHelper.TemplateMatchMode(dExt.MatchMode)); err != nil { - return 0, 0, 0, 0, err - } - - // if rect, err = dExt.findImgRect(search); err != nil { - // return 0, 0, 0, 0, err - // } - x, y, width, height = dExt.MappingToRectInUIKit(rect) - return -} - -func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { - x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale - width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale - return -} - func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error { return dExt.PerformAppiumTouchActions(touchActions) } diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index 75462167..2491bb27 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -1,8 +1,9 @@ +//go:build opencv + package uixt import ( "image" - "sort" "github.com/electricbubble/gwda" ) diff --git a/hrp/internal/uixt/gesture_test.go b/hrp/internal/uixt/gesture_test.go index 075023bd..5e4ce57b 100644 --- a/hrp/internal/uixt/gesture_test.go +++ b/hrp/internal/uixt/gesture_test.go @@ -1,7 +1,8 @@ +//go:build opencv + package uixt import ( - "strconv" "strings" "testing" ) diff --git a/hrp/internal/uixt/opencv.go b/hrp/internal/uixt/opencv.go new file mode 100644 index 00000000..504e6eef --- /dev/null +++ b/hrp/internal/uixt/opencv.go @@ -0,0 +1,146 @@ +//go:build opencv + +package uixt + +import ( + "bytes" + "image" + "io/ioutil" + "os" + + cvHelper "github.com/electricbubble/opencv-helper" +) + +const ( + // TmSqdiff maps to TM_SQDIFF + TmSqdiff TemplateMatchMode = iota + // TmSqdiffNormed maps to TM_SQDIFF_NORMED + TmSqdiffNormed + // TmCcorr maps to TM_CCORR + TmCcorr + // TmCcorrNormed maps to TM_CCORR_NORMED + TmCcorrNormed + // TmCcoeff maps to TM_CCOEFF + TmCcoeff + // TmCcoeffNormed maps to TM_CCOEFF_NORMED + TmCcoeffNormed +) + +type DebugMode int + +const ( + // DmOff no output + DmOff DebugMode = iota + // DmEachMatch output matched and mismatched values + DmEachMatch + // DmNotMatch output only values that do not match + DmNotMatch +) + +// Extend 获得扩展后的 Driver, +// 并指定匹配阀值, +// 获取当前设备的 Scale, +// 默认匹配模式为 TmCcoeffNormed, +// 默认关闭 OpenCV 匹配值计算后的输出 +func (dExt *DriverExt) extendOpenCV(threshold float64, matchMode ...TemplateMatchMode) (err error) { + if dExt.scale, err = dExt.Scale(); err != nil { + return err + } + + if len(matchMode) == 0 { + matchMode = []TemplateMatchMode{TmCcoeffNormed} + } + dExt.matchMode = matchMode[0] + cvHelper.Debug(cvHelper.DebugMode(DmOff)) + dExt.threshold = threshold + return nil +} + +func (dExt *DriverExt) Debug(dm DebugMode) { + cvHelper.Debug(cvHelper.DebugMode(dm)) +} + +func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) { + newExt = new(DriverExt) + newExt.WebDriver = dExt.WebDriver + newExt.scale = dExt.scale + newExt.matchMode = dExt.matchMode + newExt.threshold = threshold + return +} + +func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) { + newExt = new(DriverExt) + newExt.WebDriver = dExt.WebDriver + newExt.scale = dExt.scale + newExt.matchMode = matchMode + newExt.threshold = dExt.threshold + return +} + +// func (sExt *DriverExt) findImgRect(search string) (rect image.Rectangle, err error) { +// pathSource := filepath.Join(sExt.pathname, cvHelper.GenFilename()) +// if err = sExt.driver.ScreenshotToDisk(pathSource); err != nil { +// return image.Rectangle{}, err +// } +// +// if rect, err = cvHelper.FindImageRectFromDisk(pathSource, search, float32(sExt.Threshold), cvHelper.TemplateMatchMode(sExt.MatchMode)); err != nil { +// return image.Rectangle{}, err +// } +// return +// } + +func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { + var bufSource, bufSearch *bytes.Buffer + if bufSearch, err = getBufFromDisk(search); err != nil { + return nil, err + } + if bufSource, err = dExt.takeScreenShot(); err != nil { + return nil, err + } + + if rects, err = cvHelper.FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil { + return nil, err + } + return +} + +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { + var bufSource, bufSearch *bytes.Buffer + if bufSearch, err = getBufFromDisk(imagePath); err != nil { + return 0, 0, 0, 0, err + } + if bufSource, err = dExt.takeScreenShot(); err != nil { + return 0, 0, 0, 0, err + } + + var rect image.Rectangle + if rect, err = cvHelper.FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil { + return 0, 0, 0, 0, err + } + + // if rect, err = dExt.findImgRect(search); err != nil { + // return 0, 0, 0, 0, err + // } + x, y, width, height = dExt.MappingToRectInUIKit(rect) + return +} + +func getBufFromDisk(name string) (*bytes.Buffer, error) { + var f *os.File + var err error + if f, err = os.Open(name); err != nil { + return nil, err + } + var all []byte + if all, err = ioutil.ReadAll(f); err != nil { + return nil, err + } + return bytes.NewBuffer(all), nil +} + +func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { + x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale + width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale + return +} diff --git a/scripts/build.sh b/scripts/build.sh index 8043ceb0..7a75d8ed 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -15,6 +15,7 @@ mkdir -p "output" bin_path="output/hrp" # build +# optional build tags: opencv go build -ldflags '-s -w' -o "$bin_path" hrp/cmd/cli/main.go # check output and version From 937b48236eca83b1ebf5efff6f94dc93509659ff Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 16:03:58 +0800 Subject: [PATCH 057/169] change: make ocr as optional build tags --- hrp/internal/uixt/ext.go | 35 ++++++++++++------- hrp/internal/uixt/init.go | 2 +- hrp/internal/uixt/ocr_off.go | 10 ++++++ hrp/internal/uixt/{ocr.go => ocr_on.go} | 6 ++-- hrp/internal/uixt/ocr_test.go | 2 ++ .../uixt/{default.go => opencv_off.go} | 6 ++-- hrp/internal/uixt/{opencv.go => opencv_on.go} | 33 +++++++++++------ scripts/build.sh | 4 +-- 8 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 hrp/internal/uixt/ocr_off.go rename hrp/internal/uixt/{ocr.go => ocr_on.go} (98%) rename hrp/internal/uixt/{default.go => opencv_off.go} (78%) rename hrp/internal/uixt/{opencv.go => opencv_on.go} (88%) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 02462183..40907c4f 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -21,24 +21,36 @@ import ( // TemplateMatchMode is the type of the template matching operation. type TemplateMatchMode int +type CVArgs struct { + scale float64 + matchMode TemplateMatchMode + threshold float64 +} + +type CVOption func(*CVArgs) + +func WithTemplateMatchMode(mode TemplateMatchMode) CVOption { + return func(args *CVArgs) { + args.matchMode = mode + } +} + +func WithThreshold(threshold float64) CVOption { + return func(args *CVArgs) { + args.threshold = threshold + } +} + type DriverExt struct { gwda.WebDriver windowSize gwda.Size frame *bytes.Buffer doneMjpegStream chan bool - // OpenCV - scale float64 - matchMode TemplateMatchMode - threshold float64 + CVArgs } -// Extend 获得扩展后的 Driver, -// 并指定匹配阀值, -// 获取当前设备的 Scale, -// 默认匹配模式为 TmCcoeffNormed, -// 默认关闭 OpenCV 匹配值计算后的输出 -func Extend(driver gwda.WebDriver, threshold float64, matchMode ...TemplateMatchMode) (dExt *DriverExt, err error) { +func extend(driver gwda.WebDriver) (dExt *DriverExt, err error) { dExt = &DriverExt{WebDriver: driver} dExt.doneMjpegStream = make(chan bool, 1) @@ -48,8 +60,7 @@ func Extend(driver gwda.WebDriver, threshold float64, matchMode ...TemplateMatch return nil, errors.Wrap(err, "failed to get windows size") } - err = dExt.extendOpenCV(threshold, matchMode...) - return dExt, err + return dExt, nil } func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/init.go index 5acd9bb7..7f45db42 100644 --- a/hrp/internal/uixt/init.go +++ b/hrp/internal/uixt/init.go @@ -45,7 +45,7 @@ func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } - driverExt, err := Extend(driver, 0.95) + driverExt, err := Extend(driver) if err != nil { return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") } diff --git a/hrp/internal/uixt/ocr_off.go b/hrp/internal/uixt/ocr_off.go new file mode 100644 index 00000000..7c3536b5 --- /dev/null +++ b/hrp/internal/uixt/ocr_off.go @@ -0,0 +1,10 @@ +//go:build !ocr + +package uixt + +import "github.com/rs/zerolog/log" + +func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float64, err error) { + log.Fatal().Msg("OCR is not supported") + return +} diff --git a/hrp/internal/uixt/ocr.go b/hrp/internal/uixt/ocr_on.go similarity index 98% rename from hrp/internal/uixt/ocr.go rename to hrp/internal/uixt/ocr_on.go index d261e3de..2edf0d5a 100644 --- a/hrp/internal/uixt/ocr.go +++ b/hrp/internal/uixt/ocr_on.go @@ -1,16 +1,14 @@ +//go:build ocr + package uixt import ( "bytes" - "encoding/base64" - "encoding/json" "fmt" "image" - "io/ioutil" "mime/multipart" "net/http" "strings" - "time" ) var client = &http.Client{ diff --git a/hrp/internal/uixt/ocr_test.go b/hrp/internal/uixt/ocr_test.go index c334a1dd..928b39a2 100644 --- a/hrp/internal/uixt/ocr_test.go +++ b/hrp/internal/uixt/ocr_test.go @@ -1,3 +1,5 @@ +//go:build ocr + package uixt import ( diff --git a/hrp/internal/uixt/default.go b/hrp/internal/uixt/opencv_off.go similarity index 78% rename from hrp/internal/uixt/default.go rename to hrp/internal/uixt/opencv_off.go index 7506f686..7d56d5c5 100644 --- a/hrp/internal/uixt/default.go +++ b/hrp/internal/uixt/opencv_off.go @@ -5,12 +5,12 @@ package uixt import ( "image" + "github.com/electricbubble/gwda" "github.com/rs/zerolog/log" ) -func (dExt *DriverExt) extendOpenCV(threshold float64, matchMode ...TemplateMatchMode) (err error) { - log.Fatal().Msg("opencv is not supported") - return +func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) { + return extend(driver) } func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) { diff --git a/hrp/internal/uixt/opencv.go b/hrp/internal/uixt/opencv_on.go similarity index 88% rename from hrp/internal/uixt/opencv.go rename to hrp/internal/uixt/opencv_on.go index 504e6eef..6a184695 100644 --- a/hrp/internal/uixt/opencv.go +++ b/hrp/internal/uixt/opencv_on.go @@ -8,12 +8,15 @@ import ( "io/ioutil" "os" + "github.com/electricbubble/gwda" cvHelper "github.com/electricbubble/opencv-helper" ) const ( + // TmCcoeffNormed maps to TM_CCOEFF_NORMED + TmCcoeffNormed TemplateMatchMode = iota // TmSqdiff maps to TM_SQDIFF - TmSqdiff TemplateMatchMode = iota + TmSqdiff // TmSqdiffNormed maps to TM_SQDIFF_NORMED TmSqdiffNormed // TmCcorr maps to TM_CCORR @@ -22,8 +25,6 @@ const ( TmCcorrNormed // TmCcoeff maps to TM_CCOEFF TmCcoeff - // TmCcoeffNormed maps to TM_CCOEFF_NORMED - TmCcoeffNormed ) type DebugMode int @@ -42,18 +43,28 @@ const ( // 获取当前设备的 Scale, // 默认匹配模式为 TmCcoeffNormed, // 默认关闭 OpenCV 匹配值计算后的输出 -func (dExt *DriverExt) extendOpenCV(threshold float64, matchMode ...TemplateMatchMode) (err error) { - if dExt.scale, err = dExt.Scale(); err != nil { - return err +func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) { + dExt, err = extend(driver) + if err != nil { + return nil, err } - if len(matchMode) == 0 { - matchMode = []TemplateMatchMode{TmCcoeffNormed} + for _, option := range options { + option(&dExt.CVArgs) + } + + if dExt.scale, err = dExt.Scale(); err != nil { + return nil, err + } + + if dExt.threshold == 0 { + dExt.threshold = 0.95 // default threshold + } + if dExt.matchMode == 0 { + dExt.matchMode = TmCcoeffNormed // default match mode } - dExt.matchMode = matchMode[0] cvHelper.Debug(cvHelper.DebugMode(DmOff)) - dExt.threshold = threshold - return nil + return } func (dExt *DriverExt) Debug(dm DebugMode) { diff --git a/scripts/build.sh b/scripts/build.sh index 7a75d8ed..1c6cfd52 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -15,8 +15,8 @@ mkdir -p "output" bin_path="output/hrp" # build -# optional build tags: opencv -go build -ldflags '-s -w' -o "$bin_path" hrp/cmd/cli/main.go +# optional build tags: opencv ocr +go build -ldflags '-s -w' -tags ocr -o "$bin_path" hrp/cmd/cli/main.go # check output and version ls -lh "$bin_path" From 976e7153573033f15ec5230e2912d06e10ea5b9a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 17:50:08 +0800 Subject: [PATCH 058/169] fix: run ocr alone without opencv --- Makefile | 2 +- examples/uitest/demo_weixin_live.json | 7 +++++-- examples/uitest/demo_weixin_live.yaml | 3 +++ hrp/internal/uixt/README.md | 4 ++++ hrp/internal/uixt/ext.go | 12 +++++++++++- hrp/internal/uixt/gesture.go | 1 + hrp/internal/uixt/ocr_on.go | 4 ++++ hrp/internal/uixt/opencv_off.go | 5 ----- hrp/internal/uixt/opencv_on.go | 10 ---------- scripts/build.sh | 14 +++++++++++--- scripts/bump_version.sh | 4 ++-- 11 files changed, 42 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index b094ea9c..f2f15434 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ bump: ## bump hrp version, e.g. make bump version=4.0.0 .PHONY: build build: ## build hrp cli tool @echo "[info] build hrp cli tool" - @. scripts/build.sh + @. scripts/build.sh $(tags) .PHONY: install-hooks install-hooks: ## install git hooks diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json index a789f0ee..9aa990f3 100644 --- a/examples/uitest/demo_weixin_live.json +++ b/examples/uitest/demo_weixin_live.json @@ -64,6 +64,7 @@ { "method": "swipe_to_tap_text", "params": "轻触进入直播间", + "identifier": "进入直播间", "max_retry_times": 10 } ] @@ -75,7 +76,8 @@ "actions": [ { "method": "swipe", - "params": "up" + "params": "up", + "identifier": "第一次上划" }, { "method": "sleep", @@ -86,7 +88,8 @@ }, { "method": "swipe", - "params": "up" + "params": "up", + "identifier": "第二次上划" }, { "method": "sleep", diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml index d5f3d795..45393eb2 100644 --- a/examples/uitest/demo_weixin_live.yaml +++ b/examples/uitest/demo_weixin_live.yaml @@ -33,17 +33,20 @@ teststeps: actions: - method: swipe_to_tap_text params: 轻触进入直播间 + identifier: 进入直播间 max_retry_times: 10 - name: 向上滑动,等待 10s ios: actions: - method: swipe params: up + identifier: 第一次上划 - method: sleep params: 10 - method: screenshot - method: swipe params: up + identifier: 第二次上划 - method: sleep params: 10 - method: screenshot diff --git a/hrp/internal/uixt/README.md b/hrp/internal/uixt/README.md index e9191b29..111281c0 100644 --- a/hrp/internal/uixt/README.md +++ b/hrp/internal/uixt/README.md @@ -28,6 +28,10 @@ You can get more installation introduction on [hybridgroup/gocv]. OCR API is a paid service, you need to pre-purchase and configure the account key. +```bash +$ make build tags=ocr +``` + ## Thanks This uixt module is initially forked from [electricbubble/gwda-ext-opencv] and made a lot of changes. diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 40907c4f..c140edc2 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -22,7 +22,6 @@ import ( type TemplateMatchMode int type CVArgs struct { - scale float64 matchMode TemplateMatchMode threshold float64 } @@ -46,6 +45,7 @@ type DriverExt struct { windowSize gwda.Size frame *bytes.Buffer doneMjpegStream chan bool + scale float64 CVArgs } @@ -60,6 +60,10 @@ func extend(driver gwda.WebDriver) (dExt *DriverExt, err error) { return nil, errors.Wrap(err, "failed to get windows size") } + if dExt.scale, err = dExt.Scale(); err != nil { + return nil, err + } + return dExt, nil } @@ -213,6 +217,12 @@ func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height flo return dExt.FindImageRectInUIKit(search) } +func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { + x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale + width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale + return +} + func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error { return dExt.PerformAppiumTouchActions(touchActions) } diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index 2491bb27..5edd8147 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -4,6 +4,7 @@ package uixt import ( "image" + "sort" "github.com/electricbubble/gwda" ) diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 2edf0d5a..79d0c271 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -4,11 +4,15 @@ package uixt import ( "bytes" + "encoding/base64" + "encoding/json" "fmt" "image" + "io/ioutil" "mime/multipart" "net/http" "strings" + "time" ) var client = &http.Client{ diff --git a/hrp/internal/uixt/opencv_off.go b/hrp/internal/uixt/opencv_off.go index 7d56d5c5..18045295 100644 --- a/hrp/internal/uixt/opencv_off.go +++ b/hrp/internal/uixt/opencv_off.go @@ -22,8 +22,3 @@ func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, heig log.Fatal().Msg("opencv is not supported") return } - -func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { - log.Fatal().Msg("opencv is not supported") - return -} diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 6a184695..81afe225 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -53,10 +53,6 @@ func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err er option(&dExt.CVArgs) } - if dExt.scale, err = dExt.Scale(); err != nil { - return nil, err - } - if dExt.threshold == 0 { dExt.threshold = 0.95 // default threshold } @@ -149,9 +145,3 @@ func getBufFromDisk(name string) (*bytes.Buffer, error) { } return bytes.NewBuffer(all), nil } - -func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { - x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale - width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale - return -} diff --git a/scripts/build.sh b/scripts/build.sh index 1c6cfd52..67c514ce 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,8 +4,10 @@ # Usage: # $ make build +# $ make build tags=ocr # or -# $ bash cli/scripts/build.sh +# $ bash scripts/build.sh +# $ bash scripts/build.sh ocr set -e set -x @@ -14,9 +16,15 @@ set -x mkdir -p "output" bin_path="output/hrp" -# build # optional build tags: opencv ocr -go build -ldflags '-s -w' -tags ocr -o "$bin_path" hrp/cmd/cli/main.go +tags=$1 + +# build +if [ -z "$tags" ]; then + go build -ldflags '-s -w' -o "$bin_path" hrp/cmd/cli/main.go +else + go build -ldflags '-s -w' -tags "$tags" -o "$bin_path" hrp/cmd/cli/main.go +fi # check output and version ls -lh "$bin_path" diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 1abba52d..98141421 100644 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -3,9 +3,9 @@ # release will be triggered on github actions, see .github/workflows/release.yml # Usage: -# $ make bump version=v0.5.2 +# $ make bump version=v4.3.0 # or -# $ bash cli/scripts/bump_version.sh v0.5.2 +# $ bash scripts/bump_version.sh v4.3.0 set -e From 7a94afaf7ebaf2c707edcceddab5df42fc36a921 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 22 Sep 2022 21:11:38 +0800 Subject: [PATCH 059/169] feat: get wda logs --- examples/uitest/demo_weixin_live.json | 12 ++- examples/uitest/demo_weixin_live.yaml | 5 ++ examples/uitest/demo_weixin_test.go | 7 +- hrp/config.go | 24 ++--- hrp/internal/dial/dns.go | 2 +- hrp/internal/uixt/drag_test.go | 2 +- hrp/internal/uixt/ext.go | 1 + hrp/internal/uixt/init.go | 121 +++++++++++++++++++++++++- hrp/internal/uixt/ocr_on.go | 3 +- hrp/internal/uixt/swipe_test.go | 2 +- hrp/internal/uixt/tap_test.go | 6 +- hrp/internal/uixt/touch_test.go | 4 +- hrp/runner.go | 2 +- hrp/session.go | 17 ++++ hrp/step_ios_ui.go | 47 ++++------ hrp/step_ios_ui_test.go | 4 +- hrp/summary.go | 2 +- 17 files changed, 201 insertions(+), 60 deletions(-) diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json index 9aa990f3..f5f3f23a 100644 --- a/examples/uitest/demo_weixin_live.json +++ b/examples/uitest/demo_weixin_live.json @@ -1,6 +1,13 @@ { "config": { - "name": "通过 feed 卡片进入微信直播间" + "name": "通过 feed 卡片进入微信直播间", + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] }, "teststeps": [ { @@ -40,7 +47,8 @@ }, { "method": "tap_ocr", - "params": "视频号" + "params": "视频号", + "identifier": "进入视频号" } ] } diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml index 45393eb2..44b19b50 100644 --- a/examples/uitest/demo_weixin_live.yaml +++ b/examples/uitest/demo_weixin_live.yaml @@ -1,5 +1,9 @@ config: name: 通过 feed 卡片进入微信直播间 + ios: + - port: 8700 + mjpeg_port: 8800 + log_on: true teststeps: - name: 启动微信 ios: @@ -22,6 +26,7 @@ teststeps: params: 发现 - method: tap_ocr params: 视频号 + identifier: 进入视频号 - name: 处理青少年弹窗 ios: actions: diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 1031aefa..c692ddf1 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -9,7 +9,8 @@ import ( func TestIOSWeixinLive(t *testing.T) { testCase := &hrp.TestCase{ - Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), // .SetIOS(hrp.WDADevice{Port: 8700, MjpegPort: 8800}) + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). + SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动微信"). IOS(). @@ -20,8 +21,8 @@ func TestIOSWeixinLive(t *testing.T) { AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), hrp.NewStep("进入直播页"). IOS(). - Tap("发现"). // 进入「发现页」 - TapByOCR("视频号"), // 通过 OCR 识别「视频号」 + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号", hrp.WithIdentifier("进入视频号")), // 通过 OCR 识别「视频号」 hrp.NewStep("处理青少年弹窗"). IOS(). TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), diff --git a/hrp/config.go b/hrp/config.go index 103ea986..1884368b 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -29,7 +29,7 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"` - IOS []*IOSConfig `json:"ios,omitempty" yaml:"ios,omitempty"` + IOS []*WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"` Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -100,23 +100,23 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { return c } -func (c *TConfig) SetIOS(device WDADevice) *TConfig { +func (c *TConfig) SetIOS(options ...WDAOption) *TConfig { + wdaOptions := &WDAOptions{} + for _, option := range options { + option(wdaOptions) + } + // each device can have its own settings - if device.UDID != "" { - c.IOS = append(c.IOS, &IOSConfig{ - WDADevice: device, - }) + if wdaOptions.UDID != "" { + c.IOS = append(c.IOS, wdaOptions) return c } - // device UDID is not specified ,settings will be shared - iosConfig := &IOSConfig{ - WDADevice: device, - } + // device UDID is not specified, settings will be shared if len(c.IOS) == 0 { - c.IOS = append(c.IOS, iosConfig) + c.IOS = append(c.IOS, wdaOptions) } else { - c.IOS[0] = iosConfig + c.IOS[0] = wdaOptions } return c } diff --git a/hrp/internal/dial/dns.go b/hrp/internal/dial/dns.go index 7f1e7e03..20a3f3d4 100644 --- a/hrp/internal/dial/dns.go +++ b/hrp/internal/dial/dns.go @@ -1,7 +1,6 @@ package dial import ( - "encoding/json" "fmt" "io/ioutil" "net" @@ -18,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go index 09b554b1..258c515c 100644 --- a/hrp/internal/uixt/drag_test.go +++ b/hrp/internal/uixt/drag_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_Drag(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index c140edc2..d27d3815 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -46,6 +46,7 @@ type DriverExt struct { frame *bytes.Buffer doneMjpegStream chan bool scale float64 + host string CVArgs } diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/init.go index 7f45db42..4c6b39e7 100644 --- a/hrp/internal/uixt/init.go +++ b/hrp/internal/uixt/init.go @@ -1,9 +1,16 @@ package uixt import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( @@ -21,9 +28,53 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) -func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { +type WDAOptions struct { + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +type WDAOption func(*WDAOptions) + +func WithUDID(udid string) WDAOption { + return func(device *WDAOptions) { + device.UDID = udid + } +} + +func WithPort(port int) WDAOption { + return func(device *WDAOptions) { + device.Port = port + } +} + +func WithMjpegPort(port int) WDAOption { + return func(device *WDAOptions) { + device.MjpegPort = port + } +} + +func WithLogOn(logOn bool) WDAOption { + return func(device *WDAOptions) { + device.LogOn = logOn + } +} + +func InitWDAClient(options *WDAOptions) (*DriverExt, error) { + var deviceOptions []gwda.DeviceOption + if options.UDID != "" { + deviceOptions = append(deviceOptions, gwda.WithSerialNumber(options.UDID)) + } + if options.Port != 0 { + deviceOptions = append(deviceOptions, gwda.WithPort(options.Port)) + } + if options.MjpegPort != 0 { + deviceOptions = append(deviceOptions, gwda.WithMjpegPort(options.MjpegPort)) + } + // init wda device - targetDevice, err := gwda.NewDevice(options...) + targetDevice, err := gwda.NewDevice(deviceOptions...) if err != nil { return nil, err } @@ -58,5 +109,71 @@ func InitWDAClient(options ...gwda.DeviceOption) (*DriverExt, error) { } log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) + if options.LogOn { + err = driverExt.StartWDALog("hrp_wda_log") + if err != nil { + return nil, err + } + } + return driverExt, nil } + +type wdaResponse struct { + Value string `json:"value"` + SessionID string `json:"sessionId"` +} + +func (dExt *DriverExt) StartWDALog(identifier string) error { + log.Info().Msg("start WDA log recording") + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} + _, err := dExt.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + + return nil +} + +func (dExt *DriverExt) GetWDALog() (string, error) { + log.Info().Msg("stop WDA log recording") + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + + return reply.Value, nil +} + +func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { + // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] + postJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/gtf/automation/log", dExt.host) + log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log") + resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON)) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode) + } + + rawResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + reply := new(wdaResponse) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + return reply, nil +} diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 79d0c271..68739172 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -5,7 +5,6 @@ package uixt import ( "bytes" "encoding/base64" - "encoding/json" "fmt" "image" "io/ioutil" @@ -13,6 +12,8 @@ import ( "net/http" "strings" "time" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) var client = &http.Client{ diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index d7a1c8e9..9f4390d6 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -5,7 +5,7 @@ import ( ) func TestSwipeUntil(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) var x, y, width, height float64 diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 9c8c2e09..482c06f4 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_TapWithNumber(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -20,7 +20,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) { } func TestDriverExt_TapXY(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) err = driverExt.TapXY(0.4, 0.5, "") @@ -28,7 +28,7 @@ func TestDriverExt_TapXY(t *testing.T) { } func TestDriverExt_TapWithOCR(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) // 需要点击文字上方的图标 diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index a814b12b..9ec38aee 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -5,7 +5,7 @@ import ( ) func TestDriverExt_ForceTouch(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" @@ -21,7 +21,7 @@ func TestDriverExt_ForceTouch(t *testing.T) { } func TestDriverExt_TouchAndHold(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" diff --git a/hrp/runner.go b/hrp/runner.go index 690fc232..a34783fa 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -386,7 +386,7 @@ func (r *testCaseRunner) parseConfig() error { // init iOS WDA clients for _, iosDeviceConfig := range r.parsedConfig.IOS { - _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig.WDADevice) + _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") } diff --git a/hrp/session.go b/hrp/session.go index 35192bf4..359981b7 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -7,6 +7,8 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // SessionRunner is used to run testcase and its steps. @@ -160,5 +162,20 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary { } caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ConfigVars = r.parsedConfig.Variables + + logs := make(map[string]string) + for udid, client := range r.hrpRunner.wdaClients { + log, err := client.GetWDALog() + if err != nil { + logs[udid] = err.Error() + } else { + logs[udid] = log + } + + } + logsStr, _ := json.Marshal(logs) + caseSummary.Logs = string(logsStr) + + // caseSummary.Log return caseSummary } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 43997f05..836e1049 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -6,25 +6,26 @@ import ( "strings" "time" - "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -type IOSConfig struct { - WDADevice -} +type ( + WDAOptions = uixt.WDAOptions + WDAOption = uixt.WDAOption +) -type WDADevice struct { - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` -} +var ( + WithUDID = uixt.WithUDID + WithPort = uixt.WithPort + WithMjpegPort = uixt.WithMjpegPort + WithLogOn = uixt.WithLogOn +) type IOSStep struct { - WDADevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal MobileAction `yaml:",inline"` Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } @@ -462,36 +463,26 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error) { +func (r *HRPRunner) InitWDAClient(options *WDAOptions) (client *uiDriver, err error) { // avoid duplicate init - if device.UDID == "" && len(r.wdaClients) == 1 { + if options.UDID == "" && len(r.wdaClients) == 1 { for _, v := range r.wdaClients { return v, nil } } // avoid duplicate init - if device.UDID != "" { - if client, ok := r.wdaClients[device.UDID]; ok { + if options.UDID != "" { + if client, ok := r.wdaClients[options.UDID]; ok { return client, nil } } - var deviceOptions []gwda.DeviceOption - if device.UDID != "" { - deviceOptions = append(deviceOptions, gwda.WithSerialNumber(device.UDID)) - } - if device.Port != 0 { - deviceOptions = append(deviceOptions, gwda.WithPort(device.Port)) - } - if device.MjpegPort != 0 { - deviceOptions = append(deviceOptions, gwda.WithMjpegPort(device.MjpegPort)) - } - - driverExt, err := uixt.InitWDAClient(deviceOptions...) + driverExt, err := uixt.InitWDAClient(options) if err != nil { return nil, err } + client = &uiDriver{ DriverExt: *driverExt, } @@ -500,7 +491,7 @@ func (r *HRPRunner) InitWDAClient(device WDADevice) (client *uiDriver, err error if r.wdaClients == nil { r.wdaClients = make(map[string]*uiDriver) } - r.wdaClients[device.UDID] = client + r.wdaClients[options.UDID] = client return client, nil } @@ -515,7 +506,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro screenshots := make([]string, 0) // init wdaClient driver - wdaClient, err := s.hrpRunner.InitWDAClient(step.IOS.WDADevice) + wdaClient, err := s.hrpRunner.InitWDAClient(&step.IOS.WDAOptions) if err != nil { return } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 9f867a22..3de1f6db 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -50,7 +50,7 @@ func TestIOSSearchApp(t *testing.T) { func TestIOSAppLaunch(t *testing.T) { testCase := &TestCase{ Config: NewConfig("启动 & 关闭 App"). - SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), + SetIOS(WithPort(8100), WithMjpegPort(9100)), TestSteps: []IStep{ NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), @@ -73,7 +73,7 @@ func TestIOSAppLaunch(t *testing.T) { func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 微信直播"). - SetIOS(WDADevice{Port: 8100, MjpegPort: 9100}), + SetIOS(WithLogOn(true), WithPort(8100), WithMjpegPort(9100)), TestSteps: []IStep{ NewStep("启动微信"). IOS(). diff --git a/hrp/summary.go b/hrp/summary.go index b73522cc..a944d6a3 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO + Logs string `json:"logs,omitempty" yaml:"logs,omitempty"` // TODO Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` } From 642a74e5ea274a00c9b6bc67459126f8abd554b1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 23 Sep 2022 17:40:07 +0800 Subject: [PATCH 060/169] refactor: restructure --- hrp/config.go | 8 +- hrp/internal/uixt/android.go | 1 + hrp/internal/uixt/drag.go | 2 +- hrp/internal/uixt/ext.go | 323 +++++++++++++++++- hrp/internal/uixt/interface.go | 4 + hrp/internal/uixt/{init.go => ios.go} | 16 +- hrp/internal/uixt/ocr_test.go | 2 +- hrp/internal/uixt/opencv_on.go | 4 +- hrp/internal/uixt/swipe.go | 7 +- hrp/internal/uixt/swipe_test.go | 6 +- hrp/internal/uixt/tap.go | 8 +- hrp/internal/uixt/touch.go | 4 +- hrp/response.go | 36 ++ hrp/runner.go | 13 +- hrp/session.go | 4 +- hrp/step.go | 82 +---- hrp/step_android_ui.go | 153 ++++++--- hrp/step_ios_ui.go | 462 ++++++-------------------- 18 files changed, 624 insertions(+), 511 deletions(-) create mode 100644 hrp/internal/uixt/android.go create mode 100644 hrp/internal/uixt/interface.go rename hrp/internal/uixt/{init.go => ios.go} (93%) diff --git a/hrp/config.go b/hrp/config.go index 1884368b..99ebcbad 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -5,6 +5,7 @@ import ( "time" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) // NewConfig returns a new constructed testcase config with specified testcase name. @@ -29,7 +30,8 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"` - IOS []*WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"` + IOS []*uixt.WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"` + Android []*uixt.UIAOptions `json:"android,omitempty" yaml:"android,omitempty"` Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -100,8 +102,8 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { return c } -func (c *TConfig) SetIOS(options ...WDAOption) *TConfig { - wdaOptions := &WDAOptions{} +func (c *TConfig) SetIOS(options ...uixt.WDAOption) *TConfig { + wdaOptions := &uixt.WDAOptions{} for _, option := range options { option(wdaOptions) } diff --git a/hrp/internal/uixt/android.go b/hrp/internal/uixt/android.go new file mode 100644 index 00000000..64c90179 --- /dev/null +++ b/hrp/internal/uixt/android.go @@ -0,0 +1 @@ +package uixt diff --git a/hrp/internal/uixt/drag.go b/hrp/internal/uixt/drag.go index 0fc7fa64..510b5915 100644 --- a/hrp/internal/uixt/drag.go +++ b/hrp/internal/uixt/drag.go @@ -27,6 +27,6 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs fromX := x + width*xOffset fromY := y + height*yOffset - return dExt.WebDriver.DragFloat(fromX, fromY, toX, toY, + return dExt.Driver.DragFloat(fromX, fromY, toX, toY, gwda.WithPressDuration(pressForDuration[0])) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index d27d3815..76ffc417 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -2,6 +2,7 @@ package uixt import ( "bytes" + "encoding/json" "fmt" "image" "image/jpeg" @@ -12,12 +13,90 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) +type MobileMethod string + +const ( + AppInstall MobileMethod = "install" + AppUninstall MobileMethod = "uninstall" + AppStart MobileMethod = "app_start" + AppLaunch MobileMethod = "app_launch" // 等待 app 打开并堵塞到 app 首屏加载完成,可以传入 app 的启动参数、环境变量 + AppLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app,不堵塞等待,不可传入启动参数 + AppTerminate MobileMethod = "app_terminate" + AppStop MobileMethod = "app_stop" + CtlScreenShot MobileMethod = "screenshot" + CtlSleep MobileMethod = "sleep" + CtlStartCamera MobileMethod = "camera_start" // alias for app_launch camera + CtlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera + RecordStart MobileMethod = "record_start" + RecordStop MobileMethod = "record_stop" + + // UI validation + SelectorName string = "ui_name" + SelectorLabel string = "ui_label" + SelectorOCR string = "ui_ocr" + SelectorImage string = "ui_image" + AssertionExists string = "exists" + AssertionNotExists string = "not_exists" + + // UI handling + ACTION_Home MobileMethod = "home" + ACTION_TapXY MobileMethod = "tap_xy" + ACTION_TapByOCR MobileMethod = "tap_ocr" + ACTION_TapByCV MobileMethod = "tap_cv" + ACTION_Tap MobileMethod = "tap" + ACTION_DoubleTapXY MobileMethod = "double_tap_xy" + ACTION_DoubleTap MobileMethod = "double_tap" + ACTION_Swipe MobileMethod = "swipe" + ACTION_Input MobileMethod = "input" + + // custom actions + ACTION_SwipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap + ACTION_SwipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap +) + +type MobileAction struct { + Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"` + Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` + + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log + MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action + IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found +} + +type ActionOption func(o *MobileAction) + +func WithIdentifier(identifier string) ActionOption { + return func(o *MobileAction) { + o.Identifier = identifier + } +} + +func WithMaxRetryTimes(maxRetryTimes int) ActionOption { + return func(o *MobileAction) { + o.MaxRetryTimes = maxRetryTimes + } +} + +func WithTimeout(timeout int) ActionOption { + return func(o *MobileAction) { + o.Timeout = timeout + } +} + +func WithIgnoreNotFoundError(ignoreError bool) ActionOption { + return func(o *MobileAction) { + o.IgnoreNotFoundError = ignoreError + } +} + // TemplateMatchMode is the type of the template matching operation. type TemplateMatchMode int @@ -41,27 +120,29 @@ func WithThreshold(threshold float64) CVOption { } type DriverExt struct { - gwda.WebDriver + Driver gwda.WebDriver windowSize gwda.Size frame *bytes.Buffer doneMjpegStream chan bool scale float64 host string + StartTime time.Time // used to associate screenshots name + ScreenShots []string // save screenshots path CVArgs } func extend(driver gwda.WebDriver) (dExt *DriverExt, err error) { - dExt = &DriverExt{WebDriver: driver} + dExt = &DriverExt{Driver: driver} dExt.doneMjpegStream = make(chan bool, 1) // get device window size - dExt.windowSize, err = dExt.WebDriver.WindowSize() + dExt.windowSize, err = dExt.Driver.WindowSize() if err != nil { return nil, errors.Wrap(err, "failed to get windows size") } - if dExt.scale, err = dExt.Scale(); err != nil { + if dExt.scale, err = dExt.Driver.Scale(); err != nil { return nil, err } @@ -129,7 +210,7 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { if dExt.frame != nil { return dExt.frame, nil } - if raw, err = dExt.WebDriver.Screenshot(); err != nil { + if raw, err = dExt.Driver.Screenshot(); err != nil { log.Error().Err(err).Msgf("screenshot failed: %v", err) return nil, err } @@ -206,7 +287,7 @@ func (dExt *DriverExt) FindUIElement(param string) (ele gwda.WebElement, err err } } - return dExt.WebDriver.FindElement(selector) + return dExt.Driver.FindElement(selector) } func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height float64, err error) { @@ -225,18 +306,18 @@ func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, } func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error { - return dExt.PerformAppiumTouchActions(touchActions) + return dExt.Driver.PerformAppiumTouchActions(touchActions) } func (dExt *DriverExt) PerformActions(actions *gwda.W3CActions) error { - return dExt.PerformW3CActions(actions) + return dExt.Driver.PerformW3CActions(actions) } func (dExt *DriverExt) IsNameExist(name string) bool { selector := gwda.BySelector{ LinkText: gwda.NewElementAttribute().WithName(name), } - _, err := dExt.FindElement(selector) + _, err := dExt.Driver.FindElement(selector) return err == nil } @@ -244,7 +325,7 @@ func (dExt *DriverExt) IsLabelExist(label string) bool { selector := gwda.BySelector{ LinkText: gwda.NewElementAttribute().WithLabel(label), } - _, err := dExt.FindElement(selector) + _, err := dExt.Driver.FindElement(selector) return err == nil } @@ -257,3 +338,225 @@ func (dExt *DriverExt) IsImageExist(text string) bool { _, _, _, _, err := dExt.FindImageRectInUIKit(text) return err == nil } + +var errActionNotImplemented = errors.New("UI action not implemented") + +func (dExt *DriverExt) DoAction(action MobileAction) error { + log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") + + switch action.Method { + case AppInstall: + // TODO + return errActionNotImplemented + case AppLaunch: + if bundleId, ok := action.Params.(string); ok { + return dExt.Driver.AppLaunch(bundleId) + } + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + AppLaunch, action.Params) + case AppLaunchUnattached: + if bundleId, ok := action.Params.(string); ok { + return dExt.Driver.AppLaunchUnattached(bundleId) + } + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + AppLaunchUnattached, action.Params) + case ACTION_SwipeToTapApp: + if appName, ok := action.Params.(string); ok { + var x, y, width, height float64 + findApp := func(d *DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(appName) + return err + } + foundAppAction := func(d *DriverExt) error { + // click app to launch + return d.Driver.TapFloat(x+width*0.5, y+height*0.5-20) + } + + // go to home screen + if err := dExt.Driver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + dExt.SwipeRight() + } + + // default to retry 5 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 5 + } + // swipe next screen until app found + return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes) + } + return fmt.Errorf("invalid %s params, should be app name(string), got %v", + ACTION_SwipeToTapApp, action.Params) + case ACTION_SwipeToTapText: + if text, ok := action.Params.(string); ok { + var x, y, width, height float64 + findText := func(d *DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(text) + return err + } + foundTextAction := func(d *DriverExt) error { + // tap text + return d.Driver.TapFloat(x+width*0.5, y+height*0.5) + } + + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + // swipe until live room found + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + } + return fmt.Errorf("invalid %s params, should be app text(string), got %v", + ACTION_SwipeToTapText, action.Params) + case AppTerminate: + if bundleId, ok := action.Params.(string); ok { + success, err := dExt.Driver.AppTerminate(bundleId) + if err != nil { + return errors.Wrap(err, "failed to terminate app") + } + if !success { + log.Warn().Str("bundleId", bundleId).Msg("app was not running") + } + return nil + } + return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) + case ACTION_Home: + return dExt.Driver.Homescreen() + case ACTION_TapXY: + if location, ok := action.Params.([]float64); ok { + // relative x,y of window size: [0.5, 0.5] + if len(location) != 2 { + return fmt.Errorf("invalid tap location params: %v", location) + } + return dExt.TapXY(location[0], location[1], action.Identifier) + } + return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) + case ACTION_Tap: + if param, ok := action.Params.(string); ok { + return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError) + } + return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) + case ACTION_TapByOCR: + if ocrText, ok := action.Params.(string); ok { + return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError) + } + return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) + case ACTION_TapByCV: + if imagePath, ok := action.Params.(string); ok { + return dExt.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError) + } + return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params) + case ACTION_DoubleTapXY: + if location, ok := action.Params.([]float64); ok { + // relative x,y of window size: [0.5, 0.5] + if len(location) != 2 { + return fmt.Errorf("invalid tap location params: %v", location) + } + return dExt.DoubleTapXY(location[0], location[1]) + } + return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params) + case ACTION_DoubleTap: + if param, ok := action.Params.(string); ok { + return dExt.DoubleTap(param) + } + return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params) + case ACTION_Swipe: + if positions, ok := action.Params.([]float64); ok { + // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] + if len(positions) != 4 { + return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions) + } + return dExt.SwipeRelative( + positions[0], positions[1], positions[2], positions[3], action.Identifier) + } + if direction, ok := action.Params.(string); ok { + return dExt.SwipeTo(direction, action.Identifier) + } + return fmt.Errorf("invalid %s params: %v", ACTION_Swipe, action.Params) + case ACTION_Input: + // input text on current active element + // append \n to send text with enter + // send \b\b\b to delete 3 chars + param := fmt.Sprintf("%v", action.Params) + return dExt.Driver.SendKeys(param) + case CtlSleep: + if param, ok := action.Params.(json.Number); ok { + seconds, _ := param.Float64() + time.Sleep(time.Duration(seconds*1000) * time.Millisecond) + return nil + } else if param, ok := action.Params.(float64); ok { + time.Sleep(time.Duration(param*1000) * time.Millisecond) + return nil + } + return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params) + case CtlScreenShot: + // take snapshot + log.Info().Msg("take snapshot for current screen") + screenshotPath, err := dExt.ScreenShot(fmt.Sprintf("%d_screenshot_%d", + dExt.StartTime.Unix(), time.Now().Unix())) + if err != nil { + return errors.Wrap(err, "take screenshot failed") + } + log.Info().Str("path", screenshotPath).Msg("take screenshot") + dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath) + return err + case CtlStartCamera: + // start camera, alias for app_launch com.apple.camera + return dExt.Driver.AppLaunch("com.apple.camera") + case CtlStopCamera: + // stop camera, alias for app_terminate com.apple.camera + success, err := dExt.Driver.AppTerminate("com.apple.camera") + if err != nil { + return errors.Wrap(err, "failed to terminate camera") + } + if !success { + log.Warn().Msg("camera was not running") + } + return nil + } + return nil +} + +func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool { + var exists bool + if assert == AssertionExists { + exists = true + } else { + exists = false + } + var result bool + switch check { + case SelectorName: + result = (dExt.IsNameExist(expected) == exists) + case SelectorLabel: + result = (dExt.IsLabelExist(expected) == exists) + case SelectorOCR: + result = (dExt.IsOCRExist(expected) == exists) + case SelectorImage: + result = (dExt.IsImageExist(expected) == exists) + } + + if !result { + if message == nil { + message = []string{""} + } + log.Error(). + Str("assert", assert). + Str("expect", expected). + Str("msg", message[0]). + Msg("validate UI failed") + return false + } + + log.Info(). + Str("assert", assert). + Str("expect", expected). + Msg("validate UI success") + return true +} diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go new file mode 100644 index 00000000..b788c6e1 --- /dev/null +++ b/hrp/internal/uixt/interface.go @@ -0,0 +1,4 @@ +package uixt + +type WebDriver interface { +} diff --git a/hrp/internal/uixt/init.go b/hrp/internal/uixt/ios.go similarity index 93% rename from hrp/internal/uixt/init.go rename to hrp/internal/uixt/ios.go index 4c6b39e7..d79caf27 100644 --- a/hrp/internal/uixt/init.go +++ b/hrp/internal/uixt/ios.go @@ -28,6 +28,10 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) +type Options interface { + UUID() string +} + type WDAOptions struct { UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` @@ -35,6 +39,10 @@ type WDAOptions struct { LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } +func (o WDAOptions) UUID() string { + return o.UDID +} + type WDAOption func(*WDAOptions) func WithUDID(udid string) WDAOption { @@ -100,7 +108,7 @@ func InitWDAClient(options *WDAOptions) (*DriverExt, error) { if err != nil { return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") } - settings, err := driverExt.SetAppiumSettings(map[string]interface{}{ + settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ "snapshotMaxDepth": snapshotMaxDepth, "acceptAlertButtonSelector": acceptAlertButtonSelector, }) @@ -111,7 +119,7 @@ func InitWDAClient(options *WDAOptions) (*DriverExt, error) { driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) if options.LogOn { - err = driverExt.StartWDALog("hrp_wda_log") + err = driverExt.StartLogRecording("hrp_wda_log") if err != nil { return nil, err } @@ -125,7 +133,7 @@ type wdaResponse struct { SessionID string `json:"sessionId"` } -func (dExt *DriverExt) StartWDALog(identifier string) error { +func (dExt *DriverExt) StartLogRecording(identifier string) error { log.Info().Msg("start WDA log recording") data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} _, err := dExt.triggerWDALog(data) @@ -136,7 +144,7 @@ func (dExt *DriverExt) StartWDALog(identifier string) error { return nil } -func (dExt *DriverExt) GetWDALog() (string, error) { +func (dExt *DriverExt) GetLogs() (string, error) { log.Info().Msg("stop WDA log recording") data := map[string]interface{}{"action": "stop"} reply, err := dExt.triggerWDALog(data) diff --git a/hrp/internal/uixt/ocr_test.go b/hrp/internal/uixt/ocr_test.go index 928b39a2..922c9e0b 100644 --- a/hrp/internal/uixt/ocr_test.go +++ b/hrp/internal/uixt/ocr_test.go @@ -14,5 +14,5 @@ func TestDriverExtOCR(t *testing.T) { checkErr(t, err) t.Logf("x: %v, y: %v, width: %v, height: %v", x, y, width, height) - driverExt.WebDriver.TapFloat(x+width*0.5, y+height*0.5-20) + driverExt.Driver.TapFloat(x+width*0.5, y+height*0.5-20) } diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 81afe225..326e9277 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -69,7 +69,7 @@ func (dExt *DriverExt) Debug(dm DebugMode) { func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) { newExt = new(DriverExt) - newExt.WebDriver = dExt.WebDriver + newExt.Driver = dExt.Driver newExt.scale = dExt.scale newExt.matchMode = dExt.matchMode newExt.threshold = threshold @@ -78,7 +78,7 @@ func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) { newExt = new(DriverExt) - newExt.WebDriver = dExt.WebDriver + newExt.Driver = dExt.Driver newExt.scale = dExt.scale newExt.matchMode = matchMode newExt.threshold = dExt.threshold diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 5454682a..4d29e05b 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -2,6 +2,7 @@ package uixt import ( "fmt" + "time" "github.com/electricbubble/gwda" "github.com/rs/zerolog/log" @@ -32,9 +33,9 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier "enable": true, "data": identifier[0], }) - dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY, option) + dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, option) } - return dExt.WebDriver.SwipeFloat(fromX, fromY, toX, toY) + return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY) } func (dExt *DriverExt) SwipeTo(direction string, identifier ...string) (err error) { @@ -82,6 +83,8 @@ func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, act if err := dExt.SwipeTo(direction); err != nil { log.Error().Err(err).Msgf("swipe %s failed", direction) } + // wait for swipe done + time.Sleep(500 * time.Millisecond) } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index 9f4390d6..cb25b76c 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -16,10 +16,10 @@ func TestSwipeUntil(t *testing.T) { } foundAppAction := func(d *DriverExt) error { // click app, launch douyin - return d.TapFloat(x+width*0.5, y+height*0.5-20) + return d.Driver.TapFloat(x+width*0.5, y+height*0.5-20) } - driverExt.Homescreen() + driverExt.Driver.Homescreen() // swipe to first screen for i := 0; i < 5; i++ { @@ -37,7 +37,7 @@ func TestSwipeUntil(t *testing.T) { } foundLiveAction := func(d *DriverExt) error { // enter live room - return d.TapFloat(x+width*0.5, y+height*0.5) + return d.Driver.TapFloat(x+width*0.5, y+height*0.5) } // swipe until live room found diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index 9a522825..ac6c51fc 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -12,9 +12,9 @@ func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { "enable": true, "data": identifier, }) - return dExt.WebDriver.TapFloat(x, y, option) + return dExt.Driver.TapFloat(x, y, option) } - return dExt.WebDriver.TapFloat(x, y) + return dExt.Driver.TapFloat(x, y) } func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { @@ -83,7 +83,7 @@ func (dExt *DriverExt) DoubleTapXY(x, y float64) error { x = x * float64(dExt.windowSize.Width) y = y * float64(dExt.windowSize.Height) - return dExt.WebDriver.DoubleTapFloat(x, y) + return dExt.Driver.DoubleTapFloat(x, y) } func (dExt *DriverExt) DoubleTap(param string) (err error) { @@ -102,7 +102,7 @@ func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64) ( return err } - return dExt.WebDriver.DoubleTapFloat(x+width*xOffset, y+height*yOffset) + return dExt.Driver.DoubleTapFloat(x+width*xOffset, y+height*yOffset) } // TapWithNumber sends one or more taps diff --git a/hrp/internal/uixt/touch.go b/hrp/internal/uixt/touch.go index 6c06ae81..fe455507 100644 --- a/hrp/internal/uixt/touch.go +++ b/hrp/internal/uixt/touch.go @@ -13,7 +13,7 @@ func (dExt *DriverExt) ForceTouchOffset(pathname string, pressure, xOffset, yOff return err } - return dExt.ForceTouchFloat(x+width*xOffset, y+height*yOffset, pressure, duration[0]) + return dExt.Driver.ForceTouchFloat(x+width*xOffset, y+height*yOffset, pressure, duration[0]) } func (dExt *DriverExt) TouchAndHold(pathname string, duration ...float64) (err error) { @@ -29,5 +29,5 @@ func (dExt *DriverExt) TouchAndHoldOffset(pathname string, xOffset, yOffset floa return err } - return dExt.TouchAndHoldFloat(x+width*xOffset, y+height*yOffset, duration[0]) + return dExt.Driver.TouchAndHoldFloat(x+width*xOffset, y+height*yOffset, duration[0]) } diff --git a/hrp/response.go b/hrp/response.go index 42c7e9e1..33fe48e7 100644 --- a/hrp/response.go +++ b/hrp/response.go @@ -16,6 +16,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) var fieldTags = []string{"proto", "status_code", "headers", "cookies", "body", textExtractorSubRegexp} @@ -272,3 +273,38 @@ func (v *responseObject) searchRegexp(expr string) interface{} { log.Error().Str("expr", expr).Msg("search regexp failed") return expr } + +func validateUI(ud *uixt.DriverExt, iValidators []interface{}) (validateResults []*ValidationResult, err error) { + for _, iValidator := range iValidators { + validator, ok := iValidator.(Validator) + if !ok { + return nil, errors.New("validator type error") + } + + validataResult := &ValidationResult{ + Validator: validator, + CheckResult: "fail", + } + + // parse check value + if !strings.HasPrefix(validator.Check, "ui_") { + validataResult.CheckResult = "skip" + log.Warn().Interface("validator", validator).Msg("skip validator") + validateResults = append(validateResults, validataResult) + continue + } + + expected, ok := validator.Expect.(string) + if !ok { + return nil, errors.New("validator expect should be string") + } + + if !ud.DoValidation(validator.Check, validator.Assert, expected, validator.Message) { + return validateResults, errors.New("step validation failed") + } + + validataResult.CheckResult = "pass" + validateResults = append(validateResults, validataResult) + } + return validateResults, nil +} diff --git a/hrp/runner.go b/hrp/runner.go index a34783fa..dfb02551 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -20,6 +20,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) // Run starts to run API test with default configs. @@ -71,7 +72,7 @@ type HRPRunner struct { httpClient *http.Client http2Client *http.Client wsDialer *websocket.Dialer - wdaClients map[string]*uiDriver // wda client used for iOS UI automation, key is udid + uiClients map[string]*uixt.DriverExt // UI automation clients for iOS and Android, key is udid/serial } // SetClientTransport configures transport of http client for high concurrency load testing @@ -384,13 +385,19 @@ func (r *testCaseRunner) parseConfig() error { } r.parametersIterator = parametersIterator - // init iOS WDA clients + // init iOS/Android clients for _, iosDeviceConfig := range r.parsedConfig.IOS { - _, err := r.hrpRunner.InitWDAClient(iosDeviceConfig) + _, err := r.hrpRunner.initUIClient(iosDeviceConfig) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") } } + for _, androidDeviceConfig := range r.parsedConfig.Android { + _, err := r.hrpRunner.initUIClient(androidDeviceConfig) + if err != nil { + return errors.Wrap(err, "init Android UIAutomator client failed") + } + } return nil } diff --git a/hrp/session.go b/hrp/session.go index 359981b7..d640f2f2 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -164,8 +164,8 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary { caseSummary.InOut.ConfigVars = r.parsedConfig.Variables logs := make(map[string]string) - for udid, client := range r.hrpRunner.wdaClients { - log, err := client.GetWDALog() + for udid, client := range r.hrpRunner.uiClients { + log, err := client.GetLogs() if err != nil { logs[udid] = err.Error() } else { diff --git a/hrp/step.go b/hrp/step.go index 02a43262..f3585e40 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -1,5 +1,7 @@ package hrp +import "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + type StepType string const ( @@ -14,83 +16,13 @@ const ( stepTypeIOS StepType = "ios" ) -type MobileMethod string - -const ( - appInstall MobileMethod = "install" - appUninstall MobileMethod = "uninstall" - appStart MobileMethod = "app_start" - appLaunch MobileMethod = "app_launch" // 等待 app 打开并堵塞到 app 首屏加载完成,可以传入 app 的启动参数、环境变量 - appLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app,不堵塞等待,不可传入启动参数 - appTerminate MobileMethod = "app_terminate" - appStop MobileMethod = "app_stop" - ctlScreenShot MobileMethod = "screenshot" - ctlSleep MobileMethod = "sleep" - ctlStartCamera MobileMethod = "camera_start" // alias for app_launch camera - ctlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera - recordStart MobileMethod = "record_start" - recordStop MobileMethod = "record_stop" - - // UI handling - uiHome MobileMethod = "home" - uiTapXY MobileMethod = "tap_xy" - uiTapByOCR MobileMethod = "tap_ocr" - uiTapByCV MobileMethod = "tap_cv" - uiTap MobileMethod = "tap" - uiDoubleTapXY MobileMethod = "double_tap_xy" - uiDoubleTap MobileMethod = "double_tap" - uiSwipe MobileMethod = "swipe" - uiInput MobileMethod = "input" - - // UI validation - uiSelectorName string = "ui_name" - uiSelectorLabel string = "ui_label" - uiSelectorOCR string = "ui_ocr" - uiSelectorImage string = "ui_image" - assertionExists string = "exists" - assertionNotExists string = "not_exists" - - // custom actions - swipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap - swipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap +var ( + WithIdentifier = uixt.WithIdentifier + WithMaxRetryTimes = uixt.WithMaxRetryTimes + WithTimeout = uixt.WithTimeout + WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError ) -type MobileAction struct { - Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"` - Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` - - Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log - MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times - Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action - IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found -} - -type ActionOption func(o *MobileAction) - -func WithIdentifier(identifier string) ActionOption { - return func(o *MobileAction) { - o.Identifier = identifier - } -} - -func WithMaxRetryTimes(maxRetryTimes int) ActionOption { - return func(o *MobileAction) { - o.MaxRetryTimes = maxRetryTimes - } -} - -func WithTimeout(timeout int) ActionOption { - return func(o *MobileAction) { - o.Timeout = timeout - } -} - -func WithIgnoreNotFoundError(ignoreError bool) ActionOption { - return func(o *MobileAction) { - o.IgnoreNotFoundError = ignoreError - } -} - type StepResult struct { Name string `json:"name" yaml:"name"` // step name StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index abc1a9be..b6f44c97 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -1,11 +1,17 @@ package hrp -import "fmt" +import ( + "fmt" + "time" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/rs/zerolog/log" +) type AndroidStep struct { - MobileAction - Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` - Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` + uixt.UIAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + uixt.MobileAction + Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } // StepAndroid implements IStep interface. @@ -14,117 +20,117 @@ type StepAndroid struct { } func (s *StepAndroid) Serial(serial string) *StepAndroid { - s.step.Android.Serial = serial + s.step.Android.SerialNumber = serial return &StepAndroid{step: s.step} } func (s *StepAndroid) InstallApp(path string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: appInstall, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppInstall, Params: path, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: appStart, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppStart, Params: activity, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) StartCamera() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: ctlStartCamera, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlStartCamera, Params: nil, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) StopCamera() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: ctlStopCamera, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlStopCamera, Params: nil, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) StartRecording() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: recordStart, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.RecordStart, Params: nil, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) StopRecording() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: recordStop, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.RecordStop, Params: nil, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) Tap(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiTap, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Tap, Params: params, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) DoubleTap(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiDoubleTap, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_DoubleTap, Params: params, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) Swipe(sx, sy, ex, ey int) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiSwipe, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: []int{sx, sy, ex, ey}, }) return &StepAndroid{step: s.step} } func (s *StepAndroid) SwipeUp() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiSwipe, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "up", }) return &StepAndroid{step: s.step} } func (s *StepAndroid) SwipeDown() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiSwipe, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "down", }) return &StepAndroid{step: s.step} } func (s *StepAndroid) SwipeLeft() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiSwipe, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "left", }) return &StepAndroid{step: s.step} } func (s *StepAndroid) SwipeRight() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiSwipe, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "right", }) return &StepAndroid{step: s.step} } func (s *StepAndroid) Input(text string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, MobileAction{ - Method: uiInput, + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Input, Params: text, }) return &StepAndroid{step: s.step} @@ -160,8 +166,8 @@ type StepAndroidValidation struct { func (s *StepAndroidValidation) AssertNameExists(expectedName string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: uiSelectorName, - Assert: assertionExists, + Check: uixt.SelectorName, + Assert: uixt.AssertionExists, Expect: expectedName, } if len(msg) > 0 { @@ -175,8 +181,8 @@ func (s *StepAndroidValidation) AssertNameExists(expectedName string, msg ...str func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ...string) *StepAndroidValidation { v := Validator{ - Check: uiSelectorName, - Assert: assertionNotExists, + Check: uixt.SelectorName, + Assert: uixt.AssertionNotExists, Expect: expectedName, } if len(msg) > 0 { @@ -204,12 +210,83 @@ func (s *StepAndroidValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepAndroid(r, s.step) } -func runStepAndroid(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { +func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, StepType: stepTypeAndroid, Success: false, ContentSize: 0, } + screenshots := make([]string, 0) + + // init uiaClient driver + uiaClient, err := s.hrpRunner.initUIClient(&step.Android.UIAOptions) + if err != nil { + return + } + uiaClient.StartTime = s.startTime + + defer func() { + attachments := make(map[string]interface{}) + if err != nil { + attachments["error"] = err.Error() + } + + // save attachments + screenshots = append(screenshots, uiaClient.ScreenShots...) + attachments["screenshots"] = screenshots + stepResult.Attachments = attachments + + // update summary + s.summary.Records = append(s.summary.Records, stepResult) + s.summary.Stat.Total += 1 + if stepResult.Success { + s.summary.Stat.Successes += 1 + } else { + s.summary.Stat.Failures += 1 + // update summary result to failed + s.summary.Success = false + } + }() + + // prepare actions + var actions []uixt.MobileAction + if step.Android.Actions == nil { + actions = []uixt.MobileAction{ + { + Method: step.Android.Method, + Params: step.Android.Params, + }, + } + } else { + actions = step.Android.Actions + } + + // run actions + for _, action := range actions { + if err := uiaClient.DoAction(action); err != nil { + return stepResult, err + } + } + + // take snapshot + screenshotPath, err := uiaClient.ScreenShot( + fmt.Sprintf("%d_validate_%d", uiaClient.StartTime.Unix(), time.Now().Unix())) + if err != nil { + log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") + } else { + log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") + screenshots = append(screenshots, screenshotPath) + } + + // validate + validateResults, err := validateUI(uiaClient, step.Validators) + if err != nil { + return + } + sessionData := newSessionData() + sessionData.Validators = validateResults + stepResult.Data = sessionData + stepResult.Success = true return stepResult, nil } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 836e1049..7a6a4327 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -1,22 +1,14 @@ package hrp import ( - "encoding/json" "fmt" - "strings" "time" - "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -type ( - WDAOptions = uixt.WDAOptions - WDAOption = uixt.WDAOption -) - var ( WithUDID = uixt.WithUDID WithPort = uixt.WithPort @@ -25,9 +17,9 @@ var ( ) type IOSStep struct { - WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal - MobileAction `yaml:",inline"` - Actions []MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` + uixt.WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + uixt.MobileAction `yaml:",inline"` + Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } // StepIOS implements IStep interface. @@ -41,49 +33,49 @@ func (s *StepIOS) UDID(udid string) *StepIOS { } func (s *StepIOS) InstallApp(path string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: appInstall, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.AppInstall, Params: path, }) return s } func (s *StepIOS) AppLaunch(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: appLaunch, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.AppLaunch, Params: bundleId, }) return s } func (s *StepIOS) AppLaunchUnattached(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: appLaunchUnattached, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.AppLaunchUnattached, Params: bundleId, }) return s } func (s *StepIOS) AppTerminate(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: appTerminate, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.AppTerminate, Params: bundleId, }) return s } func (s *StepIOS) Home() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiHome, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Home, Params: nil, }) return &StepIOS{step: s.step} } // TapXY taps the point {X,Y}, X & Y is percentage of coordinates -func (s *StepIOS) TapXY(x, y float64, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiTapXY, +func (s *StepIOS) TapXY(x, y float64, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapXY, Params: []float64{x, y}, } for _, option := range options { @@ -94,9 +86,9 @@ func (s *StepIOS) TapXY(x, y float64, options ...ActionOption) *StepIOS { } // Tap taps on the target element -func (s *StepIOS) Tap(params string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiTap, +func (s *StepIOS) Tap(params string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Tap, Params: params, } for _, option := range options { @@ -107,9 +99,9 @@ func (s *StepIOS) Tap(params string, options ...ActionOption) *StepIOS { } // Tap taps on the target element by OCR recognition -func (s *StepIOS) TapByOCR(ocrText string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiTapByOCR, +func (s *StepIOS) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByOCR, Params: ocrText, } for _, option := range options { @@ -120,9 +112,9 @@ func (s *StepIOS) TapByOCR(ocrText string, options ...ActionOption) *StepIOS { } // Tap taps on the target element by CV recognition -func (s *StepIOS) TapByCV(imagePath string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiTapByCV, +func (s *StepIOS) TapByCV(imagePath string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByCV, Params: imagePath, } for _, option := range options { @@ -134,16 +126,16 @@ func (s *StepIOS) TapByCV(imagePath string, options ...ActionOption) *StepIOS { // DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates func (s *StepIOS) DoubleTapXY(x, y float64) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiDoubleTapXY, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.ACTION_DoubleTapXY, Params: []float64{x, y}, }) return &StepIOS{step: s.step} } -func (s *StepIOS) DoubleTap(params string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiDoubleTap, +func (s *StepIOS) DoubleTap(params string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_DoubleTap, Params: params, } for _, option := range options { @@ -153,9 +145,9 @@ func (s *StepIOS) DoubleTap(params string, options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) Swipe(sx, sy, ex, ey int, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiSwipe, +func (s *StepIOS) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: []int{sx, sy, ex, ey}, } for _, option := range options { @@ -165,9 +157,9 @@ func (s *StepIOS) Swipe(sx, sy, ex, ey int, options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeUp(options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiSwipe, +func (s *StepIOS) SwipeUp(options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "up", } for _, option := range options { @@ -177,9 +169,9 @@ func (s *StepIOS) SwipeUp(options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeDown(options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiSwipe, +func (s *StepIOS) SwipeDown(options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "down", } for _, option := range options { @@ -189,9 +181,9 @@ func (s *StepIOS) SwipeDown(options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeLeft(options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiSwipe, +func (s *StepIOS) SwipeLeft(options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "left", } for _, option := range options { @@ -201,9 +193,9 @@ func (s *StepIOS) SwipeLeft(options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeRight(options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: uiSwipe, +func (s *StepIOS) SwipeRight(options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_Swipe, Params: "right", } for _, option := range options { @@ -213,9 +205,9 @@ func (s *StepIOS) SwipeRight(options ...ActionOption) *StepIOS { return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeToTapApp(appName string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: swipeToTapApp, +func (s *StepIOS) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapApp, Params: appName, } for _, option := range options { @@ -225,9 +217,9 @@ func (s *StepIOS) SwipeToTapApp(appName string, options ...ActionOption) *StepIO return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeToTapText(text string, options ...ActionOption) *StepIOS { - action := MobileAction{ - Method: swipeToTapText, +func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapText, Params: text, } for _, option := range options { @@ -238,8 +230,8 @@ func (s *StepIOS) SwipeToTapText(text string, options ...ActionOption) *StepIOS } func (s *StepIOS) Input(text string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: uiInput, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Input, Params: text, }) return &StepIOS{step: s.step} @@ -268,32 +260,32 @@ func (s *StepIOS) Times(n int) *StepIOS { // Sleep specify sleep seconds after last action func (s *StepIOS) Sleep(n float64) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: ctlSleep, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.CtlSleep, Params: n, }) return &StepIOS{step: s.step} } func (s *StepIOS) ScreenShot() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: ctlScreenShot, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.CtlScreenShot, Params: nil, }) return &StepIOS{step: s.step} } func (s *StepIOS) StartCamera() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: ctlStartCamera, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.CtlStartCamera, Params: nil, }) return &StepIOS{step: s.step} } func (s *StepIOS) StopCamera() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ - Method: ctlStopCamera, + s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ + Method: uixt.CtlStopCamera, Params: nil, }) return &StepIOS{step: s.step} @@ -329,8 +321,8 @@ type StepIOSValidation struct { func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorName, - Assert: assertionExists, + Check: uixt.SelectorName, + Assert: uixt.AssertionExists, Expect: expectedName, } if len(msg) > 0 { @@ -344,8 +336,8 @@ func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorName, - Assert: assertionNotExists, + Check: uixt.SelectorName, + Assert: uixt.AssertionNotExists, Expect: expectedName, } if len(msg) > 0 { @@ -359,8 +351,8 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorLabel, - Assert: assertionExists, + Check: uixt.SelectorLabel, + Assert: uixt.AssertionExists, Expect: expectedLabel, } if len(msg) > 0 { @@ -374,8 +366,8 @@ func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...strin func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorLabel, - Assert: assertionNotExists, + Check: uixt.SelectorLabel, + Assert: uixt.AssertionNotExists, Expect: expectedLabel, } if len(msg) > 0 { @@ -389,8 +381,8 @@ func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...st func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorOCR, - Assert: assertionExists, + Check: uixt.SelectorOCR, + Assert: uixt.AssertionExists, Expect: expectedText, } if len(msg) > 0 { @@ -404,8 +396,8 @@ func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorOCR, - Assert: assertionNotExists, + Check: uixt.SelectorOCR, + Assert: uixt.AssertionNotExists, Expect: expectedText, } if len(msg) > 0 { @@ -419,8 +411,8 @@ func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...strin func (s *StepIOSValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorImage, - Assert: assertionExists, + Check: uixt.SelectorImage, + Assert: uixt.AssertionExists, Expect: expectedImagePath, } if len(msg) > 0 { @@ -434,8 +426,8 @@ func (s *StepIOSValidation) AssertImageExists(expectedImagePath string, msg ...s func (s *StepIOSValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepIOSValidation { v := Validator{ - Check: uiSelectorImage, - Assert: assertionNotExists, + Check: uixt.SelectorImage, + Assert: uixt.AssertionNotExists, Expect: expectedImagePath, } if len(msg) > 0 { @@ -463,35 +455,37 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) InitWDAClient(options *WDAOptions) (client *uiDriver, err error) { +func (r *HRPRunner) initUIClient(options uixt.Options) (client *uixt.DriverExt, err error) { + uuid := options.UUID() + // avoid duplicate init - if options.UDID == "" && len(r.wdaClients) == 1 { - for _, v := range r.wdaClients { + if uuid == "" && len(r.uiClients) == 1 { + for _, v := range r.uiClients { return v, nil } } // avoid duplicate init - if options.UDID != "" { - if client, ok := r.wdaClients[options.UDID]; ok { + if uuid != "" { + if client, ok := r.uiClients[uuid]; ok { return client, nil } } - driverExt, err := uixt.InitWDAClient(options) + if wdaOptions, ok := options.(*uixt.WDAOptions); ok { + client, err = uixt.InitWDAClient(wdaOptions) + } else if uiaOptions, ok := options.(*uixt.UIAOptions); ok { + client, err = uixt.InitUIAClient(uiaOptions) + } if err != nil { return nil, err } - client = &uiDriver{ - DriverExt: *driverExt, - } - // cache wda client - if r.wdaClients == nil { - r.wdaClients = make(map[string]*uiDriver) + if r.uiClients == nil { + r.uiClients = make(map[string]*uixt.DriverExt) } - r.wdaClients[options.UDID] = client + r.uiClients[uuid] = client return client, nil } @@ -506,11 +500,11 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro screenshots := make([]string, 0) // init wdaClient driver - wdaClient, err := s.hrpRunner.InitWDAClient(&step.IOS.WDAOptions) + wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.WDAOptions) if err != nil { return } - wdaClient.startTime = s.startTime + wdaClient.StartTime = s.startTime defer func() { attachments := make(map[string]interface{}) @@ -519,7 +513,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // save attachments - screenshots = append(screenshots, wdaClient.screenShots...) + screenshots = append(screenshots, wdaClient.ScreenShots...) attachments["screenshots"] = screenshots stepResult.Attachments = attachments @@ -536,9 +530,9 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro }() // prepare actions - var actions []MobileAction + var actions []uixt.MobileAction if step.IOS.Actions == nil { - actions = []MobileAction{ + actions = []uixt.MobileAction{ { Method: step.IOS.Method, Params: step.IOS.Params, @@ -550,14 +544,14 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro // run actions for _, action := range actions { - if err := wdaClient.doAction(action); err != nil { + if err := wdaClient.DoAction(action); err != nil { return stepResult, err } } // take snapshot - screenshotPath, err := wdaClient.DriverExt.ScreenShot( - fmt.Sprintf("%d_validate_%d", wdaClient.startTime.Unix(), time.Now().Unix())) + screenshotPath, err := wdaClient.ScreenShot( + fmt.Sprintf("%d_validate_%d", wdaClient.StartTime.Unix(), time.Now().Unix())) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } else { @@ -566,7 +560,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // validate - validateResults, err := wdaClient.doValidation(step.Validators) + validateResults, err := validateUI(wdaClient, step.Validators) if err != nil { return } @@ -576,257 +570,3 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro stepResult.Success = true return stepResult, nil } - -var errActionNotImplemented = errors.New("UI action not implemented") - -type uiDriver struct { - uixt.DriverExt - - startTime time.Time // used to associate screenshots name - screenShots []string // save screenshots path -} - -func (ud *uiDriver) doAction(action MobileAction) error { - log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") - - switch action.Method { - case appInstall: - // TODO - return errActionNotImplemented - case appLaunch: - if bundleId, ok := action.Params.(string); ok { - return ud.AppLaunch(bundleId) - } - return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", - appLaunch, action.Params) - case appLaunchUnattached: - if bundleId, ok := action.Params.(string); ok { - return ud.AppLaunchUnattached(bundleId) - } - return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", - appLaunchUnattached, action.Params) - case swipeToTapApp: - if appName, ok := action.Params.(string); ok { - var x, y, width, height float64 - findApp := func(d *uixt.DriverExt) error { - var err error - x, y, width, height, err = d.FindTextByOCR(appName) - return err - } - foundAppAction := func(d *uixt.DriverExt) error { - // click app to launch - return d.TapFloat(x+width*0.5, y+height*0.5-20) - } - - // go to home screen - if err := ud.WebDriver.Homescreen(); err != nil { - return errors.Wrap(err, "go to home screen failed") - } - - // swipe to first screen - for i := 0; i < 5; i++ { - ud.SwipeRight() - } - - // default to retry 5 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 5 - } - // swipe next screen until app found - return ud.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes) - } - return fmt.Errorf("invalid %s params, should be app name(string), got %v", - swipeToTapApp, action.Params) - case swipeToTapText: - if text, ok := action.Params.(string); ok { - var x, y, width, height float64 - findText := func(d *uixt.DriverExt) error { - var err error - x, y, width, height, err = d.FindTextByOCR(text) - return err - } - foundTextAction := func(d *uixt.DriverExt) error { - // tap text - return d.TapFloat(x+width*0.5, y+height*0.5) - } - - // default to retry 10 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 10 - } - // swipe until live room found - return ud.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) - } - return fmt.Errorf("invalid %s params, should be app text(string), got %v", - swipeToTapText, action.Params) - case appTerminate: - if bundleId, ok := action.Params.(string); ok { - success, err := ud.AppTerminate(bundleId) - if err != nil { - return errors.Wrap(err, "failed to terminate app") - } - if !success { - log.Warn().Str("bundleId", bundleId).Msg("app was not running") - } - return nil - } - return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params) - case uiHome: - return ud.Homescreen() - case uiTapXY: - if location, ok := action.Params.([]float64); ok { - // relative x,y of window size: [0.5, 0.5] - if len(location) != 2 { - return fmt.Errorf("invalid tap location params: %v", location) - } - return ud.TapXY(location[0], location[1], action.Identifier) - } - return fmt.Errorf("invalid %s params: %v", uiTapXY, action.Params) - case uiTap: - if param, ok := action.Params.(string); ok { - return ud.Tap(param, action.Identifier, action.IgnoreNotFoundError) - } - return fmt.Errorf("invalid %s params: %v", uiTap, action.Params) - case uiTapByOCR: - if ocrText, ok := action.Params.(string); ok { - return ud.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError) - } - return fmt.Errorf("invalid %s params: %v", uiTapByOCR, action.Params) - case uiTapByCV: - if imagePath, ok := action.Params.(string); ok { - return ud.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError) - } - return fmt.Errorf("invalid %s params: %v", uiTapByCV, action.Params) - case uiDoubleTapXY: - if location, ok := action.Params.([]float64); ok { - // relative x,y of window size: [0.5, 0.5] - if len(location) != 2 { - return fmt.Errorf("invalid tap location params: %v", location) - } - return ud.DoubleTapXY(location[0], location[1]) - } - return fmt.Errorf("invalid %s params: %v", uiDoubleTapXY, action.Params) - case uiDoubleTap: - if param, ok := action.Params.(string); ok { - return ud.DoubleTap(param) - } - return fmt.Errorf("invalid %s params: %v", uiDoubleTap, action.Params) - case uiSwipe: - if positions, ok := action.Params.([]float64); ok { - // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] - if len(positions) != 4 { - return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions) - } - return ud.SwipeRelative( - positions[0], positions[1], positions[2], positions[3], action.Identifier) - } - if direction, ok := action.Params.(string); ok { - return ud.SwipeTo(direction, action.Identifier) - } - return fmt.Errorf("invalid %s params: %v", uiSwipe, action.Params) - case uiInput: - // input text on current active element - // append \n to send text with enter - // send \b\b\b to delete 3 chars - param := fmt.Sprintf("%v", action.Params) - return ud.SendKeys(param) - case ctlSleep: - if param, ok := action.Params.(json.Number); ok { - seconds, _ := param.Float64() - time.Sleep(time.Duration(seconds*1000) * time.Millisecond) - return nil - } else if param, ok := action.Params.(float64); ok { - time.Sleep(time.Duration(param*1000) * time.Millisecond) - return nil - } - return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params) - case ctlScreenShot: - // take snapshot - log.Info().Msg("take snapshot for current screen") - screenshotPath, err := ud.ScreenShot(fmt.Sprintf("%d_screenshot_%d", - ud.startTime.Unix(), time.Now().Unix())) - if err != nil { - return errors.Wrap(err, "take screenshot failed") - } - log.Info().Str("path", screenshotPath).Msg("take screenshot") - ud.screenShots = append(ud.screenShots, screenshotPath) - return err - case ctlStartCamera: - // start camera, alias for app_launch com.apple.camera - return ud.AppLaunch("com.apple.camera") - case ctlStopCamera: - // stop camera, alias for app_terminate com.apple.camera - success, err := ud.AppTerminate("com.apple.camera") - if err != nil { - return errors.Wrap(err, "failed to terminate camera") - } - if !success { - log.Warn().Msg("camera was not running") - } - return nil - } - return nil -} - -func (ud *uiDriver) doValidation(iValidators []interface{}) (validateResults []*ValidationResult, err error) { - for _, iValidator := range iValidators { - validator, ok := iValidator.(Validator) - if !ok { - return nil, errors.New("validator type error") - } - - validataResult := &ValidationResult{ - Validator: validator, - CheckResult: "fail", - } - - // parse check value - if !strings.HasPrefix(validator.Check, "ui_") { - validataResult.CheckResult = "skip" - log.Warn().Interface("validator", validator).Msg("skip validator") - validateResults = append(validateResults, validataResult) - continue - } - - expected, ok := validator.Expect.(string) - if !ok { - return nil, errors.New("validator expect should be string") - } - - var exists bool - if validator.Assert == assertionExists { - exists = true - } else { - exists = false - } - var result bool - switch validator.Check { - case uiSelectorName: - result = (ud.IsNameExist(expected) == exists) - case uiSelectorLabel: - result = (ud.IsLabelExist(expected) == exists) - case uiSelectorOCR: - result = (ud.IsOCRExist(expected) == exists) - case uiSelectorImage: - result = (ud.IsImageExist(expected) == exists) - } - - if result { - log.Info(). - Str("assert", validator.Assert). - Str("expect", expected). - Msg("validate UI success") - validataResult.CheckResult = "pass" - validateResults = append(validateResults, validataResult) - } else { - log.Error(). - Str("assert", validator.Assert). - Str("expect", expected). - Str("msg", validator.Message). - Msg("validate UI failed") - validateResults = append(validateResults, validataResult) - return validateResults, errors.New("step validation failed") - } - } - return validateResults, nil -} From 1daa3c64ba8e86e4cf8556c7d8c6e52cd345d31d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 23 Sep 2022 22:39:43 +0800 Subject: [PATCH 061/169] refactor: move gwda into uixt --- go.mod | 4 +- go.sum | 2 - hrp/config.go | 8 +- .../uixt/{android.go => android_action.go} | 0 hrp/internal/uixt/android_device.go | 15 + hrp/internal/uixt/android_webdriver.go | 1 + hrp/internal/uixt/android_webelment.go | 1 + hrp/internal/uixt/drag.go | 4 +- hrp/internal/uixt/ext.go | 29 +- hrp/internal/uixt/gesture.go | 9 +- hrp/internal/uixt/interface.go | 983 +++++++++++++- hrp/internal/uixt/ios.go | 187 --- hrp/internal/uixt/ios_action.go | 373 ++++++ hrp/internal/uixt/ios_device.go | 470 +++++++ hrp/internal/uixt/ios_test.go | 1187 +++++++++++++++++ hrp/internal/uixt/ios_webdriver.go | 928 +++++++++++++ hrp/internal/uixt/ios_webelement.go | 477 +++++++ hrp/internal/uixt/ocr_on.go | 5 - hrp/internal/uixt/opencv_off.go | 3 +- hrp/internal/uixt/opencv_on.go | 3 +- hrp/internal/uixt/swipe.go | 3 +- hrp/internal/uixt/tap.go | 6 +- hrp/internal/uixt/tap_test.go | 2 +- hrp/step_android_ui.go | 7 +- hrp/step_ios_ui.go | 16 +- 25 files changed, 4475 insertions(+), 248 deletions(-) rename hrp/internal/uixt/{android.go => android_action.go} (100%) create mode 100644 hrp/internal/uixt/android_device.go create mode 100644 hrp/internal/uixt/android_webdriver.go create mode 100644 hrp/internal/uixt/android_webelment.go delete mode 100644 hrp/internal/uixt/ios.go create mode 100644 hrp/internal/uixt/ios_action.go create mode 100644 hrp/internal/uixt/ios_device.go create mode 100644 hrp/internal/uixt/ios_test.go create mode 100644 hrp/internal/uixt/ios_webdriver.go create mode 100644 hrp/internal/uixt/ios_webelement.go diff --git a/go.mod b/go.mod index 9eaee6e2..57d30aeb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 - github.com/electricbubble/gwda v0.4.0 + github.com/electricbubble/gidevice v0.6.2 github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 @@ -42,7 +42,6 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/electricbubble/gidevice v0.6.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 @@ -88,4 +87,3 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 diff --git a/go.sum b/go.sum index e615a6ac..6b9beca0 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 h1:n/O+tMRl7XmuP778Oy2wunq8QpftRS0rlBkKumaJSbc= -github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= diff --git a/hrp/config.go b/hrp/config.go index 99ebcbad..d5caffcf 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -30,8 +30,8 @@ type TConfig struct { ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"` ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"` - IOS []*uixt.WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"` - Android []*uixt.UIAOptions `json:"android,omitempty" yaml:"android,omitempty"` + IOS []*uixt.IOSDevice `json:"ios,omitempty" yaml:"ios,omitempty"` + Android []*uixt.AndroidDevice `json:"android,omitempty" yaml:"android,omitempty"` Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds Export []string `json:"export,omitempty" yaml:"export,omitempty"` Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -102,8 +102,8 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig { return c } -func (c *TConfig) SetIOS(options ...uixt.WDAOption) *TConfig { - wdaOptions := &uixt.WDAOptions{} +func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig { + wdaOptions := &uixt.IOSDevice{} for _, option := range options { option(wdaOptions) } diff --git a/hrp/internal/uixt/android.go b/hrp/internal/uixt/android_action.go similarity index 100% rename from hrp/internal/uixt/android.go rename to hrp/internal/uixt/android_action.go diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go new file mode 100644 index 00000000..b7a3a332 --- /dev/null +++ b/hrp/internal/uixt/android_device.go @@ -0,0 +1,15 @@ +package uixt + +type AndroidDevice struct { + SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` +} + +func (o AndroidDevice) UUID() string { + return o.SerialNumber +} + +func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { + return nil, nil +} diff --git a/hrp/internal/uixt/android_webdriver.go b/hrp/internal/uixt/android_webdriver.go new file mode 100644 index 00000000..64c90179 --- /dev/null +++ b/hrp/internal/uixt/android_webdriver.go @@ -0,0 +1 @@ +package uixt diff --git a/hrp/internal/uixt/android_webelment.go b/hrp/internal/uixt/android_webelment.go new file mode 100644 index 00000000..64c90179 --- /dev/null +++ b/hrp/internal/uixt/android_webelment.go @@ -0,0 +1 @@ +package uixt diff --git a/hrp/internal/uixt/drag.go b/hrp/internal/uixt/drag.go index 510b5915..27a13501 100644 --- a/hrp/internal/uixt/drag.go +++ b/hrp/internal/uixt/drag.go @@ -1,7 +1,5 @@ package uixt -import "github.com/electricbubble/gwda" - func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) { return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...) } @@ -28,5 +26,5 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs fromY := y + height*yOffset return dExt.Driver.DragFloat(fromX, fromY, toX, toY, - gwda.WithPressDuration(pressForDuration[0])) + WithPressDuration(pressForDuration[0])) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 76ffc417..a705d5b8 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -15,7 +15,6 @@ import ( "strings" "time" - "github.com/electricbubble/gwda" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -120,8 +119,8 @@ func WithThreshold(threshold float64) CVOption { } type DriverExt struct { - Driver gwda.WebDriver - windowSize gwda.Size + Driver WebDriver + windowSize Size frame *bytes.Buffer doneMjpegStream chan bool scale float64 @@ -132,7 +131,7 @@ type DriverExt struct { CVArgs } -func extend(driver gwda.WebDriver) (dExt *DriverExt, err error) { +func extend(driver WebDriver) (dExt *DriverExt, err error) { dExt = &DriverExt{Driver: driver} dExt.doneMjpegStream = make(chan bool, 1) @@ -273,17 +272,17 @@ func isPathExists(path string) bool { return true } -func (dExt *DriverExt) FindUIElement(param string) (ele gwda.WebElement, err error) { - var selector gwda.BySelector +func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { + var selector BySelector if strings.HasPrefix(param, "/") { // xpath - selector = gwda.BySelector{ + selector = BySelector{ XPath: param, } } else { // name - selector = gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithName(param), + selector = BySelector{ + LinkText: NewElementAttribute().WithName(param), } } @@ -305,25 +304,25 @@ func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, return } -func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error { +func (dExt *DriverExt) PerformTouchActions(touchActions *TouchActions) error { return dExt.Driver.PerformAppiumTouchActions(touchActions) } -func (dExt *DriverExt) PerformActions(actions *gwda.W3CActions) error { +func (dExt *DriverExt) PerformActions(actions *W3CActions) error { return dExt.Driver.PerformW3CActions(actions) } func (dExt *DriverExt) IsNameExist(name string) bool { - selector := gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithName(name), + selector := BySelector{ + LinkText: NewElementAttribute().WithName(name), } _, err := dExt.Driver.FindElement(selector) return err == nil } func (dExt *DriverExt) IsLabelExist(label string) bool { - selector := gwda.BySelector{ - LinkText: gwda.NewElementAttribute().WithLabel(label), + selector := BySelector{ + LinkText: NewElementAttribute().WithLabel(label), } _, err := dExt.Driver.FindElement(selector) return err == nil diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index 5edd8147..ce3b6b21 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -4,9 +4,6 @@ package uixt import ( "image" - "sort" - - "github.com/electricbubble/gwda" ) func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) { @@ -26,17 +23,17 @@ func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err er return false }) - touchActions := gwda.NewTouchActions(len(password)*2 + 1) + touchActions := NewTouchActions(len(password)*2 + 1) for i := range password { x, y, width, height := dExt.MappingToRectInUIKit(rects[password[i]]) x = x + width*0.5 y = y + height*0.5 if i == 0 { - touchActions.Press(gwda.NewTouchActionPress().WithXYFloat(x, y)). + touchActions.Press(NewTouchActionPress().WithXYFloat(x, y)). Wait(0.2) } else { - touchActions.MoveTo(gwda.NewTouchActionMoveTo().WithXYFloat(x, y)). + touchActions.MoveTo(NewTouchActionMoveTo().WithXYFloat(x, y)). Wait(0.2) } } diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index b788c6e1..cbe7a0e1 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -1,4 +1,985 @@ package uixt -type WebDriver interface { +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +var ( + DefaultWaitTimeout = 60 * time.Second + DefaultWaitInterval = 400 * time.Millisecond + DefaultKeepAliveInterval = 30 * time.Second +) + +type AlertAction string + +const ( + AlertActionAccept AlertAction = "accept" + AlertActionDismiss AlertAction = "dismiss" +) + +type Capabilities map[string]interface{} + +func NewCapabilities() Capabilities { + return make(Capabilities) +} + +func (caps Capabilities) WithAppLaunchOption(launchOpt AppLaunchOption) Capabilities { + for k, v := range launchOpt { + caps[k] = v + } + return caps +} + +// WithDefaultAlertAction +func (caps Capabilities) WithDefaultAlertAction(alertAction AlertAction) Capabilities { + caps["defaultAlertAction"] = alertAction + return caps +} + +// WithMaxTypingFrequency +// Defaults to `60`. +func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities { + if n <= 0 { + n = 60 + } + caps["maxTypingFrequency"] = n + return caps +} + +// WithWaitForIdleTimeout +// Defaults to `10` +func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities { + caps["waitForIdleTimeout"] = second + return caps +} + +// WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility +// Defaults to `false` +func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities { + caps["shouldUseTestManagerForVisibilityDetection"] = b + return caps +} + +// WithShouldUseCompactResponses If set to YES will use compact (standards-compliant) & faster responses +// Defaults to `true` +func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities { + caps["shouldUseCompactResponses"] = b + return caps +} + +// WithElementResponseAttributes If shouldUseCompactResponses == NO, +// is the comma-separated list of fields to return with each element. +// Defaults to `type,label`. +func (caps Capabilities) WithElementResponseAttributes(s string) Capabilities { + caps["elementResponseAttributes"] = s + return caps +} + +// WithShouldUseSingletonTestManager +// Defaults to `true` +func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities { + caps["shouldUseSingletonTestManager"] = b + return caps +} + +// WithDisableAutomaticScreenshots +// Defaults to `true` +func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities { + caps["disableAutomaticScreenshots"] = b + return caps +} + +// WithShouldTerminateApp +// Defaults to `true` +func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities { + caps["shouldTerminateApp"] = b + return caps +} + +// WithEventloopIdleDelaySec +// Delays the invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]' by the timer interval passed. +// which is skipped on setting it to zero. +func (caps Capabilities) WithEventloopIdleDelaySec(second float64) Capabilities { + caps["eventloopIdleDelaySec"] = second + return caps +} + +type SessionInfo struct { + SessionId string `json:"sessionId"` + Capabilities struct { + Device string `json:"device"` + BrowserName string `json:"browserName"` + SdkVersion string `json:"sdkVersion"` + CFBundleIdentifier string `json:"CFBundleIdentifier"` + } `json:"capabilities"` +} + +type DeviceStatus struct { + Message string `json:"message"` + State string `json:"state"` + OS struct { + TestmanagerdVersion int `json:"testmanagerdVersion"` + Name string `json:"name"` + SdkVersion string `json:"sdkVersion"` + Version string `json:"version"` + } `json:"os"` + IOS struct { + IP string `json:"ip"` + SimulatorVersion string `json:"simulatorVersion"` + } `json:"ios"` + Ready bool `json:"ready"` + Build struct { + Time string `json:"time"` + ProductBundleIdentifier string `json:"productBundleIdentifier"` + } `json:"build"` +} + +type DeviceInfo struct { + TimeZone string `json:"timeZone"` + CurrentLocale string `json:"currentLocale"` + Model string `json:"model"` + UUID string `json:"uuid"` + UserInterfaceIdiom int `json:"userInterfaceIdiom"` + UserInterfaceStyle string `json:"userInterfaceStyle"` + Name string `json:"name"` + IsSimulator bool `json:"isSimulator"` + ThermalState int `json:"thermalState"` +} + +type Location struct { + AuthorizationStatus int `json:"authorizationStatus"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Altitude float64 `json:"altitude"` +} + +type BatteryInfo struct { + // Battery level in range [0.0, 1.0], where 1.0 means 100% charge. + Level float64 `json:"level"` + + // Battery state ( 1: on battery, discharging; 2: plugged in, less than 100%, 3: plugged in, at 100% ) + State BatteryState `json:"state"` +} + +type BatteryState int + +const ( + _ = iota + BatteryStateUnplugged BatteryState = iota // on battery, discharging + BatteryStateCharging // plugged in, less than 100% + BatteryStateFull // plugged in, at 100% +) + +func (v BatteryState) String() string { + switch v { + case BatteryStateUnplugged: + return "On battery, discharging" + case BatteryStateCharging: + return "Plugged in, less than 100%" + case BatteryStateFull: + return "Plugged in, at 100%" + default: + return "UNKNOWN" + } +} + +type Size struct { + Width int `json:"width"` + Height int `json:"height"` +} + +type Screen struct { + StatusBarSize Size `json:"statusBarSize"` + Scale float64 `json:"scale"` +} + +type AppInfo struct { + ProcessArguments struct { + Env interface{} `json:"env"` + Args []interface{} `json:"args"` + } `json:"processArguments"` + Name string `json:"name"` + AppBaseInfo +} + +type AppBaseInfo struct { + Pid int `json:"pid"` + BundleId string `json:"bundleId"` +} + +type AppState int + +const ( + AppStateNotRunning AppState = 1 << iota + AppStateRunningBack + AppStateRunningFront +) + +func (v AppState) String() string { + switch v { + case AppStateNotRunning: + return "Not Running" + case AppStateRunningBack: + return "Running (Back)" + case AppStateRunningFront: + return "Running (Front)" + default: + return "UNKNOWN" + } +} + +// AppLaunchOption Configure app launch parameters +type AppLaunchOption map[string]interface{} + +func NewAppLaunchOption() AppLaunchOption { + return make(AppLaunchOption) +} + +func (opt AppLaunchOption) WithBundleId(bundleId string) AppLaunchOption { + opt["bundleId"] = bundleId + return opt +} + +// WithShouldWaitForQuiescence whether to wait for quiescence on application startup +// Defaults to `true` +func (opt AppLaunchOption) WithShouldWaitForQuiescence(b bool) AppLaunchOption { + opt["shouldWaitForQuiescence"] = b + return opt +} + +// WithArguments The optional array of application command line arguments. +// The arguments are going to be applied if the application was not running before. +func (opt AppLaunchOption) WithArguments(args []string) AppLaunchOption { + opt["arguments"] = args + return opt +} + +// WithEnvironment The optional dictionary of environment variables for the application, which is going to be executed. +// The environment variables are going to be applied if the application was not running before. +func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOption { + opt["environment"] = env + return opt +} + +// PasteboardType The type of the item on the pasteboard. +type PasteboardType string + +const ( + PasteboardTypePlaintext PasteboardType = "plaintext" + PasteboardTypeImage PasteboardType = "image" + PasteboardTypeUrl PasteboardType = "url" +) + +const ( + TextBackspace string = "\u0008" + TextDelete string = "\u007F" +) + +// type KeyboardKeyLabel string +// +// const ( +// KeyboardKeyReturn = "return" +// ) + +// DeviceButton A physical button on an iOS device. +type DeviceButton string + +const ( + DeviceButtonHome DeviceButton = "home" + DeviceButtonVolumeUp DeviceButton = "volumeUp" + DeviceButtonVolumeDown DeviceButton = "volumeDown" +) + +type NotificationType string + +const ( + NotificationTypePlain NotificationType = "plain" + NotificationTypeDarwin NotificationType = "darwin" +) + +// EventPageID The event page identifier +type EventPageID int + +const EventPageIDConsumer EventPageID = 0x0C + +// EventUsageID The event usage identifier (usages are defined per-page) +type EventUsageID int + +const ( + EventUsageIDCsmrVolumeUp EventUsageID = 0xE9 + EventUsageIDCsmrVolumeDown EventUsageID = 0xEA + EventUsageIDCsmrHome EventUsageID = 0x40 + EventUsageIDCsmrPower EventUsageID = 0x30 + EventUsageIDCsmrSnapshot EventUsageID = 0x65 // Power + Home +) + +type Orientation string + +const ( + // OrientationPortrait Device oriented vertically, home button on the bottom + OrientationPortrait Orientation = "PORTRAIT" + + // OrientationPortraitUpsideDown Device oriented vertically, home button on the top + OrientationPortraitUpsideDown Orientation = "UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN" + + // OrientationLandscapeLeft Device oriented horizontally, home button on the right + OrientationLandscapeLeft Orientation = "LANDSCAPE" + + // OrientationLandscapeRight Device oriented horizontally, home button on the left + OrientationLandscapeRight Orientation = "UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT" +) + +type Rotation struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` +} + +// SourceOption Configure the format or attribute of the Source +type SourceOption map[string]interface{} + +func NewSourceOption() SourceOption { + return make(SourceOption) +} + +// WithFormatAsJson Application elements tree in form of json string +func (opt SourceOption) WithFormatAsJson() SourceOption { + opt["format"] = "json" + return opt +} + +// WithFormatAsXml Application elements tree in form of xml string +func (opt SourceOption) WithFormatAsXml() SourceOption { + opt["format"] = "xml" + return opt +} + +// WithFormatAsDescription Application elements tree in form of internal XCTest debugDescription string +func (opt SourceOption) WithFormatAsDescription() SourceOption { + opt["format"] = "description" + return opt +} + +// WithScope Allows to provide XML scope. +// only `xml` is supported. +func (opt SourceOption) WithScope(scope string) SourceOption { + if vFormat, ok := opt["format"]; ok && vFormat != "xml" { + return opt + } + opt["scope"] = scope + return opt +} + +// WithExcludedAttributes Excludes the given attribute names. +// only `xml` is supported. +func (opt SourceOption) WithExcludedAttributes(attributes []string) SourceOption { + if vFormat, ok := opt["format"]; ok && vFormat != "xml" { + return opt + } + opt["excluded_attributes"] = strings.Join(attributes, ",") + return opt +} + +const ( + // legacyWebElementIdentifier is the string constant used in the old + // WebDriver JSON protocol that is the key for the map that contains an + // unique element identifier. + legacyWebElementIdentifier = "ELEMENT" + + // webElementIdentifier is the string constant defined by the W3C + // specification that is the key for the map that contains a unique element identifier. + webElementIdentifier = "element-6066-11e4-a52e-4f735466cecf" +) + +func elementIDFromValue(val map[string]string) string { + for _, key := range []string{webElementIdentifier, legacyWebElementIdentifier} { + if v, ok := val[key]; ok && v != "" { + return v + } + } + return "" +} + +// performance ranking: class name > accessibility id > link text > predicate > class chain > xpath +type BySelector struct { + ClassName ElementType `json:"class name"` + + // isSearchByIdentifier + Name string `json:"name"` + Id string `json:"id"` + AccessibilityId string `json:"accessibility id"` + // isSearchByIdentifier + + // partialSearch + LinkText ElementAttribute `json:"link text"` + PartialLinkText ElementAttribute `json:"partial link text"` + // partialSearch + + Predicate string `json:"predicate string"` + + ClassChain string `json:"class chain"` + + XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively +} + +func (wl BySelector) getUsingAndValue() (using, value string) { + vBy := reflect.ValueOf(wl) + tBy := reflect.TypeOf(wl) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + switch vi := vi.(type) { + case ElementType: + value = vi.String() + case string: + value = vi + case ElementAttribute: + value = vi.String() + } + if value != "" && value != "UNKNOWN" { + using = tBy.Field(i).Tag.Get("json") + return + } + } + return +} + +type ElementAttribute map[string]interface{} + +func (ea ElementAttribute) String() string { + for k, v := range ea { + switch v := v.(type) { + case bool: + return k + "=" + strconv.FormatBool(v) + case string: + return k + "=" + v + default: + return k + "=" + fmt.Sprintf("%v", v) + } + } + return "UNKNOWN" +} + +func (ea ElementAttribute) getAttributeName() string { + for k := range ea { + return k + } + return "UNKNOWN" +} + +func NewElementAttribute() ElementAttribute { + return make(ElementAttribute) +} + +// WithUID Element's unique identifier +func (ea ElementAttribute) WithUID(uid string) ElementAttribute { + ea["UID"] = uid + return ea +} + +// WithAccessibilityContainer Whether element is an accessibility container +// (contains children of any depth that are accessible) +func (ea ElementAttribute) WithAccessibilityContainer(b bool) ElementAttribute { + ea["accessibilityContainer"] = b + return ea +} + +// WithAccessible Whether element is accessible +func (ea ElementAttribute) WithAccessible(b bool) ElementAttribute { + ea["accessible"] = b + return ea +} + +// WithEnabled Whether element is enabled +func (ea ElementAttribute) WithEnabled(b bool) ElementAttribute { + ea["enabled"] = b + return ea +} + +// WithLabel Element's label +func (ea ElementAttribute) WithLabel(s string) ElementAttribute { + ea["label"] = s + return ea +} + +// WithName Element's name +func (ea ElementAttribute) WithName(s string) ElementAttribute { + ea["name"] = s + return ea +} + +// WithSelected Element's selected state +func (ea ElementAttribute) WithSelected(b bool) ElementAttribute { + ea["selected"] = b + return ea +} + +// WithType Element's type +func (ea ElementAttribute) WithType(elemType ElementType) ElementAttribute { + ea["type"] = elemType + return ea +} + +// WithValue Element's value +func (ea ElementAttribute) WithValue(s string) ElementAttribute { + ea["value"] = s + return ea +} + +// WithVisible +// +// Whether element is visible +func (ea ElementAttribute) WithVisible(b bool) ElementAttribute { + ea["visible"] = b + return ea +} + +func (et ElementType) String() string { + vBy := reflect.ValueOf(et) + tBy := reflect.TypeOf(et) + for i := 0; i < vBy.NumField(); i++ { + if vBy.Field(i).Bool() { + return tBy.Field(i).Tag.Get("json") + } + } + return "UNKNOWN" +} + +// ElementType +// !!! This mapping should be updated if there are changes after each new XCTest release"` +type ElementType struct { + Any bool `json:"XCUIElementTypeAny"` + Other bool `json:"XCUIElementTypeOther"` + Application bool `json:"XCUIElementTypeApplication"` + Group bool `json:"XCUIElementTypeGroup"` + Window bool `json:"XCUIElementTypeWindow"` + Sheet bool `json:"XCUIElementTypeSheet"` + Drawer bool `json:"XCUIElementTypeDrawer"` + Alert bool `json:"XCUIElementTypeAlert"` + Dialog bool `json:"XCUIElementTypeDialog"` + Button bool `json:"XCUIElementTypeButton"` + RadioButton bool `json:"XCUIElementTypeRadioButton"` + RadioGroup bool `json:"XCUIElementTypeRadioGroup"` + CheckBox bool `json:"XCUIElementTypeCheckBox"` + DisclosureTriangle bool `json:"XCUIElementTypeDisclosureTriangle"` + PopUpButton bool `json:"XCUIElementTypePopUpButton"` + ComboBox bool `json:"XCUIElementTypeComboBox"` + MenuButton bool `json:"XCUIElementTypeMenuButton"` + ToolbarButton bool `json:"XCUIElementTypeToolbarButton"` + Popover bool `json:"XCUIElementTypePopover"` + Keyboard bool `json:"XCUIElementTypeKeyboard"` + Key bool `json:"XCUIElementTypeKey"` + NavigationBar bool `json:"XCUIElementTypeNavigationBar"` + TabBar bool `json:"XCUIElementTypeTabBar"` + TabGroup bool `json:"XCUIElementTypeTabGroup"` + Toolbar bool `json:"XCUIElementTypeToolbar"` + StatusBar bool `json:"XCUIElementTypeStatusBar"` + Table bool `json:"XCUIElementTypeTable"` + TableRow bool `json:"XCUIElementTypeTableRow"` + TableColumn bool `json:"XCUIElementTypeTableColumn"` + Outline bool `json:"XCUIElementTypeOutline"` + OutlineRow bool `json:"XCUIElementTypeOutlineRow"` + Browser bool `json:"XCUIElementTypeBrowser"` + CollectionView bool `json:"XCUIElementTypeCollectionView"` + Slider bool `json:"XCUIElementTypeSlider"` + PageIndicator bool `json:"XCUIElementTypePageIndicator"` + ProgressIndicator bool `json:"XCUIElementTypeProgressIndicator"` + ActivityIndicator bool `json:"XCUIElementTypeActivityIndicator"` + SegmentedControl bool `json:"XCUIElementTypeSegmentedControl"` + Picker bool `json:"XCUIElementTypePicker"` + PickerWheel bool `json:"XCUIElementTypePickerWheel"` + Switch bool `json:"XCUIElementTypeSwitch"` + Toggle bool `json:"XCUIElementTypeToggle"` + Link bool `json:"XCUIElementTypeLink"` + Image bool `json:"XCUIElementTypeImage"` + Icon bool `json:"XCUIElementTypeIcon"` + SearchField bool `json:"XCUIElementTypeSearchField"` + ScrollView bool `json:"XCUIElementTypeScrollView"` + ScrollBar bool `json:"XCUIElementTypeScrollBar"` + StaticText bool `json:"XCUIElementTypeStaticText"` + TextField bool `json:"XCUIElementTypeTextField"` + SecureTextField bool `json:"XCUIElementTypeSecureTextField"` + DatePicker bool `json:"XCUIElementTypeDatePicker"` + TextView bool `json:"XCUIElementTypeTextView"` + Menu bool `json:"XCUIElementTypeMenu"` + MenuItem bool `json:"XCUIElementTypeMenuItem"` + MenuBar bool `json:"XCUIElementTypeMenuBar"` + MenuBarItem bool `json:"XCUIElementTypeMenuBarItem"` + Map bool `json:"XCUIElementTypeMap"` + WebView bool `json:"XCUIElementTypeWebView"` + IncrementArrow bool `json:"XCUIElementTypeIncrementArrow"` + DecrementArrow bool `json:"XCUIElementTypeDecrementArrow"` + Timeline bool `json:"XCUIElementTypeTimeline"` + RatingIndicator bool `json:"XCUIElementTypeRatingIndicator"` + ValueIndicator bool `json:"XCUIElementTypeValueIndicator"` + SplitGroup bool `json:"XCUIElementTypeSplitGroup"` + Splitter bool `json:"XCUIElementTypeSplitter"` + RelevanceIndicator bool `json:"XCUIElementTypeRelevanceIndicator"` + ColorWell bool `json:"XCUIElementTypeColorWell"` + HelpTag bool `json:"XCUIElementTypeHelpTag"` + Matte bool `json:"XCUIElementTypeMatte"` + DockItem bool `json:"XCUIElementTypeDockItem"` + Ruler bool `json:"XCUIElementTypeRuler"` + RulerMarker bool `json:"XCUIElementTypeRulerMarker"` + Grid bool `json:"XCUIElementTypeGrid"` + LevelIndicator bool `json:"XCUIElementTypeLevelIndicator"` + Cell bool `json:"XCUIElementTypeCell"` + LayoutArea bool `json:"XCUIElementTypeLayoutArea"` + LayoutItem bool `json:"XCUIElementTypeLayoutItem"` + Handle bool `json:"XCUIElementTypeHandle"` + Stepper bool `json:"XCUIElementTypeStepper"` + Tab bool `json:"XCUIElementTypeTab"` + TouchBar bool `json:"XCUIElementTypeTouchBar"` + StatusItem bool `json:"XCUIElementTypeStatusItem"` +} + +// ProtectedResource A system resource that requires user authorization to access. +type ProtectedResource int + +// https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc +const ( + ProtectedResourceContacts ProtectedResource = 1 + ProtectedResourceCalendar ProtectedResource = 2 + ProtectedResourceReminders ProtectedResource = 3 + ProtectedResourcePhotos ProtectedResource = 4 + ProtectedResourceMicrophone ProtectedResource = 5 + ProtectedResourceCamera ProtectedResource = 6 + ProtectedResourceMediaLibrary ProtectedResource = 7 + ProtectedResourceHomeKit ProtectedResource = 8 + ProtectedResourceSystemRootDirectory ProtectedResource = 0x40000000 + ProtectedResourceUserDesktopDirectory ProtectedResource = 0x40000001 + ProtectedResourceUserDownloadsDirectory ProtectedResource = 0x40000002 + ProtectedResourceUserDocumentsDirectory ProtectedResource = 0x40000003 + ProtectedResourceBluetooth ProtectedResource = -0x40000000 + ProtectedResourceKeyboardNetwork ProtectedResource = -0x40000001 + ProtectedResourceLocation ProtectedResource = -0x40000002 + ProtectedResourceHealth ProtectedResource = -0x40000003 +) + +type Condition func(wd WebDriver) (bool, error) + +type Direction string + +const ( + DirectionUp Direction = "up" + DirectionDown Direction = "down" + DirectionLeft Direction = "left" + DirectionRight Direction = "right" +) + +type PickerWheelOrder string + +const ( + PickerWheelOrderNext PickerWheelOrder = "next" + PickerWheelOrderPrevious PickerWheelOrder = "previous" +) + +type Point struct { + X int `json:"x"` // upper left X coordinate of selected element + Y int `json:"y"` // upper left Y coordinate of selected element +} + +type Rect struct { + Point + Size +} + +type DataOption func(data map[string]interface{}) + +func WithCustomOption(key string, value interface{}) DataOption { + return func(data map[string]interface{}) { + data[key] = value + } +} + +func WithPressDuration(duraion float64) DataOption { + return func(data map[string]interface{}) { + data["duration"] = duraion + } +} + +func WithFrequency(frequency int) DataOption { + return func(data map[string]interface{}) { + data["frequency"] = frequency + } +} + +// WebDriver defines methods supported by WebDriver drivers. +type WebDriver interface { + // NewSession starts a new session and returns the SessionInfo. + NewSession(capabilities Capabilities) (SessionInfo, error) + + ActiveSession() (SessionInfo, error) + // DeleteSession Kills application associated with that session and removes session + // 1) alertsMonitor disable + // 2) testedApplicationBundleId terminate + DeleteSession() error + + Status() (DeviceStatus, error) + + DeviceInfo() (DeviceInfo, error) + + // Location Returns device location data. + // + // It requires to configure location access permission by manual. + // The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization. + // 'authorizationStatus' indicates current authorization status. '3' is 'Always'. + // https://developer.apple.com/documentation/corelocation/clauthorizationstatus + // + // Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always + // + // The return value could be zero even if the permission is set to 'Always' + // since the location service needs some time to update the location data. + Location() (Location, error) + BatteryInfo() (BatteryInfo, error) + WindowSize() (Size, error) + Screen() (Screen, error) + Scale() (float64, error) + ActiveAppInfo() (AppInfo, error) + // ActiveAppsList Retrieves the information about the currently active apps + ActiveAppsList() ([]AppBaseInfo, error) + // AppState Get the state of the particular application in scope of the current session. + // !This method is only returning reliable results since Xcode9 SDK + AppState(bundleId string) (AppState, error) + + // IsLocked Checks if the screen is locked or not. + IsLocked() (bool, error) + // Unlock Forces the device under test to unlock. + // An immediate return will happen if the device is already unlocked + // and an error is going to be thrown if the screen has not been unlocked after the timeout. + Unlock() error + // Lock Forces the device under test to switch to the lock screen. + // An immediate return will happen if the device is already locked + // and an error is going to be thrown if the screen has not been locked after the timeout. + Lock() error + + // Homescreen Forces the device under test to switch to the home screen + Homescreen() error + + // AlertText Returns alert's title and description separated by new lines + AlertText() (string, error) + // AlertButtons Gets the labels of the buttons visible in the alert + AlertButtons() ([]string, error) + // AlertAccept Accepts alert, if present + AlertAccept(label ...string) error + // AlertDismiss Dismisses alert, if present + AlertDismiss(label ...string) error + // AlertSendKeys Types a text into an input inside the alert container, if it is present + AlertSendKeys(text string) error + + // AppLaunch Launch an application with given bundle identifier in scope of current session. + // !This method is only available since Xcode9 SDK + AppLaunch(bundleId string, launchOpt ...AppLaunchOption) error + // AppLaunchUnattached Launch the app with the specified bundle ID. + AppLaunchUnattached(bundleId string) error + // AppTerminate Terminate an application with the given bundle id. + // Either `true` if the app has been successfully terminated or `false` if it was not running + AppTerminate(bundleId string) (bool, error) + // AppActivate Activate an application with given bundle identifier in scope of current session. + // !This method is only available since Xcode9 SDK + AppActivate(bundleId string) error + // AppDeactivate Deactivates application for given time and then activate it again + // The minimum application switch wait is 3 seconds + AppDeactivate(second float64) error + + // AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4 + AppAuthReset(ProtectedResource) error + + // Tap Sends a tap event at the coordinate. + Tap(x, y int, options ...DataOption) error + TapFloat(x, y float64, options ...DataOption) error + + // DoubleTap Sends a double tap event at the coordinate. + DoubleTap(x, y int) error + DoubleTapFloat(x, y float64) error + + // TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration. + // second: The default value is 1 + TouchAndHold(x, y int, second ...float64) error + TouchAndHoldFloat(x, y float64, second ...float64) error + + // Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate. + // WithPressDuration option can be used to set pressForDuration (default to 1 second). + Drag(fromX, fromY, toX, toY int, options ...DataOption) error + DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) error + + // Swipe works like Drag, but `pressForDuration` value is 0 + Swipe(fromX, fromY, toX, toY int, options ...DataOption) error + SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error + + ForceTouch(x, y int, pressure float64, second ...float64) error + ForceTouchFloat(x, y, pressure float64, second ...float64) error + + // PerformW3CActions Perform complex touch action in scope of the current application. + PerformW3CActions(actions *W3CActions) error + PerformAppiumTouchActions(touchActs *TouchActions) error + + // SetPasteboard Sets data to the general pasteboard + SetPasteboard(contentType PasteboardType, content string) error + // GetPasteboard Gets the data contained in the general pasteboard. + // It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330 + GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err 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 + SendKeys(text string, options ...DataOption) error + + // KeyboardDismiss Tries to dismiss the on-screen keyboard + KeyboardDismiss(keyNames ...string) error + + // PressButton Presses the corresponding hardware button on the device + PressButton(devBtn DeviceButton) error + + // IOHIDEvent Emulated triggering of the given low-level IOHID device event. + // duration: The event duration in float seconds (XCTest uses 0.005 for a single press event) + IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) error + + // ExpectNotification Creates an expectation that is fulfilled when an expected Notification is received + ExpectNotification(notifyName string, notifyType NotificationType, second ...int) error + + // SiriActivate Activates Siri service voice recognition with the given text to parse + SiriActivate(text string) error + // SiriOpenUrl Opens the particular url scheme using Siri voice recognition helpers. + // !This will only work since XCode 8.3/iOS 10.3 + // It doesn't actually work, right? + SiriOpenUrl(url string) error + + Orientation() (Orientation, error) + // SetOrientation Sets requested device interface orientation. + SetOrientation(Orientation) error + + Rotation() (Rotation, error) + // SetRotation Sets the devices orientation to the rotation passed. + SetRotation(Rotation) error + + // MatchTouchID Matches or mismatches TouchID request + MatchTouchID(isMatch bool) error + + // ActiveElement Returns the element, which currently holds the keyboard input focus or nil if there are no such elements. + ActiveElement() (WebElement, error) + FindElement(by BySelector) (WebElement, error) + FindElements(by BySelector) ([]WebElement, error) + + Screenshot() (*bytes.Buffer, error) + + // Source Return application elements tree + Source(srcOpt ...SourceOption) (string, error) + // AccessibleSource Return application elements accessibility tree + AccessibleSource() (string, error) + + // HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions + // Checks health of XCTest by: + // 1) Querying application for some elements, + // 2) Triggering some device events. + HealthCheck() error + GetAppiumSettings() (map[string]interface{}, error) + SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) + + IsHealthy() (bool, error) + + // WaitWithTimeoutAndInterval waits for the condition to evaluate to true. + WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error + // WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. + WaitWithTimeout(condition Condition, timeout time.Duration) error + // Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. + Wait(condition Condition) error + + // Close inner connections properly + Close() error +} + +// WebElement defines method supported by web elements. +type WebElement interface { + // Click Waits for element to become stable (not move) and performs sync tap on element. + Click() error + // SendKeys Types a text into element. It will try to activate keyboard on element, + // if element has no keyboard focus. + // frequency: Frequency of typing (letters per sec). The default value is 60 + SendKeys(text string, frequency ...int) error + // Clear Clears text on element. It will try to activate keyboard on element, + // if element has no keyboard focus. + Clear() error + + // Tap Waits for element to become stable (not move) and performs sync tap on element, + // relative to the current element position + Tap(x, y int) error + TapFloat(x, y float64) error + + // DoubleTap Sends a double tap event to a hittable point computed for the element. + DoubleTap() error + + // TouchAndHold Sends a long-press gesture to a hittable point computed for the element, + // holding for the specified duration. + // second: The default value is 1 + TouchAndHold(second ...float64) error + // TwoFingerTap Sends a two finger tap event to a hittable point computed for the element. + TwoFingerTap() error + // TapWithNumberOfTaps Sends one or more taps with one or more touch points. + TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) error + // ForceTouch Waits for element to become stable (not move) and performs sync force touch on element. + // second: The default value is 1 + ForceTouch(pressure float64, second ...float64) error + // ForceTouchFloat works like ForceTouch, but relative to the current element position + ForceTouchFloat(x, y, pressure float64, second ...float64) error + + // Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate. + // relative to the current element position + // pressForDuration: The default value is 1 second. + Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error + DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) error + + // Swipe works like Drag, but `pressForDuration` value is 0. + // relative to the current element position + Swipe(fromX, fromY, toX, toY int) error + SwipeFloat(fromX, fromY, toX, toY float64) error + // SwipeDirection Performs swipe gesture on the element. + // velocity: swipe speed in pixels per second. Custom velocity values are only supported since Xcode SDK 11.4. + SwipeDirection(direction Direction, velocity ...float64) error + + // Pinch Sends a pinching gesture with two touches. + // scale: The scale of the pinch gesture. Use a scale between 0 and 1 to "pinch close" or zoom out + // and a scale greater than 1 to "pinch open" or zoom in. + // velocity: The velocity of the pinch in scale factor per second. + Pinch(scale, velocity float64) error + PinchToZoomOutByW3CAction(scale ...float64) error + + // Rotate Sends a rotation gesture with two touches. + // rotation: The rotation of the gesture in radians. + // velocity: The velocity of the rotation gesture in radians per second. + Rotate(rotation float64, velocity ...float64) error + + // PickerWheelSelect + // offset: The default value is 2 + PickerWheelSelect(order PickerWheelOrder, offset ...int) error + + ScrollElementByName(name string) error + ScrollElementByPredicate(predicate string) error + ScrollToVisible() error + // ScrollDirection + // distance: The default value is 0.5 + ScrollDirection(direction Direction, distance ...float64) error + + FindElement(by BySelector) (element WebElement, err error) + FindElements(by BySelector) (elements []WebElement, err error) + FindVisibleCells() (elements []WebElement, err error) + + Rect() (rect Rect, err error) + Location() (Point, error) + Size() (Size, error) + Text() (text string, err error) + Type() (elemType string, err error) + IsEnabled() (enabled bool, err error) + IsDisplayed() (displayed bool, err error) + IsSelected() (selected bool, err error) + IsAccessible() (accessible bool, err error) + IsAccessibilityContainer() (isAccessibilityContainer bool, err error) + GetAttribute(attr ElementAttribute) (value string, err error) + UID() (uid string) + + Screenshot() (raw *bytes.Buffer, err error) } diff --git a/hrp/internal/uixt/ios.go b/hrp/internal/uixt/ios.go deleted file mode 100644 index d79caf27..00000000 --- a/hrp/internal/uixt/ios.go +++ /dev/null @@ -1,187 +0,0 @@ -package uixt - -import ( - "bytes" - "fmt" - "io/ioutil" - "net/http" - - "github.com/electricbubble/gwda" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/internal/json" -) - -const ( - // Changes the value of maximum depth for traversing elements source tree. - // It may help to prevent out of memory or timeout errors while getting the elements source tree, - // but it might restrict the depth of source tree. - // A part of elements source tree might be lost if the value was too small. Defaults to 50 - snapshotMaxDepth = 10 - // Allows to customize accept/dismiss alert button selector. - // It helps you to handle an arbitrary element as accept button in accept alert command. - // The selector should be a valid class chain expression, where the search root is the alert element itself. - // The default button location algorithm is used if the provided selector is wrong or does not match any element. - // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] - acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" - dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" -) - -type Options interface { - UUID() string -} - -type WDAOptions struct { - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` -} - -func (o WDAOptions) UUID() string { - return o.UDID -} - -type WDAOption func(*WDAOptions) - -func WithUDID(udid string) WDAOption { - return func(device *WDAOptions) { - device.UDID = udid - } -} - -func WithPort(port int) WDAOption { - return func(device *WDAOptions) { - device.Port = port - } -} - -func WithMjpegPort(port int) WDAOption { - return func(device *WDAOptions) { - device.MjpegPort = port - } -} - -func WithLogOn(logOn bool) WDAOption { - return func(device *WDAOptions) { - device.LogOn = logOn - } -} - -func InitWDAClient(options *WDAOptions) (*DriverExt, error) { - var deviceOptions []gwda.DeviceOption - if options.UDID != "" { - deviceOptions = append(deviceOptions, gwda.WithSerialNumber(options.UDID)) - } - if options.Port != 0 { - deviceOptions = append(deviceOptions, gwda.WithPort(options.Port)) - } - if options.MjpegPort != 0 { - deviceOptions = append(deviceOptions, gwda.WithMjpegPort(options.MjpegPort)) - } - - // init wda device - targetDevice, err := gwda.NewDevice(deviceOptions...) - if err != nil { - return nil, err - } - - // switch to iOS springboard before init WDA session - // aviod getting stuck when some super app is activate such as douyin or wexin - log.Info().Msg("switch to iOS springboard") - bundleID := "com.apple.springboard" - _, err = targetDevice.GIDevice().AppLaunch(bundleID) - if err != nil { - return nil, errors.Wrap(err, "launch springboard failed") - } - - // init WDA driver - gwda.SetDebug(true) - capabilities := gwda.NewCapabilities() - capabilities.WithDefaultAlertAction(gwda.AlertActionAccept) - driver, err := gwda.NewUSBDriver(capabilities, *targetDevice) - if err != nil { - return nil, errors.Wrap(err, "failed to init WDA driver") - } - driverExt, err := Extend(driver) - if err != nil { - return nil, errors.Wrap(err, "failed to extend gwda.WebDriver") - } - settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ - "snapshotMaxDepth": snapshotMaxDepth, - "acceptAlertButtonSelector": acceptAlertButtonSelector, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to set appium WDA settings") - } - log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") - - driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) - if options.LogOn { - err = driverExt.StartLogRecording("hrp_wda_log") - if err != nil { - return nil, err - } - } - - return driverExt, nil -} - -type wdaResponse struct { - Value string `json:"value"` - SessionID string `json:"sessionId"` -} - -func (dExt *DriverExt) StartLogRecording(identifier string) error { - log.Info().Msg("start WDA log recording") - data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} - _, err := dExt.triggerWDALog(data) - if err != nil { - return errors.Wrap(err, "failed to start WDA log recording") - } - - return nil -} - -func (dExt *DriverExt) GetLogs() (string, error) { - log.Info().Msg("stop WDA log recording") - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - return "", errors.Wrap(err, "failed to get WDA logs") - } - - return reply.Value, nil -} - -func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { - // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] - postJSON, err := json.Marshal(data) - if err != nil { - return nil, err - } - - url := fmt.Sprintf("%s/gtf/automation/log", dExt.host) - log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log") - resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON)) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode) - } - - rawResp, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - reply := new(wdaResponse) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - return reply, nil -} diff --git a/hrp/internal/uixt/ios_action.go b/hrp/internal/uixt/ios_action.go new file mode 100644 index 00000000..0541f827 --- /dev/null +++ b/hrp/internal/uixt/ios_action.go @@ -0,0 +1,373 @@ +package uixt + +import ( + "strconv" + "strings" +) + +type W3CActions []map[string]interface{} + +func NewW3CActions(capacity ...int) *W3CActions { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(W3CActions, 0, capacity[0]) + return &tmp +} + +func (act *W3CActions) SendKeys(text string) *W3CActions { + keyboard := make(map[string]interface{}) + keyboard["type"] = "key" + keyboard["id"] = "keyboard" + strconv.FormatInt(int64(len(*act)+1), 10) + + ss := strings.Split(text, "") + type KeyEvent struct { + Type string `json:"type"` + Value string `json:"value"` + } + actOptKey := make([]KeyEvent, 0, len(ss)+1) + for i := range ss { + actOptKey = append( + actOptKey, + KeyEvent{Type: "keyDown", Value: ss[i]}, + KeyEvent{Type: "keyUp", Value: ss[i]}, + ) + } + keyboard["actions"] = actOptKey + *act = append(*act, keyboard) + return act +} + +func (act *W3CActions) _newFinger() map[string]interface{} { + pointer := make(map[string]interface{}) + pointer["type"] = "pointer" + pointer["id"] = "finger" + strconv.FormatInt(int64(len(*act)+1), 10) + pointer["parameters"] = map[string]string{"pointerType": "touch"} + return pointer +} + +func (act *W3CActions) FingerAction(fingerAct *FingerAction, fActs ...*FingerAction) *W3CActions { + fActs = append([]*FingerAction{fingerAct}, fActs...) + for i := range fActs { + pointer := act._newFinger() + pointer["actions"] = *fActs[i] + *act = append(*act, pointer) + } + return act +} + +type FingerAction []map[string]interface{} + +func NewFingerAction(capacity ...int) *FingerAction { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(FingerAction, 0, capacity[0]) + return &tmp +} + +type FingerMove map[string]interface{} + +func NewFingerMove() FingerMove { + return map[string]interface{}{"type": "pointerMove"} +} + +func (fm FingerMove) WithXY(x, y int) FingerMove { + fm["x"] = x + fm["y"] = y + return fm +} + +func (fm FingerMove) WithXYFloat(x, y float64) FingerMove { + fm["x"] = x + fm["y"] = y + return fm +} + +func (fm FingerMove) WithOrigin(element WebElement) FingerMove { + fm["origin"] = element.UID() + return fm +} + +func (fm FingerMove) WithDuration(second float64) FingerMove { + fm["duration"] = second + return fm +} + +func (fa *FingerAction) Move(fm FingerMove) *FingerAction { + *fa = append(*fa, fm) + return fa +} + +func (fa *FingerAction) Down() *FingerAction { + *fa = append(*fa, map[string]interface{}{"type": "pointerDown"}) + return fa +} + +func (fa *FingerAction) Up() *FingerAction { + *fa = append(*fa, map[string]interface{}{"type": "pointerUp"}) + return fa +} + +func (fa *FingerAction) Pause(second ...float64) *FingerAction { + if len(second) == 0 || second[0] < 0 { + second = []float64{0.5} + } + tmp := map[string]interface{}{ + "type": "pause", + "duration": second[0] * 1000, + } + *fa = append(*fa, tmp) + return fa +} + +func (act *W3CActions) Tap(x, y int, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(0.1). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) DoubleTap(x, y int, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(0.1). + Up(). + Pause(0.04). + Down(). + Pause(0.1). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) Press(x, y int, second float64, element ...WebElement) *W3CActions { + fm := NewFingerMove().WithXY(x, y) + if len(element) != 0 { + fm.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fm). + Down(). + Pause(second). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) Swipe(fromX, fromY, toX, toY int, element ...WebElement) *W3CActions { + fmFrom := NewFingerMove().WithXY(fromX, fromY) + fmTo := NewFingerMove().WithXY(toX, toY) + if len(element) != 0 { + fmFrom.WithOrigin(element[0]) + fmTo.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fmFrom). + Down(). + Pause(0.25). + Move(fmTo). + Pause(0.25). + Up() + return act.FingerAction(fingerAction) +} + +func (act *W3CActions) SwipeFloat(fromX, fromY, toX, toY float64, element ...WebElement) *W3CActions { + fmFrom := NewFingerMove().WithXYFloat(fromX, fromY) + fmTo := NewFingerMove().WithXYFloat(toX, toY) + if len(element) != 0 { + fmFrom.WithOrigin(element[0]) + fmTo.WithOrigin(element[0]) + } + fingerAction := NewFingerAction(). + Move(fmFrom). + Down(). + Pause(0.25). + Move(fmTo). + Pause(0.25). + Up() + return act.FingerAction(fingerAction) +} + +/* ---------------------------------------------------------------------------------------------------------------- */ + +type TouchActions []map[string]interface{} + +func NewTouchActions(capacity ...int) *TouchActions { + if len(capacity) == 0 || capacity[0] <= 0 { + capacity = []int{8} + } + tmp := make(TouchActions, 0, capacity[0]) + return &tmp +} + +func (act *TouchActions) MoveTo(opt TouchActionMoveTo) *TouchActions { + tmp := map[string]interface{}{ + "action": "moveTo", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Tap(opt TouchActionTap) *TouchActions { + tmp := map[string]interface{}{ + "action": "tap", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Press(opt TouchActionPress) *TouchActions { + tmp := map[string]interface{}{ + "action": "press", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) LongPress(opt TouchActionLongPress) *TouchActions { + tmp := map[string]interface{}{ + "action": "longPress", + "options": opt, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Wait(second ...float64) *TouchActions { + if len(second) == 0 || second[0] < 0 { + second = []float64{0.5} + } + tmp := map[string]interface{}{ + "action": "wait", + "options": map[string]interface{}{"ms": second[0] * 1000}, + } + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Release() *TouchActions { + tmp := map[string]interface{}{"action": "release"} + *act = append(*act, tmp) + return act +} + +func (act *TouchActions) Cancel() *TouchActions { + tmp := map[string]interface{}{"action": "cancel"} + *act = append(*act, tmp) + return act +} + +type TouchActionMoveTo map[string]interface{} + +func NewTouchActionMoveTo() TouchActionMoveTo { + return make(map[string]interface{}) +} + +func (opt TouchActionMoveTo) WithXY(x, y int) TouchActionMoveTo { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionMoveTo) WithXYFloat(x, y float64) TouchActionMoveTo { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionMoveTo) WithElement(element WebElement) TouchActionMoveTo { + opt["element"] = element.UID() + return opt +} + +type TouchActionTap map[string]interface{} + +func NewTouchActionTap() TouchActionTap { + return make(map[string]interface{}) +} + +func (opt TouchActionTap) WithXY(x, y int) TouchActionTap { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionTap) WithXYFloat(x, y float64) TouchActionTap { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionTap) WithElement(element WebElement) TouchActionTap { + opt["element"] = element.UID() + return opt +} + +func (opt TouchActionTap) WithCount(count int) TouchActionTap { + opt["count"] = count + return opt +} + +type TouchActionPress map[string]interface{} + +func NewTouchActionPress() TouchActionPress { + return make(map[string]interface{}) +} + +func (opt TouchActionPress) WithXY(x, y int) TouchActionPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionPress) WithXYFloat(x, y float64) TouchActionPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionPress) WithElement(element WebElement) TouchActionPress { + opt["element"] = element.UID() + return opt +} + +func (opt TouchActionPress) WithPressure(pressure float64) TouchActionPress { + opt["pressure"] = pressure + return opt +} + +type TouchActionLongPress map[string]interface{} + +func NewTouchActionLongPress() TouchActionLongPress { + return make(map[string]interface{}) +} + +func (opt TouchActionLongPress) WithXY(x, y int) TouchActionLongPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionLongPress) WithXYFloat(x, y float64) TouchActionLongPress { + opt["x"] = x + opt["y"] = y + return opt +} + +func (opt TouchActionLongPress) WithElement(element WebElement) TouchActionLongPress { + opt["element"] = element.UID() + return opt +} diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go new file mode 100644 index 00000000..10b37d74 --- /dev/null +++ b/hrp/internal/uixt/ios_device.go @@ -0,0 +1,470 @@ +package uixt + +import ( + "bytes" + "context" + "encoding/base64" + builtinJSON "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "regexp" + "sync" + "time" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +const ( + // Changes the value of maximum depth for traversing elements source tree. + // It may help to prevent out of memory or timeout errors while getting the elements source tree, + // but it might restrict the depth of source tree. + // A part of elements source tree might be lost if the value was too small. Defaults to 50 + snapshotMaxDepth = 10 + // Allows to customize accept/dismiss alert button selector. + // It helps you to handle an arbitrary element as accept button in accept alert command. + // The selector should be a valid class chain expression, where the search root is the alert element itself. + // The default button location algorithm is used if the provided selector is wrong or does not match any element. + // e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`] + acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]" + dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" +) + +const ( + defaultPort = 8100 + defaultMjpegPort = 9100 +) + +func InitWDAClient(device *IOSDevice) (*DriverExt, error) { + var deviceOptions []IOSDeviceOption + if device.UDID != "" { + deviceOptions = append(deviceOptions, WithUDID(device.UDID)) + } + if device.Port != 0 { + deviceOptions = append(deviceOptions, WithPort(device.Port)) + } + if device.MjpegPort != 0 { + deviceOptions = append(deviceOptions, WithMjpegPort(device.MjpegPort)) + } + + // init wda device + targetDevice, err := NewDevice(deviceOptions...) + if err != nil { + return nil, err + } + + // switch to iOS springboard before init WDA session + // aviod getting stuck when some super app is activate such as douyin or wexin + log.Info().Msg("switch to iOS springboard") + bundleID := "com.apple.springboard" + _, err = targetDevice.GIDevice().AppLaunch(bundleID) + if err != nil { + return nil, errors.Wrap(err, "launch springboard failed") + } + + // init WDA driver + capabilities := NewCapabilities() + capabilities.WithDefaultAlertAction(AlertActionAccept) + driver, err := NewUSBDriver(capabilities, *targetDevice) + if err != nil { + return nil, errors.Wrap(err, "failed to init WDA driver") + } + driverExt, err := Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend WebDriver") + } + settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ + "snapshotMaxDepth": snapshotMaxDepth, + "acceptAlertButtonSelector": acceptAlertButtonSelector, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set appium WDA settings") + } + log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + + driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) + if device.LogOn { + err = driverExt.StartLogRecording("hrp_wda_log") + if err != nil { + return nil, err + } + } + + return driverExt, nil +} + +type Device interface { + UUID() string +} + +type IOSDevice struct { + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + + d giDevice.Device +} + +func (d IOSDevice) UUID() string { + return d.UDID +} + +func (d IOSDevice) GIDevice() giDevice.Device { + return d.d +} + +type IOSDeviceOption func(*IOSDevice) + +func WithUDID(udid string) IOSDeviceOption { + return func(device *IOSDevice) { + device.UDID = udid + } +} + +func WithPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.Port = port + } +} + +func WithMjpegPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.MjpegPort = port + } +} + +func WithLogOn(logOn bool) IOSDeviceOption { + return func(device *IOSDevice) { + device.LogOn = logOn + } +} + +func NewDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { + var usbmux giDevice.Usbmux + if usbmux, err = giDevice.NewUsbmux(); err != nil { + return nil, fmt.Errorf("init usbmux failed: %v", err) + } + + var deviceList []giDevice.Device + if deviceList, err = usbmux.Devices(); err != nil { + return nil, fmt.Errorf("get attached devices failed: %v", err) + } + + device = &IOSDevice{ + Port: defaultPort, + MjpegPort: defaultMjpegPort, + } + for _, option := range options { + option(device) + } + + serialNumber := device.UDID + for _, d := range deviceList { + // find device by serial number if specified + if serialNumber != "" && d.Properties().SerialNumber != serialNumber { + continue + } + + device.UDID = d.Properties().SerialNumber + device.d = d + return device, nil + } + + return nil, fmt.Errorf("device %s not found", device.UDID) +} + +func DeviceList() (devices []IOSDevice, err error) { + var usbmux giDevice.Usbmux + if usbmux, err = giDevice.NewUsbmux(); err != nil { + return nil, fmt.Errorf("usbmuxd: %w", err) + } + + var deviceList []giDevice.Device + if deviceList, err = usbmux.Devices(); err != nil { + return nil, fmt.Errorf("device list: %w", err) + } + + devices = make([]IOSDevice, len(deviceList)) + + for i := range devices { + devices[i].UDID = deviceList[i].Properties().SerialNumber + devices[i].Port = defaultPort + devices[i].MjpegPort = defaultMjpegPort + devices[i].d = deviceList[i] + } + + return +} + +// NewDriver creates new remote client, this will also start a new session. +func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (driver WebDriver, err error) { + if len(mjpegPort) == 0 { + mjpegPort = []int{defaultMjpegPort} + } + wd := new(remoteWD) + if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil { + return nil, err + } + var sessionInfo SessionInfo + if sessionInfo, err = wd.NewSession(capabilities); err != nil { + return nil, err + } + wd.sessionId = sessionInfo.SessionId + + if wd.mjpegConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), mjpegPort[0])); err != nil { + return nil, err + } + wd.mjpegClient = convertToHTTPClient(wd.mjpegConn) + + return wd, nil +} + +// NewUSBDriver creates new client via USB connected device, this will also start a new session. +func NewUSBDriver(capabilities Capabilities, device ...IOSDevice) (driver WebDriver, err error) { + if len(device) == 0 { + if device, err = DeviceList(); err != nil { + return nil, err + } + if len(device) == 0 { + return nil, errors.New("no device") + } + } + dev := device[0] + + wd := &remoteWD{ + usbCli: &struct { + httpCli *http.Client + defaultConn, mjpegConn giDevice.InnerConn + sync.Mutex + }{}, + } + if wd.usbCli.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { + return nil, fmt.Errorf("create connection: %w", err) + } + wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn()) + + if wd.usbCli.mjpegConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { + return nil, fmt.Errorf("create connection MJPEG: %w", err) + } + wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn()) + + if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil { + return nil, err + } + _, err = wd.NewSession(capabilities) + + go func() { + if DefaultKeepAliveInterval <= 0 { + return + } + ticker := time.NewTicker(DefaultKeepAliveInterval) + for { + <-ticker.C + if healthy, err := wd.IsHealthy(); err != nil || !healthy { + ticker.Stop() + return + } + } + }() + + return wd, err +} + +func newRequest(method string, url string, rawBody []byte) (request *http.Request, err error) { + header := map[string]string{ + "Content-Type": "application/json;charset=UTF-8", + "Accept": "application/json", + } + if request, err = http.NewRequest(method, url, bytes.NewBuffer(rawBody)); err != nil { + return nil, err + } + for k, v := range header { + request.Header.Set(k, v) + } + return +} + +func convertToHTTPClient(_conn net.Conn) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return _conn, nil + }, + }, + Timeout: 0, + } +} + +type wdaResponse struct { + Value string `json:"value"` + SessionID string `json:"sessionId"` +} + +func (dExt *DriverExt) StartLogRecording(identifier string) error { + log.Info().Msg("start WDA log recording") + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} + _, err := dExt.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + + return nil +} + +func (dExt *DriverExt) GetLogs() (string, error) { + log.Info().Msg("stop WDA log recording") + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + + return reply.Value, nil +} + +func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { + // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] + postJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/gtf/automation/log", dExt.host) + log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log") + resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON)) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode) + } + + rawResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + reply := new(wdaResponse) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + return reply, nil +} + +type rawResponse []byte + +func (r rawResponse) checkErr() (err error) { + reply := new(struct { + Value struct { + Err string `json:"error"` + Message string `json:"message"` + Traceback string `json:"traceback"` + } + }) + if err = json.Unmarshal(r, reply); err != nil { + return err + } + if reply.Value.Err != "" { + errText := reply.Value.Message + re := regexp.MustCompile(`{.+?=(.+?)}`) + if re.MatchString(reply.Value.Message) { + subMatch := re.FindStringSubmatch(reply.Value.Message) + errText = subMatch[len(subMatch)-1] + } + return fmt.Errorf("%s: %s", reply.Value.Err, errText) + } + return +} + +func (r rawResponse) valueConvertToString() (s string, err error) { + reply := new(struct{ Value string }) + if err = json.Unmarshal(r, reply); err != nil { + return "", err + } + s = reply.Value + return +} + +func (r rawResponse) valueConvertToBool() (b bool, err error) { + reply := new(struct{ Value bool }) + if err = json.Unmarshal(r, reply); err != nil { + return false, err + } + b = reply.Value + return +} + +func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) { + reply := new(struct{ Value struct{ SessionInfo } }) + if err = json.Unmarshal(r, reply); err != nil { + return SessionInfo{}, err + } + sessionInfo = reply.Value.SessionInfo + return +} + +func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) { + reply := new(struct{ Value builtinJSON.RawMessage }) + if err = json.Unmarshal(r, reply); err != nil { + return nil, err + } + raw = reply.Value + return +} + +func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) { + var str string + if str, err = r.valueConvertToString(); err != nil { + return nil, err + } + var decodeString []byte + if decodeString, err = base64.StdEncoding.DecodeString(str); err != nil { + return nil, err + } + raw = bytes.NewBuffer(decodeString) + return +} + +var errNoSuchElement = errors.New("no such element") + +func (r rawResponse) valueConvertToElementID() (id string, err error) { + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(r, reply); err != nil { + return "", err + } + if len(reply.Value) == 0 { + return "", errNoSuchElement + } + if id = elementIDFromValue(reply.Value); id == "" { + return "", fmt.Errorf("invalid element returned: %+v", reply) + } + return +} + +func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) { + reply := new(struct{ Value []map[string]string }) + if err = json.Unmarshal(r, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, errNoSuchElement + } + IDs = make([]string, len(reply.Value)) + for i, elem := range reply.Value { + var id string + if id = elementIDFromValue(elem); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + IDs[i] = id + } + return +} diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go new file mode 100644 index 00000000..a93b0bec --- /dev/null +++ b/hrp/internal/uixt/ios_test.go @@ -0,0 +1,1187 @@ +package uixt + +import ( + "bytes" + "fmt" + "math" + "testing" + "time" +) + +var ( + urlPrefix = "http://localhost:8100" + bundleId = "com.apple.Preferences" + driver WebDriver +) + +func setup(t *testing.T) { + var err error + driver, err = NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } +} + +func TestViaUSB(t *testing.T) { + devices, err := DeviceList() + if err != nil { + t.Fatal(err) + } + + drivers := make([]WebDriver, 0, len(devices)) + + for _, dev := range devices { + d, err := NewUSBDriver(nil, dev) + if err != nil { + t.Errorf("%s: %s", dev.UUID(), err) + continue + } + drivers = append(drivers, d) + } + + for _, d := range drivers { + t.Log(d.Status()) + } +} + +func TestNewDevice(t *testing.T) { + device, _ := NewDevice() + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithUDID("xxxx")) + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithPort(8700), WithMjpegPort(8800)) + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithUDID("xxxx"), WithPort(8700), WithMjpegPort(8800)) + if device != nil { + t.Log(device) + } +} + +func TestNewDriver(t *testing.T) { + var err error + driver, err = NewDriver(nil, urlPrefix) + if err != nil { + t.Fatal(err) + } +} + +func TestNewUSBDriver(t *testing.T) { + setup(t) + + // t.Log(driver.IsWdaHealthy()) +} + +func Test_remoteWD_NewSession(t *testing.T) { + setup(t) + + // sessionInfo, err := driver.NewSession(nil) + sessionInfo, err := driver.NewSession( + NewCapabilities().WithAppLaunchOption( + NewAppLaunchOption().WithBundleId(bundleId).WithArguments([]string{"-AppleLanguages", "(Russian)"}), + ), + ) + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_ActiveSession(t *testing.T) { + setup(t) + + sessionInfo, err := driver.ActiveSession() + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_DeleteSession(t *testing.T) { + setup(t) + + err := driver.DeleteSession() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_HealthCheck(t *testing.T) { + setup(t) + + err := driver.HealthCheck() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetAppiumSettings(t *testing.T) { + setup(t) + + settings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + t.Log(settings) +} + +func Test_remoteWD_SetAppiumSettings(t *testing.T) { + setup(t) + + const _acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','暂不'}`]" + const _dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" + + key := "acceptAlertButtonSelector" + value := _acceptAlertButtonSelector + + // settings, err := driver.SetAppiumSettings(map[string]interface{}{"dismissAlertButtonSelector": "暂不"}) + settings, err := driver.SetAppiumSettings(map[string]interface{}{key: value}) + if err != nil { + t.Fatal(err) + } + if settings[key] != value { + t.Fatal(settings[key]) + } +} + +func Test_remoteWD_IsWdaHealthy(t *testing.T) { + setup(t) + + healthy, err := driver.IsHealthy() + if err != nil { + t.Fatal(err) + } + if healthy == false { + t.Fatal("healthy =", healthy) + } +} + +// func Test_remoteWD_WdaShutdown(t *testing.T) { +// setup(t) +// +// if err := driver.WdaShutdown(); err != nil { +// t.Fatal(err) +// } +// } + +func Test_remoteWD_Status(t *testing.T) { + setup(t) + + status, err := driver.Status() + if err != nil { + t.Fatal(err) + } + if status.Ready == false { + t.Fatal("deviceStatus =", status) + } +} + +func Test_remoteWD_DeviceInfo(t *testing.T) { + setup(t) + + info, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + if len(info.Model) == 0 { + t.Fatal(info) + } +} + +func Test_remoteWD_BatteryInfo(t *testing.T) { + setup(t) + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal() + } + t.Log(batteryInfo) +} + +func Test_remoteWD_WindowSize(t *testing.T) { + setup(t) + + size, err := driver.WindowSize() + if err != nil { + t.Fatal() + } + t.Log(size) +} + +func Test_remoteWD_Screen(t *testing.T) { + setup(t) + + screen, err := driver.Screen() + if err != nil { + t.Fatal(err) + } + t.Log(screen) +} + +func Test_remoteWD_ActiveAppInfo(t *testing.T) { + setup(t) + + appInfo, err := driver.ActiveAppInfo() + if err != nil { + t.Fatal(err) + } + if len(appInfo.BundleId) == 0 { + t.Fatal(appInfo) + } + t.Log(appInfo) +} + +func Test_remoteWD_ActiveAppsList(t *testing.T) { + setup(t) + + appsList, err := driver.ActiveAppsList() + if err != nil { + t.Fatal(err) + } + if len(appsList) == 0 { + t.Fatal(appsList) + } + t.Log(appsList) +} + +func Test_remoteWD_AppState(t *testing.T) { + setup(t) + + runState, err := driver.AppState(bundleId) + if err != nil { + t.Fatal(err) + } + t.Log(runState) +} + +func Test_remoteWD_IsLocked(t *testing.T) { + setup(t) + + locked, err := driver.IsLocked() + if err != nil { + t.Fatal(err) + } + t.Log(locked) +} + +func Test_remoteWD_Unlock(t *testing.T) { + setup(t) + + err := driver.Unlock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Lock(t *testing.T) { + setup(t) + + err := driver.Lock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertText(t *testing.T) { + setup(t) + + text, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + _ = text + t.Log(text) +} + +func Test_remoteWD_AlertButtons(t *testing.T) { + setup(t) + + btnLabels, err := driver.AlertButtons() + if err != nil { + t.Fatal(err) + } + t.Log(btnLabels) +} + +func Test_remoteWD_AlertAccept(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertAccept() + // err := driver.AlertAccept("") + // err := driver.AlertAccept("好") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertDismiss(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertDismiss() + // err := driver.AlertDismiss("") + // err := driver.AlertDismiss("不允许") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertSendKeys(t *testing.T) { + setup(t) + + err := driver.AlertSendKeys("todo") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Homescreen(t *testing.T) { + setup(t) + + err := driver.Homescreen() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunch(t *testing.T) { + setup(t) + + // SetDebug(true) + + // bundleId = "com.hustlzp.xcz" + // bundleId = "com.github.stormbreaker.prod" + // bundleId = "com.360buy.jdmobile" + // bundleId = "com.zhihu.ios" + // bundleId = "com.tencent.xin" + // bundleId = "com.jsmcc.ZP7267A6ES" + err := driver.AppLaunch(bundleId) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithShouldWaitForQuiescence(true)) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithArguments([]string{"-AppleLanguages", "(Russian)"})) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunchUnattached(t *testing.T) { + setup(t) + + err := driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppTerminate(t *testing.T) { + setup(t) + + _, err := driver.AppTerminate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppActivate(t *testing.T) { + setup(t) + + err := driver.AppActivate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppDeactivate(t *testing.T) { + setup(t) + + err := driver.AppDeactivate(2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppAuthReset(t *testing.T) { + setup(t) + + err := driver.AppAuthReset(ProtectedResourceCamera) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Tap(t *testing.T) { + setup(t) + + err := driver.Tap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_DoubleTap(t *testing.T) { + setup(t) + + err := driver.DoubleTap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_TouchAndHold(t *testing.T) { + setup(t) + + // err := driver.TouchAndHold(200, 300) + err := driver.TouchAndHold(200, 300, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Drag(t *testing.T) { + setup(t) + + // err := driver.Drag(200, 300, 200, 500, WithPressDuration(0.5)) + err := driver.Swipe(200, 300, 200, 500) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ForceTouch(t *testing.T) { + setup(t) + + err := driver.ForceTouch(256, 400, 0.8, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SetPasteboard(t *testing.T) { + setup(t) + + // err := driver.SetPasteboard(PasteboardTypePlaintext, "gwda") + err := driver.SetPasteboard(PasteboardTypeUrl, "Clock-stopwatch://") + // userHomeDir, _ := os.UserHomeDir() + // bytesImg, _ := ioutil.ReadFile(userHomeDir + "/Pictures/IMG_0806.jpg") + // err := driver.SetPasteboard(PasteboardTypeImage, string(bytesImg)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetPasteboard(t *testing.T) { + setup(t) + + var buffer *bytes.Buffer + var err error + + buffer, err = driver.GetPasteboard(PasteboardTypePlaintext) + // buffer, err = driver.GetPasteboard(PasteboardTypeUrl) + if err != nil { + t.Fatal(err) + } + t.Log(buffer.String()) + + // buffer, err = driver.GetPasteboard(PasteboardTypeImage) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // if err = ioutil.WriteFile(userHomeDir+"/Desktop/p1.png", buffer.Bytes(), 0600); err != nil { + // t.Error(err) + // } +} + +func Test_remoteWD_SendKeys(t *testing.T) { + setup(t) + + err := driver.SendKeys("App Store") + // err := driver.SendKeys("App Store", WithFrequency(3)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PressButton(t *testing.T) { + setup(t) + + err := driver.PressButton(DeviceButtonVolumeUp) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonVolumeDown) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonHome) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriActivate(t *testing.T) { + setup(t) + + err := driver.SiriActivate("What's the weather like today") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriOpenUrl(t *testing.T) { + setup(t) + + err := driver.SiriOpenUrl("Prefs:root=Bluetooth") + // err := driver.SiriOpenUrl("Prefs:root=WIFI![]()") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Orientation(t *testing.T) { + setup(t) + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + if orientation == "" { + t.Fatal(orientation) + } +} + +func Test_remoteWD_SetOrientation(t *testing.T) { + setup(t) + + var err error + err = driver.SetOrientation(OrientationLandscapeLeft) + err = driver.SetOrientation(OrientationLandscapeRight) + err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Rotation(t *testing.T) { + setup(t) + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal() + } + t.Log(rotation) +} + +func Test_remoteWD_SetRotation(t *testing.T) { + setup(t) + + err := driver.SetRotation(Rotation{X: 0, Y: 0, Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformW3CActions(t *testing.T) { + // setup(t) + // actions := NewW3CActions().SendKeys("App Store") + + element := setupElement(t, BySelector{Name: "touchableView"}) + actions := NewW3CActions().FingerAction( + NewFingerAction(). + Move(NewFingerMove().WithXY(-15, -85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + NewFingerAction(). + Move(NewFingerMove().WithXY(15, 85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + ) + err := driver.PerformW3CActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformAppiumTouchActions(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + actions := NewTouchActions(). + Press(NewTouchActionPress().WithElement(element).WithXY(100, 150).WithPressure(0.2)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithXY(300, 150)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element).WithXY(300, 400)). + Release() + + err := driver.PerformAppiumTouchActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ActiveElement(t *testing.T) { + setup(t) + + element, err := driver.ActiveElement() + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElement(t *testing.T) { + setup(t) + + element, err := driver.FindElement(BySelector{Name: "设置"}) + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElements(t *testing.T) { + setup(t) + + elements, err := driver.FindElements(BySelector{ClassName: ElementType{Icon: true}}) + if err != nil { + t.Fatal(err) + } + _ = elements + t.Log(elements) +} + +func Test_remoteWD_Screenshot(t *testing.T) { + setup(t) + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} + +func Test_remoteWD_Source(t *testing.T) { + setup(t) + + var source string + var err error + + // source, err = driver.Source() + // if err != nil { + // t.Fatal(err) + // } + + source, err = driver.Source(NewSourceOption().WithScope("AppiumAUT")) + if err != nil { + t.Fatal(err) + } + + // source, err = driver.Source(NewSourceOption().WithFormatAsJson()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsDescription()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsXml().WithExcludedAttributes([]string{"label", "type", "index"})) + // if err != nil { + // t.Fatal(err) + // } + + _ = source + fmt.Println(source) +} + +func Test_remoteWD_AccessibleSource(t *testing.T) { + setup(t) + + source, err := driver.AccessibleSource() + if err != nil { + t.Fatal(err) + } + _ = source + fmt.Println(source) +} + +func Test_remoteWD_Wait(t *testing.T) { + setup(t) + + var element WebElement + var err error + + by := BySelector{Name: "通知"} + // driver.AppLaunch() + exists := func(d WebDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + _ = exists + _ = element + + err = driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } + // element, err = driver.FindElement(by) + err = driver.WaitWithTimeoutAndInterval(exists, time.Second*10, time.Millisecond*10) + if err != nil { + t.Fatal(err) + } + + // t.Log(element.Rect()) +} + +func Test_remoteWD_Location(t *testing.T) { + setup(t) + + location, err := driver.Location() + if err != nil { + t.Fatal(err) + } + t.Log(location) +} + +func Test_remoteWD_KeyboardDismiss(t *testing.T) { + setup(t) + + err := driver.KeyboardDismiss() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ExpectNotification(t *testing.T) { + setup(t) + + // bundleId = "com.apple.shortcuts" + // err := driver.ExpectNotification("shortcuts", NotificationTypePlain, 10) + // if err != nil { + // t.Fatal(err) + // } +} + +func Test_remoteWD_IOHIDEvent(t *testing.T) { + setup(t) + + err := driver.IOHIDEvent(EventPageIDConsumer, EventUsageIDCsmrVolumeDown) + if err != nil { + t.Fatal(err) + } +} + +func setupElement(t *testing.T, by BySelector) WebElement { + setup(t) + element, err := driver.FindElement(by) + if err != nil { + t.Fatal(err) + } + return element +} + +func Test_remoteWE_Click(t *testing.T) { + element := setupElement(t, BySelector{LinkText: NewElementAttribute().WithLabel("设置")}) + + err := element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SendKeys(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.SendKeys("App Store") + // err := element.SendKeys("App Store", 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Clear(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.Clear() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Tap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.Tap(10, 20) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_DoubleTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.DoubleTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TouchAndHold(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TouchAndHold(-1) + // err := element.TouchAndHold(5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TwoFingerTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TwoFingerTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TapWithNumberOfTaps(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TapWithNumberOfTaps(3, 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_ForceTouch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.ForceTouch(1, -1) + err := element.ForceTouchFloat(10, 20, 1, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Drag(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.Drag(10, 20, 10, 300, -1) + err := element.Swipe(10, 20, 10, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SwipeDirection(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.SwipeDirection(DirectionUp, -1) + err := element.SwipeDirection(DirectionDown, 120) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Pinch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // zoom in + // err := element.Pinch(2,10) + // zoom out + err := element.Pinch(0.9, -4.5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PinchToZoomOutByW3CAction(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.PinchToZoomOutByW3CAction(15) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rotate(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // 90 CW + // err := element.Rotate(math.Pi / 2) + // 180 CCW + err := element.Rotate(math.Pi * -2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PickerWheelSelect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{PickerWheel: true}}) + + err := element.PickerWheelSelect(PickerWheelOrderNext, 3) + if err != nil { + t.Fatal(err) + } + err = element.PickerWheelSelect(PickerWheelOrderPrevious) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_scroll(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + // err = element.ScrollElementByName("电池") + // err = element.ScrollElementByPredicate("type == 'XCUIElementTypeCell' AND name LIKE 'Safari*'") + err = element.ScrollDirection(DirectionDown, 0.8) + + // element, err = driver.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + // if err != nil { + // t.Fatal(err) + // } + // err = element.ScrollToVisible() + + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElement(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + element, err = element.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + if err != nil { + t.Fatal(err) + } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElements(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + elements, err := element.FindElements(BySelector{ClassName: ElementType{Cell: true}}) + if err != nil { + t.Fatal(err) + } + + err = elements[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindVisibleCells(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + cells, err := element.FindVisibleCells() + if err != nil { + t.Fatal(err) + } + + err = cells[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + rect, err := element.Rect() + if err != nil { + t.Fatal(err) + } + location, err := element.Location() + if err != nil { + t.Fatal(err) + } + size, err := element.Size() + if err != nil { + t.Fatal(err) + } + _, _, _ = rect, location, size + t.Log(rect, location, size) +} + +func Test_remoteWE_Text(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + text, err := element.Text() + if err != nil { + t.Fatal(err) + } + _ = text + // t.Log(text) +} + +func Test_remoteWE_Type(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + elemType, err := element.Type() + if err != nil { + t.Fatal(err) + } + _ = elemType + // t.Log(elemType) +} + +func Test_remoteWE_IsEnabled(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + enabled, err := element.IsEnabled() + if err != nil { + t.Fatal(err) + } + _ = enabled + // t.Log(enabled) +} + +func Test_remoteWE_IsDisplayed(t *testing.T) { + element := setupElement(t, BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + + displayed, err := element.IsDisplayed() + if err != nil { + t.Fatal(err) + } + _ = displayed + // t.Log(displayed) +} + +func Test_remoteWE_IsSelected(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + // element := setupElement(t, BySelector{Name: "添加到主屏幕"}) + // element := setupElement(t, BySelector{Name: "仅App资源库"}) + + selected, err := element.IsSelected() + if err != nil { + t.Fatal(err) + } + _ = selected + // t.Log(selected) +} + +func Test_remoteWE_IsAccessible(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + accessible, err := element.IsAccessible() + if err != nil { + t.Fatal(err) + } + _ = accessible + // t.Log(accessible) +} + +func Test_remoteWE_IsAccessibilityContainer(t *testing.T) { + // element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + isAccessibilityContainer, err := element.IsAccessibilityContainer() + if err != nil { + t.Fatal(err) + } + _ = isAccessibilityContainer + // t.Log(isAccessibilityContainer) +} + +func Test_remoteWE_GetAttribute(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{StaticText: true}}) + + value, err := element.GetAttribute(NewElementAttribute().WithValue("")) + if err != nil { + t.Fatal(err) + } + _ = value + // t.Log(value) +} + +func Test_remoteWE_Screenshot(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{TextView: true}}) + + screenshot, err := element.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/e1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} diff --git a/hrp/internal/uixt/ios_webdriver.go b/hrp/internal/uixt/ios_webdriver.go new file mode 100644 index 00000000..fb1c5337 --- /dev/null +++ b/hrp/internal/uixt/ios_webdriver.go @@ -0,0 +1,928 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + builtinJSON "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "sync" + "time" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +// var _ WebDriver = (*remoteWD)(nil) + +type remoteWD struct { + urlPrefix *url.URL + sessionId string + + usbCli *struct { + httpCli *http.Client + defaultConn, mjpegConn giDevice.InnerConn + sync.Mutex + } + + mjpegClient *http.Client + mjpegConn net.Conn +} + +func (wd *remoteWD) _requestURL(tmpURL *url.URL, elem ...string) string { + var tmp *url.URL + if tmpURL == nil { + tmpURL = wd.urlPrefix + } + tmp, _ = url.Parse(tmpURL.String()) + tmp.Path = path.Join(append([]string{tmpURL.Path}, elem...)...) + return tmp.String() +} + +func (wd *remoteWD) executeGet(pathElem ...string) (rawResp rawResponse, err error) { + return wd.executeHTTP(http.MethodGet, wd._requestURL(nil, pathElem...), nil) +} + +func (wd *remoteWD) executePost(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return wd.executeHTTP(http.MethodPost, wd._requestURL(nil, pathElem...), bsJSON) +} + +func (wd *remoteWD) executeDelete(pathElem ...string) (rawResp rawResponse, err error) { + return wd.executeHTTP(http.MethodDelete, wd._requestURL(nil, pathElem...), nil) +} + +func (wd *remoteWD) GetMjpegHTTPClient() *http.Client { + return wd.mjpegClient +} + +func (wd *remoteWD) Close() error { + if wd.usbCli == nil { + wd.mjpegClient.CloseIdleConnections() + return wd.mjpegConn.Close() + } + + wd.usbCli.Lock() + defer wd.usbCli.Unlock() + + if wd.usbCli.defaultConn != nil { + wd.usbCli.defaultConn.Close() + } + if wd.usbCli.mjpegConn != nil { + wd.usbCli.mjpegConn.Close() + } + return nil +} + +func (wd *remoteWD) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { + // [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)] + data := make(map[string]interface{}) + if len(capabilities) == 0 { + data["capabilities"] = make(map[string]interface{}) + } else { + data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities} + } + + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session"); err != nil { + return SessionInfo{}, err + } + if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { + return SessionInfo{}, err + } + wd.sessionId = sessionInfo.SessionId + return +} + +func (wd *remoteWD) ActiveSession() (sessionInfo SessionInfo, err error) { + // [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId); err != nil { + return SessionInfo{}, err + } + if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { + return SessionInfo{}, err + } + return +} + +func (wd *remoteWD) DeleteSession() (err error) { + // [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)] + _, err = wd.executeDelete("/session", wd.sessionId) + return +} + +func (wd *remoteWD) Status() (deviceStatus DeviceStatus, err error) { + // [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/status"); err != nil { + return DeviceStatus{}, err + } + reply := new(struct{ Value struct{ DeviceStatus } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceStatus{}, err + } + deviceStatus = reply.Value.DeviceStatus + return +} + +func (wd *remoteWD) DeviceInfo() (deviceInfo DeviceInfo, err error) { + // [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)] + // [[FBRoute GET:@"/wda/device/info"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/info"); err != nil { + return DeviceInfo{}, err + } + reply := new(struct{ Value struct{ DeviceInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceInfo{}, err + } + deviceInfo = reply.Value.DeviceInfo + return +} + +func (wd *remoteWD) Location() (location Location, err error) { + // [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)] + // [[FBRoute GET:@"/wda/device/location"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/location"); err != nil { + return Location{}, err + } + reply := new(struct{ Value struct{ Location } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Location{}, err + } + location = reply.Value.Location + return +} + +func (wd *remoteWD) BatteryInfo() (batteryInfo BatteryInfo, err error) { + // [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/batteryInfo"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value struct{ BatteryInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + batteryInfo = reply.Value.BatteryInfo + return +} + +func (wd *remoteWD) WindowSize() (size Size, err error) { + // [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/window/size"); err != nil { + return Size{}, err + } + reply := new(struct{ Value struct{ Size } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + size = reply.Value.Size + return +} + +func (wd *remoteWD) Screen() (screen Screen, err error) { + // [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/screen"); err != nil { + return Screen{}, err + } + reply := new(struct{ Value struct{ Screen } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Screen{}, err + } + screen = reply.Value.Screen + return +} + +func (wd *remoteWD) Scale() (float64, error) { + screen, err := wd.Screen() + if err != nil { + return 0, err + } + return screen.Scale, nil +} + +func (wd *remoteWD) ActiveAppInfo() (info AppInfo, err error) { + // [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)] + // [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil { + return AppInfo{}, err + } + reply := new(struct{ Value struct{ AppInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return AppInfo{}, err + } + info = reply.Value.AppInfo + return +} + +func (wd *remoteWD) ActiveAppsList() (appsList []AppBaseInfo, err error) { + // [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/apps/list"); err != nil { + return nil, err + } + reply := new(struct{ Value []AppBaseInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + appsList = reply.Value + return +} + +func (wd *remoteWD) AppState(bundleId string) (runState AppState, err error) { + // [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)] + data := map[string]interface{}{"bundleId": bundleId} + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil { + return 0, err + } + reply := new(struct{ Value AppState }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + runState = reply.Value + _ = rawResp + return +} + +func (wd *remoteWD) IsLocked() (locked bool, err error) { + // [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)] + // [[FBRoute GET:@"/wda/locked"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/locked"); err != nil { + return false, err + } + if locked, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (wd *remoteWD) Unlock() (err error) { + // [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)] + // [[FBRoute POST:@"/wda/unlock"].withoutSession + _, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/unlock") + return +} + +func (wd *remoteWD) Lock() (err error) { + // [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)] + // [[FBRoute POST:@"/wda/lock"].withoutSession + _, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/lock") + return +} + +func (wd *remoteWD) Homescreen() (err error) { + // [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)] + _, err = wd.executePost(nil, "/wda/homescreen") + return +} + +func (wd *remoteWD) AlertText() (text string, err error) { + // [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)] + // [[FBRoute GET:@"/alert/text"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/alert/text"); err != nil { + return "", err + } + if text, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (wd *remoteWD) AlertButtons() (btnLabels []string, err error) { + // [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/alert/buttons"); err != nil { + return nil, err + } + reply := new(struct{ Value []string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + btnLabels = reply.Value + return +} + +func (wd *remoteWD) AlertAccept(label ...string) (err error) { + // [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)] + // [[FBRoute POST:@"/alert/accept"].withoutSession + data := make(map[string]interface{}) + if len(label) != 0 && label[0] != "" { + data["name"] = label[0] + } + _, err = wd.executePost(data, "/alert/accept") + return +} + +func (wd *remoteWD) AlertDismiss(label ...string) (err error) { + // [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)] + // [[FBRoute POST:@"/alert/dismiss"].withoutSession + data := make(map[string]interface{}) + if len(label) != 0 && label[0] != "" { + data["name"] = label[0] + } + _, err = wd.executePost(data, "/alert/dismiss") + return +} + +func (wd *remoteWD) AlertSendKeys(text string) (err error) { + // [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)] + data := map[string]interface{}{"value": strings.Split(text, "")} + _, err = wd.executePost(data, "/session", wd.sessionId, "/alert/text") + return +} + +func (wd *remoteWD) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { + // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] + data := make(map[string]interface{}) + if len(launchOpt) != 0 { + data = launchOpt[0] + } + data["bundleId"] = bundleId + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/launch") + return +} + +func (wd *remoteWD) AppLaunchUnattached(bundleId string) (err error) { + // [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)] + data := map[string]interface{}{"bundleId": bundleId} + _, err = wd.executePost(data, "/wda/apps/launchUnattached") + return +} + +func (wd *remoteWD) AppTerminate(bundleId string) (successful bool, err error) { + // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] + data := map[string]interface{}{"bundleId": bundleId} + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil { + return false, err + } + if successful, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (wd *remoteWD) AppActivate(bundleId string) (err error) { + // [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)] + data := map[string]interface{}{"bundleId": bundleId} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/activate") + return +} + +func (wd *remoteWD) AppDeactivate(second float64) (err error) { + // [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)] + if second < 3 { + second = 3.0 + } + data := map[string]interface{}{"duration": second} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/deactivateApp") + return +} + +func (wd *remoteWD) AppAuthReset(resource ProtectedResource) (err error) { + // [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)] + data := map[string]interface{}{"resource": resource} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/resetAppAuth") + return +} + +func (wd *remoteWD) Tap(x, y int, options ...DataOption) error { + return wd.TapFloat(float64(x), float64(y), options...) +} + +func (wd *remoteWD) TapFloat(x, y float64, options ...DataOption) (err error) { + // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + // append options in post data for extra WDA configurations + // e.g. add identifier in tap event logs + for _, option := range options { + option(data) + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/tap/0") + return +} + +func (wd *remoteWD) DoubleTap(x, y int) error { + return wd.DoubleTapFloat(float64(x), float64(y)) +} + +func (wd *remoteWD) DoubleTapFloat(x, y float64) (err error) { + // [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/doubleTap") + return +} + +func (wd *remoteWD) TouchAndHold(x, y int, second ...float64) error { + return wd.TouchAndHoldFloat(float64(x), float64(y), second...) +} + +func (wd *remoteWD) TouchAndHoldFloat(x, y float64, second ...float64) (err error) { + // [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["duration"] = second[0] + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touchAndHold") + return +} + +func (wd *remoteWD) Drag(fromX, fromY, toX, toY int, options ...DataOption) error { + return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (wd *remoteWD) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) { + // [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)] + data := map[string]interface{}{ + "fromX": fromX, + "fromY": fromY, + "toX": toX, + "toY": toY, + } + + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) + } + + if _, ok := data["duration"]; !ok { + data["duration"] = 1.0 // default duration + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/dragfromtoforduration") + return +} + +func (wd *remoteWD) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { + options = append(options, WithPressDuration(0)) + return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (wd *remoteWD) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { + options = append(options, WithPressDuration(0)) + return wd.DragFloat(fromX, fromY, toX, toY, options...) +} + +func (wd *remoteWD) ForceTouch(x, y int, pressure float64, second ...float64) error { + return wd.ForceTouchFloat(float64(x), float64(y), pressure, second...) +} + +func (wd *remoteWD) ForceTouchFloat(x, y, pressure float64, second ...float64) error { + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + actions := NewTouchActions(). + Press( + NewTouchActionPress().WithXYFloat(x, y).WithPressure(pressure)). + Wait(second[0]). + Release() + return wd.PerformAppiumTouchActions(actions) +} + +func (wd *remoteWD) PerformW3CActions(actions *W3CActions) (err error) { + // [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)] + data := map[string]interface{}{"actions": actions} + _, err = wd.executePost(data, "/session", wd.sessionId, "/actions") + return +} + +func (wd *remoteWD) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { + // [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)] + // [[FBRoute POST:@"/wda/touch/multi/perform"] + data := map[string]interface{}{"actions": touchActs} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch/multi/perform") + return +} + +func (wd *remoteWD) SetPasteboard(contentType PasteboardType, content string) (err error) { + // [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)] + data := map[string]interface{}{ + "contentType": contentType, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/setPasteboard") + return +} + +func (wd *remoteWD) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { + // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] + data := map[string]interface{}{"contentType": contentType} + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil { + return nil, err + } + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, err + } + return +} + +func (wd *remoteWD) SendKeys(text string, options ...DataOption) (err error) { + // [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] + data := map[string]interface{}{"value": strings.Split(text, "")} + + // append options in post data for extra WDA configurations + // e.g. use WithFrequency to set frequency of typing + for _, option := range options { + option(data) + } + + if _, ok := data["frequency"]; !ok { + data["frequency"] = 60 // default frequency + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keys") + return +} + +func (wd *remoteWD) KeyboardDismiss(keyNames ...string) (err error) { + // [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)] + if len(keyNames) == 0 { + keyNames = []string{"return"} + } + data := map[string]interface{}{"keyNames": keyNames} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keyboard/dismiss") + return +} + +func (wd *remoteWD) PressButton(devBtn DeviceButton) (err error) { + // [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)] + data := map[string]interface{}{"name": devBtn} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/pressButton") + return +} + +func (wd *remoteWD) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { + // [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)] + if len(duration) == 0 || duration[0] <= 0 { + duration = []float64{0.005} + } + data := map[string]interface{}{ + "page": pageID, + "usage": usageID, + "duration": duration[0], + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/performIoHidEvent") + return +} + +func (wd *remoteWD) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { + // [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)] + if len(second) == 0 { + second = []int{60} + } + data := map[string]interface{}{ + "name": notifyName, + "type": notifyType, + "timeout": second[0], + } + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/expectNotification") + return +} + +func (wd *remoteWD) SiriActivate(text string) (err error) { + // [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)] + data := map[string]interface{}{"text": text} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/siri/activate") + return +} + +func (wd *remoteWD) SiriOpenUrl(url string) (err error) { + // [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)] + data := map[string]interface{}{"url": url} + _, err = wd.executePost(data, "/session", wd.sessionId, "/url") + return +} + +func (wd *remoteWD) Orientation() (orientation Orientation, err error) { + // [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/orientation"); err != nil { + return "", err + } + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + orientation = reply.Value + return +} + +func (wd *remoteWD) SetOrientation(orientation Orientation) (err error) { + // [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)] + data := map[string]interface{}{"orientation": orientation} + _, err = wd.executePost(data, "/session", wd.sessionId, "/orientation") + return +} + +func (wd *remoteWD) Rotation() (rotation Rotation, err error) { + // [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/rotation"); err != nil { + return Rotation{}, err + } + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + rotation = reply.Value + return +} + +func (wd *remoteWD) SetRotation(rotation Rotation) (err error) { + // [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)] + _, err = wd.executePost(rotation, "/session", wd.sessionId, "/rotation") + return +} + +func (wd *remoteWD) MatchTouchID(isMatch bool) (err error) { + // [FBRoute POST:@"/wda/touch_id"] + data := map[string]interface{}{"match": isMatch} + _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch_id") + return +} + +func (wd *remoteWD) ActiveElement() (element WebElement, err error) { + // [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/element/active"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + return nil, err + } + element = &remoteWE{parent: wd, id: elementID} + return +} + +func (wd *remoteWD) FindElement(by BySelector) (element WebElement, err error) { + // [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/element"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + element = &remoteWE{parent: wd, id: elementID} + return +} + +func (wd *remoteWD) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/elements"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &remoteWE{parent: wd, id: elementIDs[i]} + } + return +} + +func (wd *remoteWD) Screenshot() (raw *bytes.Buffer, err error) { + // [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)] + // [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/screenshot"); err != nil { + return nil, err + } + + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, err + } + return +} + +func (wd *remoteWD) Source(srcOpt ...SourceOption) (source string, err error) { + // [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)] + // [[FBRoute GET:@"/source"].withoutSession + tmp, _ := url.Parse(wd._requestURL(nil, "/session", wd.sessionId)) + toJsonRaw := false + if len(srcOpt) != 0 { + q := tmp.Query() + for k, val := range srcOpt[0] { + v := val.(string) + q.Set(k, v) + if k == "format" && v == "json" { + toJsonRaw = true + } + } + tmp.RawQuery = q.Encode() + } + + var rawResp rawResponse + if rawResp, err = wd.executeHTTP(http.MethodGet, wd._requestURL(tmp, "/source"), nil); err != nil { + return "", nil + } + if toJsonRaw { + var jr builtinJSON.RawMessage + if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil { + return "", err + } + return string(jr), nil + } + if source, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (wd *remoteWD) AccessibleSource() (source string, err error) { + // [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)] + // [[FBRoute GET:@"/wda/accessibleSource"].withoutSession + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/accessibleSource"); err != nil { + return "", err + } + var jr builtinJSON.RawMessage + if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil { + return "", err + } + source = string(jr) + return +} + +func (wd *remoteWD) HealthCheck() (err error) { + // [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)] + _, err = wd.executeGet("/wda/healthcheck") + return +} + +func (wd *remoteWD) GetAppiumSettings() (settings map[string]interface{}, err error) { + // [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)] + var rawResp rawResponse + if rawResp, err = wd.executeGet("/session", wd.sessionId, "/appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + settings = reply.Value + return +} + +func (wd *remoteWD) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { + // [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)] + data := map[string]interface{}{"settings": settings} + var rawResp rawResponse + if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + ret = reply.Value + return +} + +func (wd *remoteWD) IsHealthy() (healthy bool, err error) { + var rawResp rawResponse + if rawResp, err = wd.executeGet("/health"); err != nil { + return false, err + } + if string(rawResp) != "I-AM-ALIVE" { + return false, nil + } + return true, nil +} + +func (wd *remoteWD) WdaShutdown() (err error) { + _, err = wd.executeGet("/wda/shutdown") + return +} + +func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { + startTime := time.Now() + for { + done, err := condition(wd) + if err != nil { + return err + } + if done { + return nil + } + + if elapsed := time.Since(startTime); elapsed > timeout { + return fmt.Errorf("timeout after %v", elapsed) + } + time.Sleep(interval) + } +} + +func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { + return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) +} + +func (wd *remoteWD) Wait(condition Condition) error { + return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +} + +// HTTPClient The default client to use to communicate with the WebDriver server. +var HTTPClient = http.DefaultClient + +func (wd *remoteWD) executeHTTP(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { + log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") + var req *http.Request + if req, err = newRequest(method, rawURL, rawBody); err != nil { + return + } + + var httpCli *http.Client + if wd.usbCli != nil { + wd.usbCli.Lock() + defer wd.usbCli.Unlock() + httpCli = wd.usbCli.httpCli + } else { + httpCli = HTTPClient + } + httpCli.Timeout = 0 + + start := time.Now() + var resp *http.Response + if resp, err = httpCli.Do(req); err != nil { + return nil, err + } + defer func() { + // https://github.com/etcd-io/etcd/blob/v3.3.25/pkg/httputil/httputil.go#L16-L22 + _, _ = io.Copy(ioutil.Discard, resp.Body) + _ = resp.Body.Close() + }() + + rawResp, err = ioutil.ReadAll(resp.Body) + logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String()) + if !strings.HasSuffix(rawURL, "screenshot") { + // avoid printing screenshot data + logger.Bytes("response", rawResp) + } + logger.Msg("get WDA response") + if err != nil { + return nil, err + } + + if err = rawResp.checkErr(); err != nil { + if resp.StatusCode == http.StatusOK { + return rawResp, nil + } + return nil, err + } + + return +} diff --git a/hrp/internal/uixt/ios_webelement.go b/hrp/internal/uixt/ios_webelement.go new file mode 100644 index 00000000..805a67c2 --- /dev/null +++ b/hrp/internal/uixt/ios_webelement.go @@ -0,0 +1,477 @@ +package uixt + +import ( + "bytes" + "fmt" + "math" + "strings" + + "github.com/pkg/errors" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +// All elements returned by search endpoints have assigned element_id. +// Given element_id you can query properties like: +// enabled, rect, size, location, text, displayed, accessible, name +type remoteWE struct { + parent *remoteWD + id string // element_id +} + +func (we remoteWE) Click() (err error) { + // [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)] + _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/click") + return +} + +func (we remoteWE) SendKeys(text string, frequency ...int) (err error) { + // [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)] + data := map[string]interface{}{"value": strings.Split(text, "")} + if len(frequency) == 0 || frequency[0] <= 0 { + frequency = []int{60} + } + data["frequency"] = frequency[0] + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/value") + return +} + +func (we remoteWE) Clear() (err error) { + // [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)] + _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear") + return +} + +func (we remoteWE) Tap(x, y int) error { + return we.TapFloat(float64(x), float64(y)) +} + +func (we remoteWE) TapFloat(x, y float64) (err error) { + // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/tap/", we.id) + return +} + +func (we remoteWE) DoubleTap() (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)] + _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap") + return +} + +func (we remoteWE) TouchAndHold(second ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)] + data := make(map[string]interface{}) + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["duration"] = second[0] + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold") + return +} + +func (we remoteWE) TwoFingerTap() (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)] + _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap") + return +} + +func (we remoteWE) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)] + if numberOfTouches <= 0 { + return errors.New("'numberOfTouches' must be greater than zero") + } + if numberOfTouches > 5 { + return errors.New("'numberOfTouches' cannot be greater than 5") + } + if numberOfTaps <= 0 { + return errors.New("'numberOfTaps' must be greater than zero") + } + if numberOfTaps > 10 { + return errors.New("'numberOfTaps' cannot be greater than 10") + } + data := map[string]interface{}{ + "numberOfTaps": numberOfTaps, + "numberOfTouches": numberOfTouches, + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps") + return +} + +func (we remoteWE) ForceTouch(pressure float64, second ...float64) (err error) { + return we.ForceTouchFloat(-1, -1, pressure, second...) +} + +func (we remoteWE) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)] + data := make(map[string]interface{}) + if x != -1 && y != -1 { + data["x"] = x + data["y"] = y + } + if len(second) == 0 || second[0] <= 0 { + second = []float64{1.0} + } + data["pressure"] = pressure + data["duration"] = second[0] + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch") + return +} + +func (we remoteWE) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error { + return we.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), pressForDuration...) +} + +func (we remoteWE) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)] + data := map[string]interface{}{ + "fromX": fromX, + "fromY": fromY, + "toX": toX, + "toY": toY, + } + if len(pressForDuration) == 0 || pressForDuration[0] < 0 { + pressForDuration = []float64{1.0} + } + data["duration"] = pressForDuration[0] + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration") + return +} + +func (we remoteWE) Swipe(fromX, fromY, toX, toY int) error { + return we.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY)) +} + +func (we remoteWE) SwipeFloat(fromX, fromY, toX, toY float64) error { + return we.DragFloat(fromX, fromY, toX, toY, 0) +} + +func (we remoteWE) SwipeDirection(direction Direction, velocity ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)] + data := map[string]interface{}{"direction": direction} + if len(velocity) != 0 && velocity[0] > 0 { + data["velocity"] = velocity[0] + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe") + return +} + +func (we remoteWE) Pinch(scale, velocity float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)] + if scale <= 0 { + return errors.New("'scale' must be greater than zero") + } + if scale == 1 { + return errors.New("'scale' must be greater or less than 1") + } + if scale < 1 && velocity > 0 { + return errors.New("'velocity' must be less than zero when 'scale' is less than 1") + } + if scale > 1 && velocity <= 0 { + return errors.New("'velocity' must be greater than zero when 'scale' is greater than 1") + } + data := map[string]interface{}{ + "scale": scale, + "velocity": velocity, + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch") + return +} + +func (we remoteWE) PinchToZoomOutByW3CAction(scale ...float64) (err error) { + if len(scale) == 0 { + scale = []float64{1.0} + } else if scale[0] > 23 { + scale[0] = 23 + } + var size Size + if size, err = we.Size(); err != nil { + return err + } + r := scale[0] * 2 / 100.0 + offsetX, offsetY := float64(size.Width)*r, float64(size.Height)*r + + actions := NewW3CActions().SwipeFloat(0-offsetX, 0-offsetY, 0, 0, we).SwipeFloat(offsetX, offsetY, 0, 0, we) + return we.parent.PerformW3CActions(actions) +} + +func (we remoteWE) Rotate(rotation float64, velocity ...float64) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)] + if rotation > math.Pi*2 || rotation < math.Pi*-2 { + return errors.New("'rotation' must not be more than 2π or less than -2π") + } + if len(velocity) == 0 || velocity[0] == 0 { + velocity = []float64{rotation} + } + if rotation > 0 && velocity[0] < 0 || rotation < 0 && velocity[0] > 0 { + return errors.New("'rotation' and 'velocity' must have the same sign") + } + data := map[string]interface{}{ + "rotation": rotation, + "velocity": velocity[0], + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate") + return +} + +func (we remoteWE) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { + // [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)] + if len(offset) == 0 { + offset = []int{2} + } else if offset[0] <= 0 || offset[0] > 5 { + return fmt.Errorf("'offset' value is expected to be in range (0, 5]. '%d' was given instead", offset[0]) + } + data := map[string]interface{}{ + "order": order, + "offset": float64(offset[0]) * 0.1, + } + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select") + return +} + +func (we remoteWE) scroll(data interface{}) (err error) { + // [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)] + _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll") + return +} + +func (we remoteWE) ScrollElementByName(name string) error { + data := map[string]interface{}{"name": name} + return we.scroll(data) +} + +func (we remoteWE) ScrollElementByPredicate(predicate string) error { + data := map[string]interface{}{"predicateString": predicate} + return we.scroll(data) +} + +func (we remoteWE) ScrollToVisible() error { + data := map[string]interface{}{"toVisible": true} + return we.scroll(data) +} + +func (we remoteWE) ScrollDirection(direction Direction, distance ...float64) error { + if len(distance) == 0 || distance[0] <= 0 { + distance = []float64{0.5} + } + data := map[string]interface{}{ + "direction": direction, + "distance": distance[0], + } + return we.scroll(data) +} + +func (we remoteWE) FindElement(by BySelector) (element WebElement, err error) { + // [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil { + return nil, err + } + var elementID string + if elementID, err = rawResp.valueConvertToElementID(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + element = &remoteWE{parent: we.parent, id: elementID} + return +} + +func (we remoteWE) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } + var rawResp rawResponse + if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]} + } + return +} + +func (we remoteWE) FindVisibleCells() (elements []WebElement, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil { + return nil, err + } + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find a cell element in this element", err) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]} + } + return +} + +func (we remoteWE) Rect() (rect Rect, err error) { + // [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil { + return Rect{}, err + } + reply := new(struct{ Value struct{ Rect } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rect{}, err + } + rect = reply.Value.Rect + return +} + +func (we remoteWE) Location() (Point, error) { + rect, err := we.Rect() + if err != nil { + return Point{}, err + } + return rect.Point, nil +} + +func (we remoteWE) Size() (Size, error) { + rect, err := we.Rect() + if err != nil { + return Size{}, err + } + return rect.Size, nil +} + +func (we remoteWE) Text() (text string, err error) { + // [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil { + return "", err + } + if text, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we remoteWE) Type() (elemType string, err error) { + // [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil { + return "", err + } + if elemType, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we remoteWE) IsEnabled() (enabled bool, err error) { + // [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil { + return false, err + } + if enabled, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we remoteWE) IsDisplayed() (displayed bool, err error) { + // [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil { + return false, err + } + if displayed, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we remoteWE) IsSelected() (selected bool, err error) { + // [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil { + return false, err + } + if selected, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we remoteWE) IsAccessible() (accessible bool, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil { + return false, err + } + if accessible, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we remoteWE) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { + // [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil { + return false, err + } + if isAccessibilityContainer, err = rawResp.valueConvertToBool(); err != nil { + return false, err + } + return +} + +func (we remoteWE) GetAttribute(attr ElementAttribute) (value string, err error) { + // [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil { + return "", err + } + if value, err = rawResp.valueConvertToString(); err != nil { + return "", err + } + return +} + +func (we remoteWE) UID() (uid string) { + return we.id +} + +func (we remoteWE) Screenshot() (raw *bytes.Buffer, err error) { + // W3C element screenshot + // [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)] + // JSONWP element screenshot + // [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)] + var rawResp rawResponse + if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil { + return nil, err + } + if raw, err = rawResp.valueDecodeAsBase64(); err != nil { + return nil, err + } + return +} diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 68739172..5809020b 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -20,11 +20,6 @@ var client = &http.Client{ Timeout: time.Second * 10, } -type Point struct { - X float32 `json:"x"` - Y float32 `json:"y"` -} - type OCRResult struct { Text string `json:"text"` Points []Point `json:"points"` diff --git a/hrp/internal/uixt/opencv_off.go b/hrp/internal/uixt/opencv_off.go index 18045295..4fc747c1 100644 --- a/hrp/internal/uixt/opencv_off.go +++ b/hrp/internal/uixt/opencv_off.go @@ -5,11 +5,10 @@ package uixt import ( "image" - "github.com/electricbubble/gwda" "github.com/rs/zerolog/log" ) -func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) { +func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) { return extend(driver) } diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 326e9277..5fdc7197 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -8,7 +8,6 @@ import ( "io/ioutil" "os" - "github.com/electricbubble/gwda" cvHelper "github.com/electricbubble/opencv-helper" ) @@ -43,7 +42,7 @@ const ( // 获取当前设备的 Scale, // 默认匹配模式为 TmCcoeffNormed, // 默认关闭 OpenCV 匹配值计算后的输出 -func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) { +func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) { dExt, err = extend(driver) if err != nil { return nil, err diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 4d29e05b..7973b53a 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "github.com/electricbubble/gwda" "github.com/rs/zerolog/log" ) @@ -29,7 +28,7 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier toY = float64(height) * toY if len(identifier) > 0 && identifier[0] != "" { - option := gwda.WithCustomOption("log", map[string]interface{}{ + option := WithCustomOption("log", map[string]interface{}{ "enable": true, "data": identifier[0], }) diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index ac6c51fc..1d4eb168 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -2,13 +2,11 @@ package uixt import ( "fmt" - - "github.com/electricbubble/gwda" ) func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { if len(identifier) > 0 { - option := gwda.WithCustomOption("log", map[string]interface{}{ + option := WithCustomOption("log", map[string]interface{}{ "enable": true, "data": identifier, }) @@ -122,6 +120,6 @@ func (dExt *DriverExt) TapWithNumberOffset(param string, numberOfTaps int, xOffs x = x + width*xOffset y = y + height*yOffset - touchActions := gwda.NewTouchActions().Tap(gwda.NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps)) + touchActions := NewTouchActions().Tap(NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps)) return dExt.PerformTouchActions(touchActions) } diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 482c06f4..6fc499e1 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -10,7 +10,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) { pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" - // gwda.SetDebug(true) + // SetDebug(true) err = driverExt.TapWithNumber(pathSearch, 3) checkErr(t, err) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index b6f44c97..b5b74f72 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -4,12 +4,13 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) type AndroidStep struct { - uixt.UIAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal uixt.MobileAction Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } @@ -220,7 +221,7 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err screenshots := make([]string, 0) // init uiaClient driver - uiaClient, err := s.hrpRunner.initUIClient(&step.Android.UIAOptions) + uiaClient, err := s.hrpRunner.initUIClient(&step.Android.AndroidDevice) if err != nil { return } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7a6a4327..588b3728 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -17,7 +17,7 @@ var ( ) type IOSStep struct { - uixt.WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal + uixt.IOSDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal uixt.MobileAction `yaml:",inline"` Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } @@ -455,8 +455,8 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) initUIClient(options uixt.Options) (client *uixt.DriverExt, err error) { - uuid := options.UUID() +func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, err error) { + uuid := device.UUID() // avoid duplicate init if uuid == "" && len(r.uiClients) == 1 { @@ -472,10 +472,10 @@ func (r *HRPRunner) initUIClient(options uixt.Options) (client *uixt.DriverExt, } } - if wdaOptions, ok := options.(*uixt.WDAOptions); ok { - client, err = uixt.InitWDAClient(wdaOptions) - } else if uiaOptions, ok := options.(*uixt.UIAOptions); ok { - client, err = uixt.InitUIAClient(uiaOptions) + if iosDevice, ok := device.(*uixt.IOSDevice); ok { + client, err = uixt.InitWDAClient(iosDevice) + } else if androidDevice, ok := device.(*uixt.AndroidDevice); ok { + client, err = uixt.InitUIAClient(androidDevice) } if err != nil { return nil, err @@ -500,7 +500,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro screenshots := make([]string, 0) // init wdaClient driver - wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.WDAOptions) + wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.IOSDevice) if err != nil { return } From 736f1d6c7b05847329e011c9935b0a34cc188116 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 23 Sep 2022 22:58:32 +0800 Subject: [PATCH 062/169] refactor: restructure --- hrp/internal/uixt/README.md | 12 +- hrp/internal/uixt/android_webdriver.go | 2 + hrp/internal/uixt/ext.go | 58 --- hrp/internal/uixt/ios_device.go | 173 ++++----- .../uixt/{ios_webdriver.go => ios_driver.go} | 346 +++++++++--------- .../{ios_webelement.go => ios_element.go} | 148 ++++---- hrp/internal/uixt/ios_test.go | 49 +-- 7 files changed, 366 insertions(+), 422 deletions(-) rename hrp/internal/uixt/{ios_webdriver.go => ios_driver.go} (64%) rename hrp/internal/uixt/{ios_webelement.go => ios_element.go} (63%) diff --git a/hrp/internal/uixt/README.md b/hrp/internal/uixt/README.md index 111281c0..bfb7d26f 100644 --- a/hrp/internal/uixt/README.md +++ b/hrp/internal/uixt/README.md @@ -2,8 +2,8 @@ From v4.3.0,HttpRunner will support mobile UI automation testing: -- iOS: based on [appium/WebDriverAgent], with client library [electricbubble/gwda] in golang -- Android: based on UiAutomation +- iOS: based on [appium/WebDriverAgent], with forked client library [electricbubble/gwda] in golang +- Android: based on [appium-uiautomator2-server], with forked client library [electricbubble/guia2] in golang Some UI recognition algorithms are also introduced for both iOS and Android: @@ -34,12 +34,18 @@ $ make build tags=ocr ## Thanks -This uixt module is initially forked from [electricbubble/gwda-ext-opencv] and made a lot of changes. +This uixt module is initially forked from the following repos and made a lot of changes. + +- [electricbubble/gwda-ext-opencv] +- [electricbubble/gwda] +- [electricbubble/guia] [electricbubble/gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv [appium/WebDriverAgent]: https://github.com/appium/WebDriverAgent [electricbubble/gwda]: https://github.com/electricbubble/gwda +[electricbubble/guia]: https://github.com/electricbubble/guia2 [OpenCV 4]: https://opencv.org/ [hybridgroup/gocv]: https://github.com/hybridgroup/gocv [volcengine]: https://www.volcengine.com/product/text-recognition +[appium-uiautomator2-server]: https://github.com/appium/appium-uiautomator2-server diff --git a/hrp/internal/uixt/android_webdriver.go b/hrp/internal/uixt/android_webdriver.go index 64c90179..91dd4070 100644 --- a/hrp/internal/uixt/android_webdriver.go +++ b/hrp/internal/uixt/android_webdriver.go @@ -1 +1,3 @@ package uixt + +type uiaWebDriver struct{} diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index a705d5b8..5eb7d0d1 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -7,9 +7,6 @@ import ( "image" "image/jpeg" "image/png" - "mime" - "mime/multipart" - "net/http" "os" "path/filepath" "strings" @@ -148,61 +145,6 @@ func extend(driver WebDriver) (dExt *DriverExt, err error) { return dExt, nil } -func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { - if httpClient == nil { - return errors.New(`'httpClient' can't be nil`) - } - - var req *http.Request - if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil { - return err - } - - var resp *http.Response - if resp, err = httpClient.Do(req); err != nil { - return err - } - // defer func() { _ = resp.Body.Close() }() - - var boundary string - if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { - return err - } else { - boundary = strings.Trim(param["boundary"], "-") - } - - mjpegReader := multipart.NewReader(resp.Body, boundary) - - go func() { - for { - select { - case <-dExt.doneMjpegStream: - _ = resp.Body.Close() - return - default: - var part *multipart.Part - if part, err = mjpegReader.NextPart(); err != nil { - dExt.frame = nil - continue - } - - raw := new(bytes.Buffer) - if _, err = raw.ReadFrom(part); err != nil { - dExt.frame = nil - continue - } - dExt.frame = raw - } - } - }() - - return -} - -func (dExt *DriverExt) CloseMjpegStream() { - dExt.doneMjpegStream <- true -} - func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { // 优先使用 MJPEG 流进行截图,性能最优 // 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口 diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 10b37d74..eeee5e5f 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -7,10 +7,13 @@ import ( builtinJSON "encoding/json" "fmt" "io/ioutil" + "mime" + "mime/multipart" "net" "net/http" "net/url" "regexp" + "strings" "sync" "time" @@ -54,7 +57,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } // init wda device - targetDevice, err := NewDevice(deviceOptions...) + iosDevice, err := NewIOSDevice(deviceOptions...) if err != nil { return nil, err } @@ -63,7 +66,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { // aviod getting stuck when some super app is activate such as douyin or wexin log.Info().Msg("switch to iOS springboard") bundleID := "com.apple.springboard" - _, err = targetDevice.GIDevice().AppLaunch(bundleID) + _, err = iosDevice.AppLaunch(bundleID) if err != nil { return nil, errors.Wrap(err, "launch springboard failed") } @@ -71,7 +74,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { // init WDA driver capabilities := NewCapabilities() capabilities.WithDefaultAlertAction(AlertActionAccept) - driver, err := NewUSBDriver(capabilities, *targetDevice) + driver, err := iosDevice.NewUSBDriver(capabilities) if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } @@ -88,7 +91,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") - driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port) + driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", iosDevice.Port) if device.LogOn { err = driverExt.StartLogRecording("hrp_wda_log") if err != nil { @@ -103,23 +106,6 @@ type Device interface { UUID() string } -type IOSDevice struct { - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` - - d giDevice.Device -} - -func (d IOSDevice) UUID() string { - return d.UDID -} - -func (d IOSDevice) GIDevice() giDevice.Device { - return d.d -} - type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -146,7 +132,7 @@ func WithLogOn(logOn bool) IOSDeviceOption { } } -func NewDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { +func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { var usbmux giDevice.Usbmux if usbmux, err = giDevice.NewUsbmux(); err != nil { return nil, fmt.Errorf("init usbmux failed: %v", err) @@ -166,49 +152,37 @@ func NewDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } serialNumber := device.UDID - for _, d := range deviceList { + for _, dev := range deviceList { // find device by serial number if specified - if serialNumber != "" && d.Properties().SerialNumber != serialNumber { + if serialNumber != "" && dev.Properties().SerialNumber != serialNumber { continue } - device.UDID = d.Properties().SerialNumber - device.d = d + device.UDID = dev.Properties().SerialNumber + device.Device = dev return device, nil } return nil, fmt.Errorf("device %s not found", device.UDID) } -func DeviceList() (devices []IOSDevice, err error) { - var usbmux giDevice.Usbmux - if usbmux, err = giDevice.NewUsbmux(); err != nil { - return nil, fmt.Errorf("usbmuxd: %w", err) - } - - var deviceList []giDevice.Device - if deviceList, err = usbmux.Devices(); err != nil { - return nil, fmt.Errorf("device list: %w", err) - } - - devices = make([]IOSDevice, len(deviceList)) - - for i := range devices { - devices[i].UDID = deviceList[i].Properties().SerialNumber - devices[i].Port = defaultPort - devices[i].MjpegPort = defaultMjpegPort - devices[i].d = deviceList[i] - } - - return +type IOSDevice struct { + giDevice.Device + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } -// NewDriver creates new remote client, this will also start a new session. -func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (driver WebDriver, err error) { - if len(mjpegPort) == 0 { - mjpegPort = []int{defaultMjpegPort} - } - wd := new(remoteWD) +func (dev *IOSDevice) UUID() string { + return dev.UDID +} + +// NewHTTPDriver creates new remote HTTP client, this will also start a new session. +func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { + wd := new(wdaDriver) + + urlPrefix := fmt.Sprintf("http://127.0.0.1:%d", dev.Port) if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil { return nil, err } @@ -218,7 +192,11 @@ func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (d } wd.sessionId = sessionInfo.SessionId - if wd.mjpegConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), mjpegPort[0])); err != nil { + if wd.mjpegConn, err = net.Dial( + "tcp", + fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), + dev.MjpegPort), + ); err != nil { return nil, err } wd.mjpegClient = convertToHTTPClient(wd.mjpegConn) @@ -227,30 +205,20 @@ func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (d } // NewUSBDriver creates new client via USB connected device, this will also start a new session. -func NewUSBDriver(capabilities Capabilities, device ...IOSDevice) (driver WebDriver, err error) { - if len(device) == 0 { - if device, err = DeviceList(); err != nil { - return nil, err - } - if len(device) == 0 { - return nil, errors.New("no device") - } - } - dev := device[0] - - wd := &remoteWD{ +func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { + wd := &wdaDriver{ usbCli: &struct { httpCli *http.Client defaultConn, mjpegConn giDevice.InnerConn sync.Mutex }{}, } - if wd.usbCli.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { + if wd.usbCli.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { return nil, fmt.Errorf("create connection: %w", err) } wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn()) - if wd.usbCli.mjpegConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { + if wd.usbCli.mjpegConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { return nil, fmt.Errorf("create connection MJPEG: %w", err) } wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn()) @@ -277,20 +245,6 @@ func NewUSBDriver(capabilities Capabilities, device ...IOSDevice) (driver WebDri return wd, err } -func newRequest(method string, url string, rawBody []byte) (request *http.Request, err error) { - header := map[string]string{ - "Content-Type": "application/json;charset=UTF-8", - "Accept": "application/json", - } - if request, err = http.NewRequest(method, url, bytes.NewBuffer(rawBody)); err != nil { - return nil, err - } - for k, v := range header { - request.Header.Set(k, v) - } - return -} - func convertToHTTPClient(_conn net.Conn) *http.Client { return &http.Client{ Transport: &http.Transport{ @@ -360,6 +314,61 @@ func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, return reply, nil } +func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { + if httpClient == nil { + return errors.New(`'httpClient' can't be nil`) + } + + var req *http.Request + if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil { + return err + } + + var resp *http.Response + if resp, err = httpClient.Do(req); err != nil { + return err + } + // defer func() { _ = resp.Body.Close() }() + + var boundary string + if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { + return err + } else { + boundary = strings.Trim(param["boundary"], "-") + } + + mjpegReader := multipart.NewReader(resp.Body, boundary) + + go func() { + for { + select { + case <-dExt.doneMjpegStream: + _ = resp.Body.Close() + return + default: + var part *multipart.Part + if part, err = mjpegReader.NextPart(); err != nil { + dExt.frame = nil + continue + } + + raw := new(bytes.Buffer) + if _, err = raw.ReadFrom(part); err != nil { + dExt.frame = nil + continue + } + dExt.frame = raw + } + } + }() + + return +} + +func (dExt *DriverExt) CloseMjpegStream() { + dExt.doneMjpegStream <- true +} + type rawResponse []byte func (r rawResponse) checkErr() (err error) { diff --git a/hrp/internal/uixt/ios_webdriver.go b/hrp/internal/uixt/ios_driver.go similarity index 64% rename from hrp/internal/uixt/ios_webdriver.go rename to hrp/internal/uixt/ios_driver.go index fb1c5337..6464fd2d 100644 --- a/hrp/internal/uixt/ios_webdriver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -22,9 +22,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/json" ) -// var _ WebDriver = (*remoteWD)(nil) - -type remoteWD struct { +type wdaDriver struct { urlPrefix *url.URL sessionId string @@ -38,39 +36,11 @@ type remoteWD struct { mjpegConn net.Conn } -func (wd *remoteWD) _requestURL(tmpURL *url.URL, elem ...string) string { - var tmp *url.URL - if tmpURL == nil { - tmpURL = wd.urlPrefix - } - tmp, _ = url.Parse(tmpURL.String()) - tmp.Path = path.Join(append([]string{tmpURL.Path}, elem...)...) - return tmp.String() -} - -func (wd *remoteWD) executeGet(pathElem ...string) (rawResp rawResponse, err error) { - return wd.executeHTTP(http.MethodGet, wd._requestURL(nil, pathElem...), nil) -} - -func (wd *remoteWD) executePost(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { - var bsJSON []byte = nil - if data != nil { - if bsJSON, err = json.Marshal(data); err != nil { - return nil, err - } - } - return wd.executeHTTP(http.MethodPost, wd._requestURL(nil, pathElem...), bsJSON) -} - -func (wd *remoteWD) executeDelete(pathElem ...string) (rawResp rawResponse, err error) { - return wd.executeHTTP(http.MethodDelete, wd._requestURL(nil, pathElem...), nil) -} - -func (wd *remoteWD) GetMjpegHTTPClient() *http.Client { +func (wd *wdaDriver) GetMjpegHTTPClient() *http.Client { return wd.mjpegClient } -func (wd *remoteWD) Close() error { +func (wd *wdaDriver) Close() error { if wd.usbCli == nil { wd.mjpegClient.CloseIdleConnections() return wd.mjpegConn.Close() @@ -88,7 +58,7 @@ func (wd *remoteWD) Close() error { return nil } -func (wd *remoteWD) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { +func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { // [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)] data := make(map[string]interface{}) if len(capabilities) == 0 { @@ -98,7 +68,7 @@ func (wd *remoteWD) NewSession(capabilities Capabilities) (sessionInfo SessionIn } var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session"); err != nil { return SessionInfo{}, err } if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { @@ -108,10 +78,10 @@ func (wd *remoteWD) NewSession(capabilities Capabilities) (sessionInfo SessionIn return } -func (wd *remoteWD) ActiveSession() (sessionInfo SessionInfo, err error) { +func (wd *wdaDriver) ActiveSession() (sessionInfo SessionInfo, err error) { // [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId); err != nil { return SessionInfo{}, err } if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil { @@ -120,16 +90,16 @@ func (wd *remoteWD) ActiveSession() (sessionInfo SessionInfo, err error) { return } -func (wd *remoteWD) DeleteSession() (err error) { +func (wd *wdaDriver) DeleteSession() (err error) { // [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)] - _, err = wd.executeDelete("/session", wd.sessionId) + _, err = wd.httpDELETE("/session", wd.sessionId) return } -func (wd *remoteWD) Status() (deviceStatus DeviceStatus, err error) { +func (wd *wdaDriver) Status() (deviceStatus DeviceStatus, err error) { // [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/status"); err != nil { + if rawResp, err = wd.httpGET("/status"); err != nil { return DeviceStatus{}, err } reply := new(struct{ Value struct{ DeviceStatus } }) @@ -140,11 +110,11 @@ func (wd *remoteWD) Status() (deviceStatus DeviceStatus, err error) { return } -func (wd *remoteWD) DeviceInfo() (deviceInfo DeviceInfo, err error) { +func (wd *wdaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { // [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)] // [[FBRoute GET:@"/wda/device/info"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/info"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/info"); err != nil { return DeviceInfo{}, err } reply := new(struct{ Value struct{ DeviceInfo } }) @@ -155,11 +125,11 @@ func (wd *remoteWD) DeviceInfo() (deviceInfo DeviceInfo, err error) { return } -func (wd *remoteWD) Location() (location Location, err error) { +func (wd *wdaDriver) Location() (location Location, err error) { // [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)] // [[FBRoute GET:@"/wda/device/location"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/location"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/location"); err != nil { return Location{}, err } reply := new(struct{ Value struct{ Location } }) @@ -170,10 +140,10 @@ func (wd *remoteWD) Location() (location Location, err error) { return } -func (wd *remoteWD) BatteryInfo() (batteryInfo BatteryInfo, err error) { +func (wd *wdaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { // [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/batteryInfo"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/batteryInfo"); err != nil { return BatteryInfo{}, err } reply := new(struct{ Value struct{ BatteryInfo } }) @@ -184,10 +154,10 @@ func (wd *remoteWD) BatteryInfo() (batteryInfo BatteryInfo, err error) { return } -func (wd *remoteWD) WindowSize() (size Size, err error) { +func (wd *wdaDriver) WindowSize() (size Size, err error) { // [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/window/size"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/window/size"); err != nil { return Size{}, err } reply := new(struct{ Value struct{ Size } }) @@ -198,10 +168,10 @@ func (wd *remoteWD) WindowSize() (size Size, err error) { return } -func (wd *remoteWD) Screen() (screen Screen, err error) { +func (wd *wdaDriver) Screen() (screen Screen, err error) { // [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/screen"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/screen"); err != nil { return Screen{}, err } reply := new(struct{ Value struct{ Screen } }) @@ -212,7 +182,7 @@ func (wd *remoteWD) Screen() (screen Screen, err error) { return } -func (wd *remoteWD) Scale() (float64, error) { +func (wd *wdaDriver) Scale() (float64, error) { screen, err := wd.Screen() if err != nil { return 0, err @@ -220,11 +190,11 @@ func (wd *remoteWD) Scale() (float64, error) { return screen.Scale, nil } -func (wd *remoteWD) ActiveAppInfo() (info AppInfo, err error) { +func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) { // [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)] // [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil { return AppInfo{}, err } reply := new(struct{ Value struct{ AppInfo } }) @@ -235,10 +205,10 @@ func (wd *remoteWD) ActiveAppInfo() (info AppInfo, err error) { return } -func (wd *remoteWD) ActiveAppsList() (appsList []AppBaseInfo, err error) { +func (wd *wdaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { // [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/apps/list"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/apps/list"); err != nil { return nil, err } reply := new(struct{ Value []AppBaseInfo }) @@ -249,11 +219,11 @@ func (wd *remoteWD) ActiveAppsList() (appsList []AppBaseInfo, err error) { return } -func (wd *remoteWD) AppState(bundleId string) (runState AppState, err error) { +func (wd *wdaDriver) AppState(bundleId string) (runState AppState, err error) { // [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)] data := map[string]interface{}{"bundleId": bundleId} var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil { return 0, err } reply := new(struct{ Value AppState }) @@ -265,11 +235,11 @@ func (wd *remoteWD) AppState(bundleId string) (runState AppState, err error) { return } -func (wd *remoteWD) IsLocked() (locked bool, err error) { +func (wd *wdaDriver) IsLocked() (locked bool, err error) { // [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)] // [[FBRoute GET:@"/wda/locked"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/locked"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/locked"); err != nil { return false, err } if locked, err = rawResp.valueConvertToBool(); err != nil { @@ -278,31 +248,31 @@ func (wd *remoteWD) IsLocked() (locked bool, err error) { return } -func (wd *remoteWD) Unlock() (err error) { +func (wd *wdaDriver) Unlock() (err error) { // [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)] // [[FBRoute POST:@"/wda/unlock"].withoutSession - _, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/unlock") + _, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/unlock") return } -func (wd *remoteWD) Lock() (err error) { +func (wd *wdaDriver) Lock() (err error) { // [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)] // [[FBRoute POST:@"/wda/lock"].withoutSession - _, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/lock") + _, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/lock") return } -func (wd *remoteWD) Homescreen() (err error) { +func (wd *wdaDriver) Homescreen() (err error) { // [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)] - _, err = wd.executePost(nil, "/wda/homescreen") + _, err = wd.httpPOST(nil, "/wda/homescreen") return } -func (wd *remoteWD) AlertText() (text string, err error) { +func (wd *wdaDriver) AlertText() (text string, err error) { // [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)] // [[FBRoute GET:@"/alert/text"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/alert/text"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/alert/text"); err != nil { return "", err } if text, err = rawResp.valueConvertToString(); err != nil { @@ -311,10 +281,10 @@ func (wd *remoteWD) AlertText() (text string, err error) { return } -func (wd *remoteWD) AlertButtons() (btnLabels []string, err error) { +func (wd *wdaDriver) AlertButtons() (btnLabels []string, err error) { // [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/alert/buttons"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/alert/buttons"); err != nil { return nil, err } reply := new(struct{ Value []string }) @@ -325,58 +295,58 @@ func (wd *remoteWD) AlertButtons() (btnLabels []string, err error) { return } -func (wd *remoteWD) AlertAccept(label ...string) (err error) { +func (wd *wdaDriver) AlertAccept(label ...string) (err error) { // [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)] // [[FBRoute POST:@"/alert/accept"].withoutSession data := make(map[string]interface{}) if len(label) != 0 && label[0] != "" { data["name"] = label[0] } - _, err = wd.executePost(data, "/alert/accept") + _, err = wd.httpPOST(data, "/alert/accept") return } -func (wd *remoteWD) AlertDismiss(label ...string) (err error) { +func (wd *wdaDriver) AlertDismiss(label ...string) (err error) { // [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)] // [[FBRoute POST:@"/alert/dismiss"].withoutSession data := make(map[string]interface{}) if len(label) != 0 && label[0] != "" { data["name"] = label[0] } - _, err = wd.executePost(data, "/alert/dismiss") + _, err = wd.httpPOST(data, "/alert/dismiss") return } -func (wd *remoteWD) AlertSendKeys(text string) (err error) { +func (wd *wdaDriver) AlertSendKeys(text string) (err error) { // [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)] data := map[string]interface{}{"value": strings.Split(text, "")} - _, err = wd.executePost(data, "/session", wd.sessionId, "/alert/text") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/alert/text") return } -func (wd *remoteWD) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { +func (wd *wdaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] data := make(map[string]interface{}) if len(launchOpt) != 0 { data = launchOpt[0] } data["bundleId"] = bundleId - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/launch") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch") return } -func (wd *remoteWD) AppLaunchUnattached(bundleId string) (err error) { +func (wd *wdaDriver) AppLaunchUnattached(bundleId string) (err error) { // [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)] data := map[string]interface{}{"bundleId": bundleId} - _, err = wd.executePost(data, "/wda/apps/launchUnattached") + _, err = wd.httpPOST(data, "/wda/apps/launchUnattached") return } -func (wd *remoteWD) AppTerminate(bundleId string) (successful bool, err error) { +func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) { // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] data := map[string]interface{}{"bundleId": bundleId} var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil { return false, err } if successful, err = rawResp.valueConvertToBool(); err != nil { @@ -385,35 +355,35 @@ func (wd *remoteWD) AppTerminate(bundleId string) (successful bool, err error) { return } -func (wd *remoteWD) AppActivate(bundleId string) (err error) { +func (wd *wdaDriver) AppActivate(bundleId string) (err error) { // [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)] data := map[string]interface{}{"bundleId": bundleId} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/activate") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/activate") return } -func (wd *remoteWD) AppDeactivate(second float64) (err error) { +func (wd *wdaDriver) AppDeactivate(second float64) (err error) { // [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)] if second < 3 { second = 3.0 } data := map[string]interface{}{"duration": second} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/deactivateApp") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/deactivateApp") return } -func (wd *remoteWD) AppAuthReset(resource ProtectedResource) (err error) { +func (wd *wdaDriver) AppAuthReset(resource ProtectedResource) (err error) { // [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)] data := map[string]interface{}{"resource": resource} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/resetAppAuth") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/resetAppAuth") return } -func (wd *remoteWD) Tap(x, y int, options ...DataOption) error { +func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error { return wd.TapFloat(float64(x), float64(y), options...) } -func (wd *remoteWD) TapFloat(x, y float64, options ...DataOption) (err error) { +func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] data := map[string]interface{}{ "x": x, @@ -424,29 +394,29 @@ func (wd *remoteWD) TapFloat(x, y float64, options ...DataOption) (err error) { for _, option := range options { option(data) } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/tap/0") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/tap/0") return } -func (wd *remoteWD) DoubleTap(x, y int) error { +func (wd *wdaDriver) DoubleTap(x, y int) error { return wd.DoubleTapFloat(float64(x), float64(y)) } -func (wd *remoteWD) DoubleTapFloat(x, y float64) (err error) { +func (wd *wdaDriver) DoubleTapFloat(x, y float64) (err error) { // [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)] data := map[string]interface{}{ "x": x, "y": y, } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/doubleTap") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/doubleTap") return } -func (wd *remoteWD) TouchAndHold(x, y int, second ...float64) error { +func (wd *wdaDriver) TouchAndHold(x, y int, second ...float64) error { return wd.TouchAndHoldFloat(float64(x), float64(y), second...) } -func (wd *remoteWD) TouchAndHoldFloat(x, y float64, second ...float64) (err error) { +func (wd *wdaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) { // [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)] data := map[string]interface{}{ "x": x, @@ -456,15 +426,15 @@ func (wd *remoteWD) TouchAndHoldFloat(x, y float64, second ...float64) (err erro second = []float64{1.0} } data["duration"] = second[0] - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touchAndHold") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touchAndHold") return } -func (wd *remoteWD) Drag(fromX, fromY, toX, toY int, options ...DataOption) error { +func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error { return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } -func (wd *remoteWD) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) { +func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) { // [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)] data := map[string]interface{}{ "fromX": fromX, @@ -482,25 +452,25 @@ func (wd *remoteWD) DragFloat(fromX, fromY, toX, toY float64, options ...DataOpt if _, ok := data["duration"]; !ok { data["duration"] = 1.0 // default duration } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/dragfromtoforduration") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration") return } -func (wd *remoteWD) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { +func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { options = append(options, WithPressDuration(0)) return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } -func (wd *remoteWD) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { +func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { options = append(options, WithPressDuration(0)) return wd.DragFloat(fromX, fromY, toX, toY, options...) } -func (wd *remoteWD) ForceTouch(x, y int, pressure float64, second ...float64) error { +func (wd *wdaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error { return wd.ForceTouchFloat(float64(x), float64(y), pressure, second...) } -func (wd *remoteWD) ForceTouchFloat(x, y, pressure float64, second ...float64) error { +func (wd *wdaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) error { if len(second) == 0 || second[0] <= 0 { second = []float64{1.0} } @@ -512,36 +482,36 @@ func (wd *remoteWD) ForceTouchFloat(x, y, pressure float64, second ...float64) e return wd.PerformAppiumTouchActions(actions) } -func (wd *remoteWD) PerformW3CActions(actions *W3CActions) (err error) { +func (wd *wdaDriver) PerformW3CActions(actions *W3CActions) (err error) { // [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)] data := map[string]interface{}{"actions": actions} - _, err = wd.executePost(data, "/session", wd.sessionId, "/actions") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/actions") return } -func (wd *remoteWD) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { +func (wd *wdaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { // [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)] // [[FBRoute POST:@"/wda/touch/multi/perform"] data := map[string]interface{}{"actions": touchActs} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch/multi/perform") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch/multi/perform") return } -func (wd *remoteWD) SetPasteboard(contentType PasteboardType, content string) (err error) { +func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { // [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)] data := map[string]interface{}{ "contentType": contentType, "content": base64.StdEncoding.EncodeToString([]byte(content)), } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/setPasteboard") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/setPasteboard") return } -func (wd *remoteWD) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { +func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] data := map[string]interface{}{"contentType": contentType} var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil { return nil, err } if raw, err = rawResp.valueDecodeAsBase64(); err != nil { @@ -550,7 +520,7 @@ func (wd *remoteWD) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer return } -func (wd *remoteWD) SendKeys(text string, options ...DataOption) (err error) { +func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) { // [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] data := map[string]interface{}{"value": strings.Split(text, "")} @@ -563,28 +533,28 @@ func (wd *remoteWD) SendKeys(text string, options ...DataOption) (err error) { if _, ok := data["frequency"]; !ok { data["frequency"] = 60 // default frequency } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keys") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keys") return } -func (wd *remoteWD) KeyboardDismiss(keyNames ...string) (err error) { +func (wd *wdaDriver) KeyboardDismiss(keyNames ...string) (err error) { // [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)] if len(keyNames) == 0 { keyNames = []string{"return"} } data := map[string]interface{}{"keyNames": keyNames} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keyboard/dismiss") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keyboard/dismiss") return } -func (wd *remoteWD) PressButton(devBtn DeviceButton) (err error) { +func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) { // [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)] data := map[string]interface{}{"name": devBtn} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/pressButton") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/pressButton") return } -func (wd *remoteWD) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { +func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { // [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)] if len(duration) == 0 || duration[0] <= 0 { duration = []float64{0.005} @@ -594,11 +564,11 @@ func (wd *remoteWD) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duratio "usage": usageID, "duration": duration[0], } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/performIoHidEvent") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/performIoHidEvent") return } -func (wd *remoteWD) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { +func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { // [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)] if len(second) == 0 { second = []int{60} @@ -608,28 +578,28 @@ func (wd *remoteWD) ExpectNotification(notifyName string, notifyType Notificatio "type": notifyType, "timeout": second[0], } - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/expectNotification") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/expectNotification") return } -func (wd *remoteWD) SiriActivate(text string) (err error) { +func (wd *wdaDriver) SiriActivate(text string) (err error) { // [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)] data := map[string]interface{}{"text": text} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/siri/activate") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/siri/activate") return } -func (wd *remoteWD) SiriOpenUrl(url string) (err error) { +func (wd *wdaDriver) SiriOpenUrl(url string) (err error) { // [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)] data := map[string]interface{}{"url": url} - _, err = wd.executePost(data, "/session", wd.sessionId, "/url") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/url") return } -func (wd *remoteWD) Orientation() (orientation Orientation, err error) { +func (wd *wdaDriver) Orientation() (orientation Orientation, err error) { // [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/orientation"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/orientation"); err != nil { return "", err } reply := new(struct{ Value Orientation }) @@ -640,17 +610,17 @@ func (wd *remoteWD) Orientation() (orientation Orientation, err error) { return } -func (wd *remoteWD) SetOrientation(orientation Orientation) (err error) { +func (wd *wdaDriver) SetOrientation(orientation Orientation) (err error) { // [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)] data := map[string]interface{}{"orientation": orientation} - _, err = wd.executePost(data, "/session", wd.sessionId, "/orientation") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/orientation") return } -func (wd *remoteWD) Rotation() (rotation Rotation, err error) { +func (wd *wdaDriver) Rotation() (rotation Rotation, err error) { // [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/rotation"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/rotation"); err != nil { return Rotation{}, err } reply := new(struct{ Value Rotation }) @@ -661,34 +631,34 @@ func (wd *remoteWD) Rotation() (rotation Rotation, err error) { return } -func (wd *remoteWD) SetRotation(rotation Rotation) (err error) { +func (wd *wdaDriver) SetRotation(rotation Rotation) (err error) { // [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)] - _, err = wd.executePost(rotation, "/session", wd.sessionId, "/rotation") + _, err = wd.httpPOST(rotation, "/session", wd.sessionId, "/rotation") return } -func (wd *remoteWD) MatchTouchID(isMatch bool) (err error) { +func (wd *wdaDriver) MatchTouchID(isMatch bool) (err error) { // [FBRoute POST:@"/wda/touch_id"] data := map[string]interface{}{"match": isMatch} - _, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch_id") + _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch_id") return } -func (wd *remoteWD) ActiveElement() (element WebElement, err error) { +func (wd *wdaDriver) ActiveElement() (element WebElement, err error) { // [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/element/active"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/element/active"); err != nil { return nil, err } var elementID string if elementID, err = rawResp.valueConvertToElementID(); err != nil { return nil, err } - element = &remoteWE{parent: wd, id: elementID} + element = &wdaElement{parent: wd, id: elementID} return } -func (wd *remoteWD) FindElement(by BySelector) (element WebElement, err error) { +func (wd *wdaDriver) FindElement(by BySelector) (element WebElement, err error) { // [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)] using, value := by.getUsingAndValue() data := map[string]interface{}{ @@ -696,7 +666,7 @@ func (wd *remoteWD) FindElement(by BySelector) (element WebElement, err error) { "value": value, } var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/element"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/element"); err != nil { return nil, err } var elementID string @@ -706,11 +676,11 @@ func (wd *remoteWD) FindElement(by BySelector) (element WebElement, err error) { } return nil, err } - element = &remoteWE{parent: wd, id: elementID} + element = &wdaElement{parent: wd, id: elementID} return } -func (wd *remoteWD) FindElements(by BySelector) (elements []WebElement, err error) { +func (wd *wdaDriver) FindElements(by BySelector) (elements []WebElement, err error) { // [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)] using, value := by.getUsingAndValue() data := map[string]interface{}{ @@ -718,7 +688,7 @@ func (wd *remoteWD) FindElements(by BySelector) (elements []WebElement, err erro "value": value, } var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/elements"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/elements"); err != nil { return nil, err } var elementIDs []string @@ -730,16 +700,16 @@ func (wd *remoteWD) FindElements(by BySelector) (elements []WebElement, err erro } elements = make([]WebElement, len(elementIDs)) for i := range elementIDs { - elements[i] = &remoteWE{parent: wd, id: elementIDs[i]} + elements[i] = &wdaElement{parent: wd, id: elementIDs[i]} } return } -func (wd *remoteWD) Screenshot() (raw *bytes.Buffer, err error) { +func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) { // [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)] // [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/screenshot"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/screenshot"); err != nil { return nil, err } @@ -749,10 +719,10 @@ func (wd *remoteWD) Screenshot() (raw *bytes.Buffer, err error) { return } -func (wd *remoteWD) Source(srcOpt ...SourceOption) (source string, err error) { +func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) { // [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)] // [[FBRoute GET:@"/source"].withoutSession - tmp, _ := url.Parse(wd._requestURL(nil, "/session", wd.sessionId)) + tmp, _ := url.Parse(wd.concatURL(nil, "/session", wd.sessionId)) toJsonRaw := false if len(srcOpt) != 0 { q := tmp.Query() @@ -767,7 +737,7 @@ func (wd *remoteWD) Source(srcOpt ...SourceOption) (source string, err error) { } var rawResp rawResponse - if rawResp, err = wd.executeHTTP(http.MethodGet, wd._requestURL(tmp, "/source"), nil); err != nil { + if rawResp, err = wd.httpRequest(http.MethodGet, wd.concatURL(tmp, "/source"), nil); err != nil { return "", nil } if toJsonRaw { @@ -783,11 +753,11 @@ func (wd *remoteWD) Source(srcOpt ...SourceOption) (source string, err error) { return } -func (wd *remoteWD) AccessibleSource() (source string, err error) { +func (wd *wdaDriver) AccessibleSource() (source string, err error) { // [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)] // [[FBRoute GET:@"/wda/accessibleSource"].withoutSession var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/accessibleSource"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/accessibleSource"); err != nil { return "", err } var jr builtinJSON.RawMessage @@ -798,16 +768,16 @@ func (wd *remoteWD) AccessibleSource() (source string, err error) { return } -func (wd *remoteWD) HealthCheck() (err error) { +func (wd *wdaDriver) HealthCheck() (err error) { // [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)] - _, err = wd.executeGet("/wda/healthcheck") + _, err = wd.httpGET("/wda/healthcheck") return } -func (wd *remoteWD) GetAppiumSettings() (settings map[string]interface{}, err error) { +func (wd *wdaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { // [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)] var rawResp rawResponse - if rawResp, err = wd.executeGet("/session", wd.sessionId, "/appium/settings"); err != nil { + if rawResp, err = wd.httpGET("/session", wd.sessionId, "/appium/settings"); err != nil { return nil, err } reply := new(struct{ Value map[string]interface{} }) @@ -818,11 +788,11 @@ func (wd *remoteWD) GetAppiumSettings() (settings map[string]interface{}, err er return } -func (wd *remoteWD) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { +func (wd *wdaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { // [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)] data := map[string]interface{}{"settings": settings} var rawResp rawResponse - if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/appium/settings"); err != nil { + if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/appium/settings"); err != nil { return nil, err } reply := new(struct{ Value map[string]interface{} }) @@ -833,9 +803,9 @@ func (wd *remoteWD) SetAppiumSettings(settings map[string]interface{}) (ret map[ return } -func (wd *remoteWD) IsHealthy() (healthy bool, err error) { +func (wd *wdaDriver) IsHealthy() (healthy bool, err error) { var rawResp rawResponse - if rawResp, err = wd.executeGet("/health"); err != nil { + if rawResp, err = wd.httpGET("/health"); err != nil { return false, err } if string(rawResp) != "I-AM-ALIVE" { @@ -844,12 +814,12 @@ func (wd *remoteWD) IsHealthy() (healthy bool, err error) { return true, nil } -func (wd *remoteWD) WdaShutdown() (err error) { - _, err = wd.executeGet("/wda/shutdown") +func (wd *wdaDriver) WdaShutdown() (err error) { + _, err = wd.httpGET("/wda/shutdown") return } -func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { +func (wd *wdaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { startTime := time.Now() for { done, err := condition(wd) @@ -867,23 +837,51 @@ func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, int } } -func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { +func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } -func (wd *remoteWD) Wait(condition Condition) error { +func (wd *wdaDriver) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } -// HTTPClient The default client to use to communicate with the WebDriver server. -var HTTPClient = http.DefaultClient - -func (wd *remoteWD) executeHTTP(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { - log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") - var req *http.Request - if req, err = newRequest(method, rawURL, rawBody); err != nil { - return +func (wd *wdaDriver) concatURL(u *url.URL, elem ...string) string { + var tmp *url.URL + if u == nil { + u = wd.urlPrefix } + tmp, _ = url.Parse(u.String()) + tmp.Path = path.Join(append([]string{u.Path}, elem...)...) + return tmp.String() +} + +func (wd *wdaDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *wdaDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return wd.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) +} + +func (wd *wdaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { + log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") + + var req *http.Request + if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + req.Header.Set("Accept", "application/json") var httpCli *http.Client if wd.usbCli != nil { @@ -891,7 +889,7 @@ func (wd *remoteWD) executeHTTP(method string, rawURL string, rawBody []byte) (r defer wd.usbCli.Unlock() httpCli = wd.usbCli.httpCli } else { - httpCli = HTTPClient + httpCli = http.DefaultClient } httpCli.Timeout = 0 diff --git a/hrp/internal/uixt/ios_webelement.go b/hrp/internal/uixt/ios_element.go similarity index 63% rename from hrp/internal/uixt/ios_webelement.go rename to hrp/internal/uixt/ios_element.go index 805a67c2..16479efe 100644 --- a/hrp/internal/uixt/ios_webelement.go +++ b/hrp/internal/uixt/ios_element.go @@ -14,72 +14,72 @@ import ( // All elements returned by search endpoints have assigned element_id. // Given element_id you can query properties like: // enabled, rect, size, location, text, displayed, accessible, name -type remoteWE struct { - parent *remoteWD +type wdaElement struct { + parent *wdaDriver id string // element_id } -func (we remoteWE) Click() (err error) { +func (we wdaElement) Click() (err error) { // [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)] - _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/click") + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/click") return } -func (we remoteWE) SendKeys(text string, frequency ...int) (err error) { +func (we wdaElement) SendKeys(text string, frequency ...int) (err error) { // [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)] data := map[string]interface{}{"value": strings.Split(text, "")} if len(frequency) == 0 || frequency[0] <= 0 { frequency = []int{60} } data["frequency"] = frequency[0] - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/value") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/value") return } -func (we remoteWE) Clear() (err error) { +func (we wdaElement) Clear() (err error) { // [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)] - _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear") + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear") return } -func (we remoteWE) Tap(x, y int) error { +func (we wdaElement) Tap(x, y int) error { return we.TapFloat(float64(x), float64(y)) } -func (we remoteWE) TapFloat(x, y float64) (err error) { +func (we wdaElement) TapFloat(x, y float64) (err error) { // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] data := map[string]interface{}{ "x": x, "y": y, } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/tap/", we.id) + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/tap/", we.id) return } -func (we remoteWE) DoubleTap() (err error) { +func (we wdaElement) DoubleTap() (err error) { // [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)] - _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap") + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap") return } -func (we remoteWE) TouchAndHold(second ...float64) (err error) { +func (we wdaElement) TouchAndHold(second ...float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)] data := make(map[string]interface{}) if len(second) == 0 || second[0] <= 0 { second = []float64{1.0} } data["duration"] = second[0] - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold") return } -func (we remoteWE) TwoFingerTap() (err error) { +func (we wdaElement) TwoFingerTap() (err error) { // [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)] - _, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap") + _, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap") return } -func (we remoteWE) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { +func (we wdaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)] if numberOfTouches <= 0 { return errors.New("'numberOfTouches' must be greater than zero") @@ -97,15 +97,15 @@ func (we remoteWE) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err e "numberOfTaps": numberOfTaps, "numberOfTouches": numberOfTouches, } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps") return } -func (we remoteWE) ForceTouch(pressure float64, second ...float64) (err error) { +func (we wdaElement) ForceTouch(pressure float64, second ...float64) (err error) { return we.ForceTouchFloat(-1, -1, pressure, second...) } -func (we remoteWE) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { +func (we wdaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)] data := make(map[string]interface{}) if x != -1 && y != -1 { @@ -117,15 +117,15 @@ func (we remoteWE) ForceTouchFloat(x, y, pressure float64, second ...float64) (e } data["pressure"] = pressure data["duration"] = second[0] - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch") return } -func (we remoteWE) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error { +func (we wdaElement) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error { return we.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), pressForDuration...) } -func (we remoteWE) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) { +func (we wdaElement) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)] data := map[string]interface{}{ "fromX": fromX, @@ -137,29 +137,29 @@ func (we remoteWE) DragFloat(fromX, fromY, toX, toY float64, pressForDuration .. pressForDuration = []float64{1.0} } data["duration"] = pressForDuration[0] - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration") return } -func (we remoteWE) Swipe(fromX, fromY, toX, toY int) error { +func (we wdaElement) Swipe(fromX, fromY, toX, toY int) error { return we.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY)) } -func (we remoteWE) SwipeFloat(fromX, fromY, toX, toY float64) error { +func (we wdaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { return we.DragFloat(fromX, fromY, toX, toY, 0) } -func (we remoteWE) SwipeDirection(direction Direction, velocity ...float64) (err error) { +func (we wdaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)] data := map[string]interface{}{"direction": direction} if len(velocity) != 0 && velocity[0] > 0 { data["velocity"] = velocity[0] } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe") return } -func (we remoteWE) Pinch(scale, velocity float64) (err error) { +func (we wdaElement) Pinch(scale, velocity float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)] if scale <= 0 { return errors.New("'scale' must be greater than zero") @@ -177,11 +177,11 @@ func (we remoteWE) Pinch(scale, velocity float64) (err error) { "scale": scale, "velocity": velocity, } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch") return } -func (we remoteWE) PinchToZoomOutByW3CAction(scale ...float64) (err error) { +func (we wdaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) { if len(scale) == 0 { scale = []float64{1.0} } else if scale[0] > 23 { @@ -198,7 +198,7 @@ func (we remoteWE) PinchToZoomOutByW3CAction(scale ...float64) (err error) { return we.parent.PerformW3CActions(actions) } -func (we remoteWE) Rotate(rotation float64, velocity ...float64) (err error) { +func (we wdaElement) Rotate(rotation float64, velocity ...float64) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)] if rotation > math.Pi*2 || rotation < math.Pi*-2 { return errors.New("'rotation' must not be more than 2π or less than -2π") @@ -213,11 +213,11 @@ func (we remoteWE) Rotate(rotation float64, velocity ...float64) (err error) { "rotation": rotation, "velocity": velocity[0], } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate") return } -func (we remoteWE) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { +func (we wdaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { // [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)] if len(offset) == 0 { offset = []int{2} @@ -228,32 +228,32 @@ func (we remoteWE) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err "order": order, "offset": float64(offset[0]) * 0.1, } - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select") return } -func (we remoteWE) scroll(data interface{}) (err error) { +func (we wdaElement) scroll(data interface{}) (err error) { // [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)] - _, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll") + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll") return } -func (we remoteWE) ScrollElementByName(name string) error { +func (we wdaElement) ScrollElementByName(name string) error { data := map[string]interface{}{"name": name} return we.scroll(data) } -func (we remoteWE) ScrollElementByPredicate(predicate string) error { +func (we wdaElement) ScrollElementByPredicate(predicate string) error { data := map[string]interface{}{"predicateString": predicate} return we.scroll(data) } -func (we remoteWE) ScrollToVisible() error { +func (we wdaElement) ScrollToVisible() error { data := map[string]interface{}{"toVisible": true} return we.scroll(data) } -func (we remoteWE) ScrollDirection(direction Direction, distance ...float64) error { +func (we wdaElement) ScrollDirection(direction Direction, distance ...float64) error { if len(distance) == 0 || distance[0] <= 0 { distance = []float64{0.5} } @@ -264,7 +264,7 @@ func (we remoteWE) ScrollDirection(direction Direction, distance ...float64) err return we.scroll(data) } -func (we remoteWE) FindElement(by BySelector) (element WebElement, err error) { +func (we wdaElement) FindElement(by BySelector) (element WebElement, err error) { // [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)] using, value := by.getUsingAndValue() data := map[string]interface{}{ @@ -272,7 +272,7 @@ func (we remoteWE) FindElement(by BySelector) (element WebElement, err error) { "value": value, } var rawResp rawResponse - if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil { + if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil { return nil, err } var elementID string @@ -282,11 +282,11 @@ func (we remoteWE) FindElement(by BySelector) (element WebElement, err error) { } return nil, err } - element = &remoteWE{parent: we.parent, id: elementID} + element = &wdaElement{parent: we.parent, id: elementID} return } -func (we remoteWE) FindElements(by BySelector) (elements []WebElement, err error) { +func (we wdaElement) FindElements(by BySelector) (elements []WebElement, err error) { // [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)] using, value := by.getUsingAndValue() data := map[string]interface{}{ @@ -294,7 +294,7 @@ func (we remoteWE) FindElements(by BySelector) (elements []WebElement, err error "value": value, } var rawResp rawResponse - if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil { + if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil { return nil, err } var elementIDs []string @@ -306,15 +306,15 @@ func (we remoteWE) FindElements(by BySelector) (elements []WebElement, err error } elements = make([]WebElement, len(elementIDs)) for i := range elementIDs { - elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]} + elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]} } return } -func (we remoteWE) FindVisibleCells() (elements []WebElement, err error) { +func (we wdaElement) FindVisibleCells() (elements []WebElement, err error) { // [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil { return nil, err } var elementIDs []string @@ -326,15 +326,15 @@ func (we remoteWE) FindVisibleCells() (elements []WebElement, err error) { } elements = make([]WebElement, len(elementIDs)) for i := range elementIDs { - elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]} + elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]} } return } -func (we remoteWE) Rect() (rect Rect, err error) { +func (we wdaElement) Rect() (rect Rect, err error) { // [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil { return Rect{}, err } reply := new(struct{ Value struct{ Rect } }) @@ -345,7 +345,7 @@ func (we remoteWE) Rect() (rect Rect, err error) { return } -func (we remoteWE) Location() (Point, error) { +func (we wdaElement) Location() (Point, error) { rect, err := we.Rect() if err != nil { return Point{}, err @@ -353,7 +353,7 @@ func (we remoteWE) Location() (Point, error) { return rect.Point, nil } -func (we remoteWE) Size() (Size, error) { +func (we wdaElement) Size() (Size, error) { rect, err := we.Rect() if err != nil { return Size{}, err @@ -361,10 +361,10 @@ func (we remoteWE) Size() (Size, error) { return rect.Size, nil } -func (we remoteWE) Text() (text string, err error) { +func (we wdaElement) Text() (text string, err error) { // [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil { return "", err } if text, err = rawResp.valueConvertToString(); err != nil { @@ -373,10 +373,10 @@ func (we remoteWE) Text() (text string, err error) { return } -func (we remoteWE) Type() (elemType string, err error) { +func (we wdaElement) Type() (elemType string, err error) { // [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil { return "", err } if elemType, err = rawResp.valueConvertToString(); err != nil { @@ -385,10 +385,10 @@ func (we remoteWE) Type() (elemType string, err error) { return } -func (we remoteWE) IsEnabled() (enabled bool, err error) { +func (we wdaElement) IsEnabled() (enabled bool, err error) { // [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil { return false, err } if enabled, err = rawResp.valueConvertToBool(); err != nil { @@ -397,10 +397,10 @@ func (we remoteWE) IsEnabled() (enabled bool, err error) { return } -func (we remoteWE) IsDisplayed() (displayed bool, err error) { +func (we wdaElement) IsDisplayed() (displayed bool, err error) { // [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil { return false, err } if displayed, err = rawResp.valueConvertToBool(); err != nil { @@ -409,10 +409,10 @@ func (we remoteWE) IsDisplayed() (displayed bool, err error) { return } -func (we remoteWE) IsSelected() (selected bool, err error) { +func (we wdaElement) IsSelected() (selected bool, err error) { // [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil { return false, err } if selected, err = rawResp.valueConvertToBool(); err != nil { @@ -421,10 +421,10 @@ func (we remoteWE) IsSelected() (selected bool, err error) { return } -func (we remoteWE) IsAccessible() (accessible bool, err error) { +func (we wdaElement) IsAccessible() (accessible bool, err error) { // [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil { return false, err } if accessible, err = rawResp.valueConvertToBool(); err != nil { @@ -433,10 +433,10 @@ func (we remoteWE) IsAccessible() (accessible bool, err error) { return } -func (we remoteWE) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { +func (we wdaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { // [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil { return false, err } if isAccessibilityContainer, err = rawResp.valueConvertToBool(); err != nil { @@ -445,10 +445,10 @@ func (we remoteWE) IsAccessibilityContainer() (isAccessibilityContainer bool, er return } -func (we remoteWE) GetAttribute(attr ElementAttribute) (value string, err error) { +func (we wdaElement) GetAttribute(attr ElementAttribute) (value string, err error) { // [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil { return "", err } if value, err = rawResp.valueConvertToString(); err != nil { @@ -457,17 +457,17 @@ func (we remoteWE) GetAttribute(attr ElementAttribute) (value string, err error) return } -func (we remoteWE) UID() (uid string) { +func (we wdaElement) UID() (uid string) { return we.id } -func (we remoteWE) Screenshot() (raw *bytes.Buffer, err error) { +func (we wdaElement) Screenshot() (raw *bytes.Buffer, err error) { // W3C element screenshot // [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)] // JSONWP element screenshot // [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)] var rawResp rawResponse - if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil { + if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil { return nil, err } if raw, err = rawResp.valueDecodeAsBase64(); err != nil { diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go index a93b0bec..9f6f8236 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -9,66 +9,53 @@ import ( ) var ( - urlPrefix = "http://localhost:8100" - bundleId = "com.apple.Preferences" - driver WebDriver + bundleId = "com.apple.Preferences" + driver WebDriver ) func setup(t *testing.T) { - var err error - driver, err = NewUSBDriver(nil) + device, err := NewIOSDevice() + if err != nil { + t.Fatal(err) + } + + driver, err = device.NewUSBDriver(nil) if err != nil { t.Fatal(err) } } func TestViaUSB(t *testing.T) { - devices, err := DeviceList() - if err != nil { - t.Fatal(err) - } - - drivers := make([]WebDriver, 0, len(devices)) - - for _, dev := range devices { - d, err := NewUSBDriver(nil, dev) - if err != nil { - t.Errorf("%s: %s", dev.UUID(), err) - continue - } - drivers = append(drivers, d) - } - - for _, d := range drivers { - t.Log(d.Status()) - } + setup(t) + t.Log(driver.Status()) } -func TestNewDevice(t *testing.T) { - device, _ := NewDevice() +func TestNewIOSDevice(t *testing.T) { + device, _ := NewIOSDevice() if device != nil { t.Log(device) } - device, _ = NewDevice(WithUDID("xxxx")) + device, _ = NewIOSDevice(WithUDID("xxxx")) if device != nil { t.Log(device) } - device, _ = NewDevice(WithPort(8700), WithMjpegPort(8800)) + device, _ = NewIOSDevice(WithPort(8700), WithMjpegPort(8800)) if device != nil { t.Log(device) } - device, _ = NewDevice(WithUDID("xxxx"), WithPort(8700), WithMjpegPort(8800)) + device, _ = NewIOSDevice(WithUDID("xxxx"), WithPort(8700), WithMjpegPort(8800)) if device != nil { t.Log(device) } } -func TestNewDriver(t *testing.T) { +func TestNewWDAHTTPDriver(t *testing.T) { + device, _ := NewIOSDevice() var err error - driver, err = NewDriver(nil, urlPrefix) + _, err = device.NewHTTPDriver(nil) if err != nil { t.Fatal(err) } From 66b5d5cc490ffe07993b8d431b88b2aee552907c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 25 Sep 2022 00:47:12 +0800 Subject: [PATCH 063/169] refactor: move guia2 into uixt --- go.mod | 1 + go.sum | 2 + hrp/internal/uixt/README.md | 4 +- hrp/internal/uixt/android_action.go | 157 +++ hrp/internal/uixt/android_device.go | 544 +++++++++- hrp/internal/uixt/android_driver.go | 1203 ++++++++++++++++++++ hrp/internal/uixt/android_elment.go | 238 ++++ hrp/internal/uixt/android_key.go | 879 +++++++++++++++ hrp/internal/uixt/android_test.go | 1384 ++++++++++++++++++++++++ hrp/internal/uixt/android_webdriver.go | 3 - hrp/internal/uixt/android_webelment.go | 1 - hrp/internal/uixt/client.go | 104 ++ hrp/internal/uixt/gesture.go | 1 + hrp/internal/uixt/interface.go | 12 + hrp/internal/uixt/ios_device.go | 50 +- hrp/internal/uixt/ios_driver.go | 125 +-- hrp/internal/uixt/ios_test.go | 2 - hrp/internal/uixt/tap_test.go | 2 - 18 files changed, 4557 insertions(+), 155 deletions(-) create mode 100644 hrp/internal/uixt/android_driver.go create mode 100644 hrp/internal/uixt/android_elment.go create mode 100644 hrp/internal/uixt/android_key.go create mode 100644 hrp/internal/uixt/android_test.go delete mode 100644 hrp/internal/uixt/android_webdriver.go delete mode 100644 hrp/internal/uixt/android_webelment.go create mode 100644 hrp/internal/uixt/client.go diff --git a/go.mod b/go.mod index 57d30aeb..9148a7aa 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 + github.com/electricbubble/gadb v0.0.7 github.com/electricbubble/gidevice v0.6.2 github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 diff --git a/go.sum b/go.sum index 6b9beca0..1f3a0ba3 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= +github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= +github.com/electricbubble/gadb v0.0.7/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= diff --git a/hrp/internal/uixt/README.md b/hrp/internal/uixt/README.md index bfb7d26f..6fc5449a 100644 --- a/hrp/internal/uixt/README.md +++ b/hrp/internal/uixt/README.md @@ -38,13 +38,13 @@ This uixt module is initially forked from the following repos and made a lot of - [electricbubble/gwda-ext-opencv] - [electricbubble/gwda] -- [electricbubble/guia] +- [electricbubble/guia2] [electricbubble/gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv [appium/WebDriverAgent]: https://github.com/appium/WebDriverAgent [electricbubble/gwda]: https://github.com/electricbubble/gwda -[electricbubble/guia]: https://github.com/electricbubble/guia2 +[electricbubble/guia2]: https://github.com/electricbubble/guia2 [OpenCV 4]: https://opencv.org/ [hybridgroup/gocv]: https://github.com/hybridgroup/gocv [volcengine]: https://www.volcengine.com/product/text-recognition diff --git a/hrp/internal/uixt/android_action.go b/hrp/internal/uixt/android_action.go index 64c90179..254972d3 100644 --- a/hrp/internal/uixt/android_action.go +++ b/hrp/internal/uixt/android_action.go @@ -1 +1,158 @@ package uixt + +import "strings" + +type touchGesture struct { + Touch PointF `json:"touch"` + Time float64 `json:"time"` +} + +type TouchAction []touchGesture + +func NewTouchAction(cap ...int) *TouchAction { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(TouchAction, 0, cap[0]) + return &tmp +} + +func (ta *TouchAction) Add(x, y int, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(x), float64(y), startTime...) +} + +func (ta *TouchAction) AddFloat(x, y float64, startTime ...float64) *TouchAction { + if len(startTime) == 0 { + var tmp float64 = 0 + if len(*ta) != 0 { + g := (*ta)[len(*ta)-1] + tmp = g.Time + 0.05 + } + startTime = []float64{tmp} + } + *ta = append(*ta, touchGesture{Touch: PointF{x, y}, Time: startTime[0]}) + return ta +} + +func (ta *TouchAction) AddPoint(point Point, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(point.X), float64(point.Y), startTime...) +} + +func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchAction { + return ta.AddFloat(point.X, point.Y, startTime...) +} + +func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { + // Must provide coordinates for at least 2 pointers + actions := make([]*TouchAction, 0) + actions = append(actions, gesture1, gesture2) + if len(tas) != 0 { + actions = append(actions, tas...) + } + data := map[string]interface{}{ + "actions": actions, + } + // register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/touch/multi/perform") + return +} + +type w3cGesture map[string]interface{} + +func _newW3CGesture() w3cGesture { + return make(w3cGesture) +} + +func (g w3cGesture) _set(key string, value interface{}) w3cGesture { + g[key] = value + return g +} + +func (g w3cGesture) pause(duration float64) w3cGesture { + return g._set("type", "pause"). + _set("duration", duration) +} + +func (g w3cGesture) keyDown(value string) w3cGesture { + return g._set("type", "keyDown"). + _set("value", value) +} + +func (g w3cGesture) keyUp(value string) w3cGesture { + return g._set("type", "keyUp"). + _set("value", value) +} + +func (g w3cGesture) pointerDown(button int) w3cGesture { + return g._set("type", "pointerDown")._set("button", button) +} + +func (g w3cGesture) pointerUp(button int) w3cGesture { + return g._set("type", "pointerUp")._set("button", button) +} + +func (g w3cGesture) pointerMove(x, y float64, origin string, duration float64, pressureAndSize ...float64) w3cGesture { + switch len(pressureAndSize) { + case 1: + g._set("pressure", pressureAndSize[0]) + case 2: + g._set("pressure", pressureAndSize[0]) + g._set("size", pressureAndSize[1]) + } + return g._set("type", "pointerMove"). + _set("duration", duration). + _set("origin", origin). + _set("x", x). + _set("y", y) +} + +func (g w3cGesture) size(size ...float64) w3cGesture { + if len(size) == 0 { + size = []float64{1.0} + } + return g._set("size", size[0]) +} + +func (g w3cGesture) pressure(pressure ...float64) w3cGesture { + if len(pressure) == 0 { + pressure = []float64{1.0} + } + return g._set("pressure", pressure[0]) +} + +type W3CGestures []w3cGesture + +func NewW3CGestures(cap ...int) *W3CGestures { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(W3CGestures, 0, cap[0]) + return &tmp +} + +func (g *W3CGestures) Pause(duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pause(duration[0]*1000)) + return g +} + +func (g *W3CGestures) KeyDown(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyDown(value)) + return g +} + +func (g *W3CGestures) KeyUp(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyUp(value)) + return g +} + +func (g *W3CGestures) SendKeys(text string) *W3CGestures { + ss := strings.Split(text, "") + for i := range ss { + g.KeyDown(ss[i]) + g.KeyUp(ss[i]) + } + return g +} diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index b7a3a332..ae394be4 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -1,7 +1,117 @@ package uixt +import ( + "bytes" + "fmt" + "net" + "reflect" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" +) + +var ( + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerPort = 6790 + DeviceTempPath = "/data/local/tmp" +) + +const forwardToPrefix = "forward-to-" + +func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { + var deviceOptions []AndroidDeviceOption + if device.SerialNumber != "" { + deviceOptions = append(deviceOptions, WithSerialNumber(device.SerialNumber)) + } + if device.IP != "" { + deviceOptions = append(deviceOptions, WithAdbIP(device.IP)) + } + if device.Port != 0 { + deviceOptions = append(deviceOptions, WithAdbPort(device.Port)) + } + + // init uia device + androidDevice, err := NewAndroidDevice(deviceOptions...) + if err != nil { + return nil, err + } + + driver, err := androidDevice.NewUSBDriver(nil) + if err != nil { + return nil, errors.Wrap(err, "failed to init UIA driver") + } + fmt.Println(driver) + + var driverExt *DriverExt + // TODO + // driverExt, err = Extend(driver) + // if err != nil { + // return nil, errors.Wrap(err, "failed to extend UIA Driver") + // } + + return driverExt, nil +} + +type AndroidDeviceOption func(*AndroidDevice) + +func WithSerialNumber(serial string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.SerialNumber = serial + } +} + +func WithAdbIP(ip string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.IP = ip + } +} + +func WithAdbPort(port int) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.Port = port + } +} + +func WithAdbLogOn(logOn bool) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.LogOn = logOn + } +} + +func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) { + deviceList, err := DeviceList() + if err != nil { + return nil, fmt.Errorf("get attached devices failed: %v", err) + } + + device = &AndroidDevice{ + Port: UIA2ServerPort, + IP: AdbServerHost, + } + for _, option := range options { + option(device) + } + + serialNumber := device.SerialNumber + for _, dev := range deviceList { + // find device by serial number if specified + if serialNumber != "" && dev.Serial() != serialNumber { + continue + } + + device.SerialNumber = dev.Serial() + device.Device = dev + return device, nil + } + + return nil, fmt.Errorf("device %s not found", device.SerialNumber) +} + type AndroidDevice struct { + gadb.Device SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` + IP string `json:"ip,omitempty" yaml:"ip,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } @@ -10,6 +120,436 @@ func (o AndroidDevice) UUID() string { return o.SerialNumber } -func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { - return nil, nil +func DeviceList() (devices []gadb.Device, err error) { + var adbClient gadb.Client + if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { + return nil, err + } + + return adbClient.DeviceList() +} + +// NewUSBDriver creates new client via USB connected device, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + var localPort int + if localPort, err = getFreePort(); err != nil { + return nil, err + } + if err = dev.Forward(localPort, UIA2ServerPort); err != nil { + return nil, err + } + + rawURL := fmt.Sprintf("http://%s%d:6790/wd/hub", forwardToPrefix, localPort) + driver, err = NewUIADriver(capabilities, rawURL) + if err != nil { + _ = dev.ForwardKill(localPort) + return nil, err + } + driver.adbDevice = dev.Device + driver.localPort = localPort + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + driver.client = convertToHTTPClient(conn) + + return driver, nil +} + +// NewHTTPDriver creates new remote HTTP client, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.IP, dev.Port) + if driver, err = NewUIADriver(capabilities, rawURL); err != nil { + return nil, err + } + driver.adbDevice = dev.Device + return driver, nil +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, fmt.Errorf("free port: %w", err) + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, fmt.Errorf("free port: %w", err) + } + defer func() { _ = l.Close() }() + return l.Addr().(*net.TCPAddr).Port, nil +} + +type UiSelectorHelper struct { + value *bytes.Buffer +} + +func NewUiSelectorHelper() UiSelectorHelper { + return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")} +} + +func (s UiSelectorHelper) String() string { + return s.value.String() + ";" +} + +// Text Set the search criteria to match the visible text displayed +// in a widget (for example, the text label to launch an app). +// +// The text for the element must match exactly with the string in your input +// argument. Matching is case-sensitive. +func (s UiSelectorHelper) Text(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.text("%s")`, text)) + return s +} + +// TextMatches Set the search criteria to match the visible text displayed in a layout +// element, using a regular expression. +// +// The text in the widget must match exactly with the string in your +// input argument. +func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex)) + return s +} + +// TextStartsWith Set the search criteria to match visible text in a widget that is +// prefixed by the text parameter. +// +// The matching is case-insensitive. +func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text)) + return s +} + +// TextContains Set the search criteria to match the visible text in a widget +// where the visible text must contain the string in your input argument. +// +// The matching is case-sensitive. +func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text)) + return s +} + +// ClassName Set the search criteria to match the class property +// for a widget (for example, "android.widget.Button"). +func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.className("%s")`, className)) + return s +} + +// ClassNameMatches Set the search criteria to match the class property +// for a widget, using a regular expression. +func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex)) + return s +} + +// Description Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +// +// Matching is case-sensitive. +func (s UiSelectorHelper) Description(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc)) + return s +} + +// DescriptionMatches Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex)) + return s +} + +// DescriptionStartsWith Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must start +// with the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc)) + return s +} + +// DescriptionContains Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must contain +// the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc)) + return s +} + +// ResourceId Set the search criteria to match the given resource ID. +func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id)) + return s +} + +// ResourceIdMatches Set the search criteria to match the resource ID +// of the widget, using a regular expression. +func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex)) + return s +} + +// Index Set the search criteria to match the widget by its node +// index in the layout hierarchy. +// +// The index value must be 0 or greater. +// +// Using the index can be unreliable and should only +// be used as a last resort for matching. Instead, +// consider using the `Instance(int)` method. +func (s UiSelectorHelper) Index(index int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.index(%d)`, index)) + return s +} + +// Instance Set the search criteria to match the +// widget by its instance number. +// +// The instance value must be 0 or greater, where +// the first instance is 0. +// +// For example, to simulate a user click on +// the third image that is enabled in a UI screen, you +// could specify a a search criteria where the instance is +// 2, the `className(String)` matches the image +// widget class, and `enabled(boolean)` is true. +// The code would look like this: +// `new UiSelector().className("android.widget.ImageView") +// .enabled(true).instance(2);` +func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance)) + return s +} + +// Enabled Set the search criteria to match widgets that are enabled. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b)) + return s +} + +// Focused Set the search criteria to match widgets that have focus. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b)) + return s +} + +// Focusable Set the search criteria to match widgets that are focusable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b)) + return s +} + +// Scrollable Set the search criteria to match widgets that are scrollable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b)) + return s +} + +// Selected Set the search criteria to match widgets that +// are currently selected. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b)) + return s +} + +// Checked Set the search criteria to match widgets that +// are currently checked (usually for checkboxes). +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b)) + return s +} + +// Checkable Set the search criteria to match widgets that are checkable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b)) + return s +} + +// Clickable Set the search criteria to match widgets that are clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b)) + return s +} + +// LongClickable Set the search criteria to match widgets that are long-clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b)) + return s +} + +// packageName Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) packageName(name string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name)) + return s +} + +// PackageNameMatches Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex)) + return s +} + +// ChildSelector Adds a child UiSelector criteria to this selector. +// +// Use this selector to narrow the search scope to +// child widgets under a specific parent widget. +func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String())) + return s +} + +// FromParent Adds a child UiSelector criteria to this selector which is used to +// start search from the parent widget. +// +// Use this selector to narrow the search scope to +// sibling widgets as well all child widgets under a parent. +func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String())) + return s +} + +type AndroidBySelector struct { + // Set the search criteria to match the given resource ResourceIdID. + ResourceIdID string `json:"id"` + // Set the search criteria to match the content-description property for a widget. + ContentDescription string `json:"accessibility id"` + XPath string `json:"xpath"` + // Set the search criteria to match the class property for a widget (for example, "android.widget.Button"). + ClassName string `json:"class name"` + UiAutomator string `json:"-android uiautomator"` +} + +func (by AndroidBySelector) getMethodAndSelector() (method, selector string) { + vBy := reflect.ValueOf(by) + tBy := reflect.TypeOf(by) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + // switch vi := vi.(type) { + // case string: + // selector = vi + // } + selector = vi.(string) + if selector != "" && selector != "UNKNOWN" { + method = tBy.Field(i).Tag.Get("json") + return + } + } + return } diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go new file mode 100644 index 00000000..7851a3da --- /dev/null +++ b/hrp/internal/uixt/android_driver.go @@ -0,0 +1,1203 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/electricbubble/gadb" +) + +type uiaDriver struct { + Driver + + adbDevice gadb.Device + localPort int +} + +func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDriver, err error) { + if capabilities == nil { + capabilities = NewCapabilities() + } + driver = new(uiaDriver) + if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { + return nil, err + } + if driver.sessionId, err = driver.NewSession(capabilities); err != nil { + return nil, err + } + return +} + +func (d *uiaDriver) NewSession(capabilities Capabilities) (sessionID string, err error) { + // register(postHandler, new NewSession("/wd/hub/session")) + var rawResp rawResponse + data := map[string]interface{}{"capabilities": capabilities} + if rawResp, err = d.httpPOST(data, "/session"); err != nil { + return "", err + } + reply := new(struct{ Value struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + sessionID = reply.Value.SessionId + // d.sessionIdCache[sessionID] = true + return +} + +func (d *uiaDriver) Quit() (err error) { + // register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId")) + if d.sessionId == "" { + return nil + } + if _, err = d.httpDELETE("/session", d.sessionId); err == nil { + d.sessionId = "" + } + + return err +} + +func (d *uiaDriver) ActiveSessionID() string { + return d.sessionId +} + +func (d *uiaDriver) SessionIDs() (sessionIDs []string, err error) { + // register(getHandler, new GetSessions("/wd/hub/sessions")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/sessions"); err != nil { + return nil, err + } + reply := new(struct{ Value []struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + sessionIDs = make([]string, len(reply.Value)) + for i := range reply.Value { + sessionIDs[i] = reply.Value[i].SessionId + } + return +} + +func (d *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { + // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + scrollData = reply.Value + return +} + +func (d *uiaDriver) Status() (ready bool, err error) { + // register(getHandler, new Status("/wd/hub/status")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/status"); err != nil { + return false, err + } + reply := new(struct { + Value struct { + // Message string + Ready bool + } + }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return false, err + } + ready = reply.Value.Ready + return +} + +// Screenshot grab device screenshot +func (d *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { + // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err + } + + raw = bytes.NewBuffer(decodeStr) + return +} + +func (d *uiaDriver) Orientation() (orientation Orientation, err error) { + // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "orientation"); err != nil { + return "", err + } + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + orientation = reply.Value + return +} + +func (d *uiaDriver) Rotation() (rotation Rotation, err error) { + // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "rotation"); err != nil { + return Rotation{}, err + } + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + + rotation = reply.Value + return +} + +// DeviceSize get window size of the device +func (d *uiaDriver) DeviceSize() (deviceSize Size, err error) { + // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "window/:windowHandle/size"); err != nil { + return Size{}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + + deviceSize = reply.Value + return +} + +// Source get page source +func (d *uiaDriver) Source() (sXML string, err error) { + // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "source"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + sXML = reply.Value + return +} + +// StatusBarHeight get status bar height of the device +func (d *uiaDriver) StatusBarHeight() (height int, err error) { + // register(getHandler, new GetSystemBars("/wd/hub/session/:sessionId/appium/device/system_bars")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/system_bars"); err != nil { + return 0, err + } + reply := new(struct{ Value struct{ StatusBar int } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + + height = reply.Value.StatusBar + return +} + +func (d *uiaDriver) check() error { + if d.adbDevice.Serial() == "" { + return errors.New("adb daemon: the device is not ready") + } + return nil +} + +// Dispose corresponds to the command: +// adb -s $serial forward --remove $localPort +func (d *uiaDriver) Dispose() (err error) { + if err = d.check(); err != nil { + return err + } + if d.localPort == 0 { + return nil + } + return d.adbDevice.ForwardKill(d.localPort) +} + +func (d *uiaDriver) ActiveAppActivity() (appActivity string, err error) { + if err = d.check(); err != nil { + return "", err + } + + var sOutput string + if sOutput, err = d.adbDevice.RunShellCommand("dumpsys activity activities | grep mResumedActivity"); err != nil { + return "", err + } + re := regexp.MustCompile(`\{(.+?)\}`) + if !re.MatchString(sOutput) { + return "", fmt.Errorf("active app activity: %s", strings.TrimSpace(sOutput)) + } + fields := strings.Fields(re.FindStringSubmatch(sOutput)[1]) + appActivity = fields[2] + return +} + +func (d *uiaDriver) ActiveAppPackageName() (appPackageName string, err error) { + var activity string + if activity, err = d.ActiveAppActivity(); err != nil { + return "", err + } + appPackageName = strings.Split(activity, "/")[0] + return +} + +func (d *uiaDriver) AppLaunch(appPackageName string, waitForComplete ...AndroidBySelector) (err error) { + if err = d.check(); err != nil { + return err + } + + var sOutput string + if sOutput, err = d.adbDevice.RunShellCommand("monkey -p", appPackageName, "-c android.intent.category.LAUNCHER 1"); err != nil { + return err + } + if strings.Contains(sOutput, "monkey aborted") { + return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) + } + + if len(waitForComplete) != 0 { + var ce error + exists := func(d *uiaDriver) (bool, error) { + for i := range waitForComplete { + _, ce = d.FindElement(waitForComplete[i]) + if ce == nil { + return true, nil + } + } + return false, nil + } + if err = d.WaitWithTimeoutAndInterval(exists, 45, 1.5); err != nil { + return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + } + } + return +} + +func (d *uiaDriver) AppTerminate(appPackageName string) (err error) { + if err = d.check(); err != nil { + return err + } + + _, err = d.adbDevice.RunShellCommand("am force-stop", appPackageName) + return +} + +func (d *uiaDriver) AppInstall(apkPath string, reinstall ...bool) (err error) { + if err = d.check(); err != nil { + return err + } + + apkName := filepath.Base(apkPath) + if !strings.HasSuffix(strings.ToLower(apkName), ".apk") { + return fmt.Errorf("apk file must have an extension of '.apk': %s", apkPath) + } + + var apkFile *os.File + if apkFile, err = os.Open(apkPath); err != nil { + return fmt.Errorf("apk file: %w", err) + } + + remotePath := path.Join(DeviceTempPath, apkName) + if err = d.adbDevice.PushFile(apkFile, remotePath); err != nil { + return fmt.Errorf("apk push: %w", err) + } + + var shellOutput string + if len(reinstall) != 0 && reinstall[0] { + shellOutput, err = d.adbDevice.RunShellCommand("pm install", "-r", remotePath) + } else { + shellOutput, err = d.adbDevice.RunShellCommand("pm install", remotePath) + } + + if err != nil { + return fmt.Errorf("apk install: %w", err) + } + + if !strings.Contains(shellOutput, "Success") { + return fmt.Errorf("apk installed: %s", shellOutput) + } + + return +} + +func (d *uiaDriver) AppUninstall(appPackageName string, keepDataAndCache ...bool) (err error) { + if err = d.check(); err != nil { + return err + } + + var shellOutput string + if len(keepDataAndCache) != 0 && keepDataAndCache[0] { + shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", "-k", appPackageName) + } else { + shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", appPackageName) + } + + if err != nil { + return fmt.Errorf("apk uninstall: %w", err) + } + + if !strings.Contains(shellOutput, "Success") { + return fmt.Errorf("apk uninstalled: %s", shellOutput) + } + + return +} + +type BatteryStatus int + +const ( + _ = iota + BatteryStatusUnknown BatteryStatus = iota + BatteryStatusCharging + BatteryStatusDischarging + BatteryStatusNotCharging + BatteryStatusFull +) + +func (bs BatteryStatus) String() string { + switch bs { + case BatteryStatusUnknown: + return "unknown" + case BatteryStatusCharging: + return "charging" + case BatteryStatusDischarging: + return "discharging" + case BatteryStatusNotCharging: + return "not charging" + case BatteryStatusFull: + return "full" + default: + return fmt.Sprintf("unknown status code (%d)", bs) + } +} + +func (d *uiaDriver) BatteryInfo() (info BatteryInfo, err error) { + // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/battery_info"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value BatteryInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + + info = reply.Value + if info.Level == -1 || info.Status == -1 { + return info, errors.New("cannot be retrieved from the system") + } + return +} + +func (d *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { + // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + settings = reply.Value + return +} + +// DeviceScaleRatio get device pixel ratio +func (d *uiaDriver) DeviceScaleRatio() (scale float64, err error) { + // register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/pixel_ratio"); err != nil { + return 0, err + } + reply := new(struct{ Value float64 }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + + scale = reply.Value + return +} + +type ( + AndroidDeviceInfo struct { + // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user + // first sets up the device and should remain constant for the lifetime of the user's device. The value + // may change if a factory reset is performed on the device. + AndroidID string `json:"androidId"` + // Build.MANUFACTURER value + Manufacturer string `json:"manufacturer"` + // Build.MODEL value + Model string `json:"model"` + // Build.BRAND value + Brand string `json:"brand"` + // Current running OS's API VERSION + APIVersion string `json:"apiVersion"` + // The current version string, for example "1.0" or "3.4b5" + PlatformVersion string `json:"platformVersion"` + // the name of the current celluar network carrier + CarrierName string `json:"carrierName"` + // the real size of the default display + RealDisplaySize string `json:"realDisplaySize"` + // The logical density of the display in Density Independent Pixel units. + DisplayDensity int `json:"displayDensity"` + // available networks + Networks []networkInfo `json:"networks"` + // current system locale + Locale string `json:"locale"` + // current system timezone + // e.g. "Asia/Tokyo", "America/Caracas", "Asia/Shanghai" + TimeZone string `json:"timeZone"` + Bluetooth struct { + State string `json:"state"` + } `json:"bluetooth"` + } + networkCapabilities struct { + TransportTypes string `json:"transportTypes"` + NetworkCapabilities string `json:"networkCapabilities"` + LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` + LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` + SignalStrength int `json:"signalStrength"` + SSID string `json:"SSID"` + } + networkInfo struct { + Type int `json:"type"` + TypeName string `json:"typeName"` + Subtype int `json:"subtype"` + SubtypeName string `json:"subtypeName"` + IsConnected bool `json:"isConnected"` + DetailedState string `json:"detailedState"` + State string `json:"state"` + ExtraInfo string `json:"extraInfo"` + IsAvailable bool `json:"isAvailable"` + IsRoaming bool `json:"isRoaming"` + IsFailover bool `json:"isFailover"` + Capabilities networkCapabilities `json:"capabilities"` + } +) + +func (d *uiaDriver) DeviceInfo() (info AndroidDeviceInfo, err error) { + // register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/info"); err != nil { + return AndroidDeviceInfo{}, err + } + reply := new(struct{ Value AndroidDeviceInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return AndroidDeviceInfo{}, err + } + + info = reply.Value + return +} + +// AlertText get text of the on-screen dialog +func (d *uiaDriver) AlertText() (text string, err error) { + // register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "alert/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + text = reply.Value + return +} + +// Tap perform a click at arbitrary coordinates specified +func (d *uiaDriver) Tap(x, y int) (err error) { + return d.TapFloat(float64(x), float64(y)) +} + +func (d *uiaDriver) TapFloat(x, y float64) (err error) { + // register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap")) + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/tap") + return +} + +func (d *uiaDriver) TapPoint(point Point) (err error) { + return d.Tap(point.X, point.Y) +} + +func (d *uiaDriver) TapPointF(point PointF) (err error) { + return d.TapFloat(point.X, point.Y) +} + +func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { + // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) + data := map[string]interface{}{ + "startX": startX, + "startY": startY, + "endX": endX, + "endY": endY, + "steps": steps, + } + if len(elementID) != 0 { + data["elementId"] = elementID[0] + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/perform") + return +} + +// Swipe performs a swipe from one coordinate to another using the number of steps +// to determine smoothness and speed. Each step execution is throttled to 5ms +// per step. So for a 100 steps, the swipe will take about 1/2 second to complete. +// `steps` is the number of move steps sent to the system +func (d *uiaDriver) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { + return d.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (d *uiaDriver) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { + if len(steps) == 0 { + steps = []int{12} + } + return d._swipe(startX, startY, endX, endY, steps[0]) +} + +func (d *uiaDriver) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { + return d.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { + return d.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) _drag(data map[string]interface{}) (err error) { + // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/drag") + return +} + +// Drag performs a swipe from one coordinate to another coordinate. You can control +// the smoothness and speed of the swipe by specifying the number of steps. +// Each step execution is throttled to 5 milliseconds per step, so for a 100 +// steps, the swipe will take around 0.5 seconds to complete. +func (d *uiaDriver) Drag(startX, startY, endX, endY int, steps ...int) (err error) { + return d.DragFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (d *uiaDriver) DragFloat(startX, startY, endX, endY float64, steps ...int) error { + if len(steps) == 0 { + steps = []int{12} + } + data := map[string]interface{}{ + "startX": startX, + "startY": startY, + "endX": endX, + "endY": endY, + "steps": steps[0], + } + return d._drag(data) +} + +func (d *uiaDriver) DragPoint(startPoint Point, endPoint Point, steps ...int) error { + return d.Drag(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) DragPointF(startPoint PointF, endPoint PointF, steps ...int) (err error) { + return d.DragFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) TouchLongClick(x, y int, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + "duration": int(duration[0] * 1000), + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/longclick") + return +} + +func (d *uiaDriver) TouchLongClickPoint(point Point, duration ...float64) (err error) { + return d.TouchLongClick(point.X, point.Y, duration...) +} + +func (d *uiaDriver) SendKeys(text string, isReplace ...bool) (err error) { + if len(isReplace) == 0 { + isReplace = []bool{true} + } + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) + // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + "replace": isReplace[0], + } + _, err = d.httpPOST(data, "/session", d.sessionId, "keys") + return +} + +// PressBack simulates a short press on the BACK button. +func (d *uiaDriver) PressBack() (err error) { + // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) + _, err = d.httpPOST(nil, "/session", d.sessionId, "back") + return +} + +// public class KeyCodeModel extends BaseModel { +// @RequiredField +// public Integer keycode; +// public Integer metastate; +// public Integer flags; +// } +func (d *uiaDriver) LongPressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + data := map[string]interface{}{ + "keycode": keyCode, + "metastate": metaState, + "flags": flags[0], + } + // register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/appium/device/long_press_keycode") + return +} + +func (d *uiaDriver) _pressKeyCode(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, + } + if metaState != KMEmpty { + data["metastate"] = metaState + } + if len(flags) != 0 { + data["flags"] = flags[0] + } + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/press_keycode") + return +} + +func (d *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + return d._pressKeyCode(keyCode, metaState, KFFromSystem) +} + +// PressKeyCodeAsync simulates a short press using a key code. +func (d *uiaDriver) PressKeyCodeAsync(keyCode KeyCode, metaState ...KeyMeta) (err error) { + if len(metaState) == 0 { + metaState = []KeyMeta{KMEmpty} + } + return d._pressKeyCode(keyCode, metaState[0]) +} + +func (d *uiaDriver) TouchDown(x, y int) (err error) { + // register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/down") + return +} + +func (d *uiaDriver) TouchDownPoint(point Point) error { + return d.TouchDown(point.X, point.Y) +} + +func (d *uiaDriver) TouchUp(x, y int) (err error) { + // register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/up") + return +} + +func (d *uiaDriver) TouchUpPoint(point Point) error { + return d.TouchUp(point.X, point.Y) +} + +func (d *uiaDriver) TouchMove(x, y int) (err error) { + // register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/move") + return +} + +func (d *uiaDriver) TouchMovePoint(point Point) error { + return d.TouchMove(point.X, point.Y) +} + +// OpenNotification opens the notification shade. +func (d *uiaDriver) OpenNotification() (err error) { + // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) + _, err = d.httpPOST(nil, "/session", d.sessionId, "appium/device/open_notifications") + return +} + +func (d *uiaDriver) _flick(data map[string]interface{}) (err error) { + // register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick")) + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/flick") + return +} + +func (d *uiaDriver) Flick(xSpeed, ySpeed int) (err error) { + data := map[string]interface{}{ + "xspeed": xSpeed, + "yspeed": ySpeed, + } + if xSpeed == 0 && ySpeed == 0 { + return errors.New("both 'xSpeed' and 'ySpeed' cannot be zero") + } + + return d._flick(data) +} + +func (d *uiaDriver) _scrollTo(method, selector string, maxSwipes int, elementID ...string) (err error) { + // register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll")) + params := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if maxSwipes > 0 { + params["maxSwipes"] = maxSwipes + } + data := map[string]interface{}{"params": params} + if len(elementID) != 0 { + data["origin"] = map[string]string{ + legacyWebElementIdentifier: elementID[0], + webElementIdentifier: elementID[0], + } + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/scroll") + return +} + +func (d *uiaDriver) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { + if len(maxSwipes) == 0 { + maxSwipes = []int{0} + } + method, selector := by.getMethodAndSelector() + return d._scrollTo(method, selector, maxSwipes[0]) +} + +type W3CMouseButtonType int + +const ( + MBTLeft W3CMouseButtonType = 0 + MBTMiddle W3CMouseButtonType = 1 + MBTRight W3CMouseButtonType = 2 +) + +func (g *W3CGestures) PointerDown(button ...W3CMouseButtonType) *W3CGestures { + if len(button) == 0 { + button = []W3CMouseButtonType{MBTLeft} + } + *g = append(*g, _newW3CGesture().pointerDown(int(button[0]))) + return g +} + +func (g *W3CGestures) PointerUp(button ...W3CMouseButtonType) *W3CGestures { + if len(button) == 0 { + button = []W3CMouseButtonType{MBTLeft} + } + *g = append(*g, _newW3CGesture().pointerUp(int(button[0]))) + return g +} + +type W3CPointerMoveType string + +const ( + PMTViewport W3CPointerMoveType = "viewport" + PMTPointer W3CPointerMoveType = "pointer" +) + +func (g *W3CGestures) PointerMove(x, y float64, origin interface{}, duration float64, pressure, size float64) *W3CGestures { + val := "" + switch v := origin.(type) { + case string: + val = v + case W3CPointerMoveType: + val = string(v) + case *uiaElement: + val = v.id + default: + val = string(PMTViewport) + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, val, duration, pressure, size)) + return g +} + +func (g *W3CGestures) PointerMoveTo(x, y float64, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTViewport), duration[0]*1000)) + return g +} + +func (g *W3CGestures) PointerMoveRelative(x, y float64, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTPointer), duration[0]*1000)) + return g +} + +func (g *W3CGestures) PointerMouseOver(x, y float64, element *uiaElement, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, element.id, duration[0]*1000)) + return g +} + +type W3CAction map[string]interface{} + +type W3CActionType string + +const ( + _ W3CActionType = "none" + ATKey W3CActionType = "key" + ATPointer W3CActionType = "pointer" +) + +type W3CPointerType string + +const ( + PTMouse W3CPointerType = "mouse" + PTPen W3CPointerType = "pen" + PTTouch W3CPointerType = "touch" +) + +func NewW3CAction(actionType W3CActionType, gestures *W3CGestures, pointerType ...W3CPointerType) W3CAction { + w3cAction := make(W3CAction) + w3cAction["type"] = actionType + w3cAction["actions"] = gestures + if actionType != ATPointer { + return w3cAction + } + + if len(pointerType) == 0 { + pointerType = []W3CPointerType{PTTouch} + } + type W3CItemParameters struct { + PointerType W3CPointerType `json:"pointerType"` + } + w3cAction["parameters"] = W3CItemParameters{PointerType: pointerType[0]} + return w3cAction +} + +func (d *uiaDriver) PerformW3CActions(action W3CAction, acts ...W3CAction) (err error) { + var actionId uint64 = 1 + acts = append([]W3CAction{action}, acts...) + for i := range acts { + item := acts[i] + item["id"] = strconv.FormatUint(actionId, 10) + actionId++ + acts[i] = item + } + data := map[string]interface{}{ + "actions": acts, + } + // register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/actions") + return +} + +type ClipDataType string + +const ClipDataTypePlaintext ClipDataType = "PLAINTEXT" + +func (d *uiaDriver) GetClipboard(contentType ...ClipDataType) (content string, err error) { + if len(contentType) == 0 { + contentType = []ClipDataType{ClipDataTypePlaintext} + } + // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) + data := map[string]interface{}{ + "contentType": contentType[0], + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/get_clipboard"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + content = reply.Value + if data, err := base64.StdEncoding.DecodeString(content); err != nil { + return content, err + } else { + content = string(data) + } + return +} + +func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label ...string) (err error) { + lbl := content + if len(label) != 0 { + lbl = label[0] + } + const defaultLabelLen = 10 + if len(lbl) > defaultLabelLen { + lbl = lbl[:defaultLabelLen] + } + + data := map[string]interface{}{ + "contentType": contentType, + "label": lbl, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + } + // register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard")) + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/set_clipboard") + return +} + +func (d *uiaDriver) AlertAccept(buttonLabel ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(buttonLabel) != 0 { + data["buttonLabel"] = buttonLabel[0] + } + // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) + _, err = d.httpPOST(data, "/session", d.sessionId, "alert/accept") + return +} + +func (d *uiaDriver) AlertDismiss(buttonLabel ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(buttonLabel) != 0 { + data["buttonLabel"] = buttonLabel[0] + } + // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) + _, err = d.httpPOST(data, "/session", d.sessionId, "alert/dismiss") + return +} + +func (d *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (err error) { + data := map[string]interface{}{ + "settings": settings, + } + // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/settings") + return +} + +func (d *uiaDriver) SetOrientation(orientation Orientation) (err error) { + data := map[string]interface{}{ + "orientation": orientation, + } + // register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation")) + _, err = d.httpPOST(data, "/session", d.sessionId, "orientation") + return +} + +// SetRotation +// `x` and `y` are ignored. We only care about `z` +// 0/90/180/270 +func (d *uiaDriver) SetRotation(rotation Rotation) (err error) { + data := map[string]interface{}{ + "z": rotation.Z, + } + // register(postHandler, new SetRotation("/wd/hub/session/:sessionId/rotation")) + _, err = d.httpPOST(data, "/session", d.sessionId, "rotation") + return +} + +type NetworkType int + +const ( + NetworkTypeWifi NetworkType = 2 + + // NetworkTypeNone NetworkType = iota + // NetworkTypeAirplane + // NetworkTypeWifi + // _ + // NetworkTypeData + // _ + // NetworkTypeAll +) + +// NetworkConnection always turn on +func (d *uiaDriver) NetworkConnection(networkType NetworkType) (err error) { + // register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection")) + data := map[string]interface{}{ + "type": networkType, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "network_connection") + return +} + +func (d *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []*uiaElement, err error) { + // register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/elements"); err != nil { + return nil, err + } + reply := new(struct{ Value []map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + elements = make([]*uiaElement, len(reply.Value)) + for i, elem := range reply.Value { + var id string + if id = elementIDFromValue(elem); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elements[i] = &uiaElement{parent: d, id: id} + } + return +} + +func (d *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { + // register(postHandler, new FindElement("/wd/hub/session/:sessionId/element")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/element"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + var id string + if id = elementIDFromValue(reply.Value); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elem = &uiaElement{parent: d, id: id} + return +} + +func (d *uiaDriver) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { + return d._findElements(by.getMethodAndSelector()) +} + +func (d *uiaDriver) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { + return d._findElement(by.getMethodAndSelector()) +} + +func (d *uiaDriver) ActiveElement() (elem *uiaElement, err error) { + // register(getHandler, new ActiveElement("/wd/hub/session/:sessionId/element/active")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "/element/active"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, errors.New("no such element") + } + var id string + if id = elementIDFromValue(reply.Value); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elem = &uiaElement{parent: d, id: id} + return +} + +type AndroidCondition func(d *uiaDriver) (bool, error) + +func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval time.Duration) (err error) { + startTime := time.Now() + for { + done, err := condition(d) + if err != nil { + return err + } + if done { + return nil + } + + if elapsed := time.Since(startTime); elapsed > timeout { + return fmt.Errorf("timeout after %v", elapsed) + } + time.Sleep(interval) + } +} + +// WaitWithTimeoutAndInterval waits for the condition to evaluate to true. +func (d *uiaDriver) WaitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval float64) (err error) { + dTimeout := time.Millisecond * time.Duration(timeout*1000) + dInterval := time.Millisecond * time.Duration(interval*1000) + return d._waitWithTimeoutAndInterval(condition, dTimeout, dInterval) +} + +// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. +func (d *uiaDriver) WaitWithTimeout(condition AndroidCondition, timeout float64) error { + dTimeout := time.Millisecond * time.Duration(timeout*1000) + return d._waitWithTimeoutAndInterval(condition, dTimeout, DefaultWaitInterval) +} + +// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. +func (d *uiaDriver) Wait(condition AndroidCondition) error { + return d._waitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +} diff --git a/hrp/internal/uixt/android_elment.go b/hrp/internal/uixt/android_elment.go new file mode 100644 index 00000000..533217f0 --- /dev/null +++ b/hrp/internal/uixt/android_elment.go @@ -0,0 +1,238 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" +) + +type uiaElement struct { + parent *uiaDriver + id string +} + +func (e *uiaElement) Text() (text string, err error) { + // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + text = reply.Value + return +} + +func (e *uiaElement) GetAttribute(name string) (attribute string, err error) { + // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/attribute", name); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + attribute = reply.Value + return +} + +func (e *uiaElement) ContentDescription() (name string, err error) { + // register(getHandler, new GetName("/wd/hub/session/:sessionId/element/:id/name")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/name"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + name = reply.Value + return +} + +func (e *uiaElement) Size() (size Size, err error) { + // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/size"); err != nil { + return Size{-1, -1}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{-1, -1}, err + } + size = reply.Value + return +} + +func (e *uiaElement) Rect() (rect Rect, err error) { + // register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/rect"); err != nil { + return Rect{}, err + } + reply := new(struct{ Value Rect }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rect{}, err + } + rect = reply.Value + return +} + +func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { + // W3C endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/element/:id/screenshot")) + // JSONWP endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/screenshot/:id")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err + } + + raw = bytes.NewBuffer(decodeStr) + return +} + +func (e *uiaElement) Location() (point Point, err error) { + // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/location"); err != nil { + return Point{-1, -1}, err + } + reply := new(struct{ Value Point }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Point{-1, -1}, err + } + point = reply.Value + return +} + +func (e *uiaElement) Click() (err error) { + // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/click") + return +} + +func (e *uiaElement) Clear() (err error) { + // register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/clear") + return +} + +func (e *uiaElement) SendKeys(text string, isReplace ...bool) (err error) { + if len(isReplace) == 0 { + isReplace = []bool{true} + } + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) + // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + "replace": isReplace[0], + } + _, err = e.parent.httpPOST(data, "/session", e.parent.sessionId, "/element", e.id, "/value") + return +} + +func (e *uiaElement) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { + method, selector := by.getMethodAndSelector() + return e.parent._findElements(method, selector, e.id) +} + +func (e *uiaElement) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { + method, selector := by.getMethodAndSelector() + return e.parent._findElement(method, selector, e.id) +} + +func (e *uiaElement) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { + return e.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (e *uiaElement) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { + if len(steps) == 0 { + steps = []int{12} + } + return e.parent._swipe(startX, startY, endX, endY, steps[0], e.id) +} + +func (e *uiaElement) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { + return e.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { + return e.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) Drag(endX, endY int, steps ...int) (err error) { + return e.DragFloat(float64(endX), float64(endY), steps...) +} + +func (e *uiaElement) DragFloat(endX, endY float64, steps ...int) error { + if len(steps) == 0 { + steps = []int{12 * 10} + } else { + steps[0] = 12 * 10 + } + data := map[string]interface{}{ + "elementId": e.id, + "endX": endX, + "endY": endY, + "steps": steps[0], + } + return e.parent._drag(data) +} + +func (e *uiaElement) DragPoint(endPoint Point, steps ...int) error { + return e.Drag(endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) DragPointF(endPoint PointF, steps ...int) (err error) { + return e.DragFloat(endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) DragTo(destElem *uiaElement, steps ...int) error { + if len(steps) == 0 { + steps = []int{12} + } + data := map[string]interface{}{ + "elementId": e.id, + "destElId": destElem.id, + "steps": steps[0], + } + return e.parent._drag(data) +} + +func (e *uiaElement) Flick(xOffset, yOffset, speed int) (err error) { + data := map[string]interface{}{ + legacyWebElementIdentifier: e.id, + webElementIdentifier: e.id, + "xoffset": xOffset, + "yoffset": yOffset, + "speed": speed, + } + return e.parent._flick(data) +} + +func (e *uiaElement) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { + if len(maxSwipes) == 0 { + maxSwipes = []int{0} + } + method, selector := by.getMethodAndSelector() + return e.parent._scrollTo(method, selector, maxSwipes[0], e.id) +} + +func (e *uiaElement) ScrollToElement(element *uiaElement) (err error) { + // register(postHandler, new ScrollToElement("/wd/hub/session/:sessionId/appium/element/:id/scroll_to/:id2")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/appium/element", e.id, "/scroll_to", element.id) + return +} diff --git a/hrp/internal/uixt/android_key.go b/hrp/internal/uixt/android_key.go new file mode 100644 index 00000000..07f0d5b7 --- /dev/null +++ b/hrp/internal/uixt/android_key.go @@ -0,0 +1,879 @@ +package uixt + +type KeyMeta int + +const ( + KMEmpty KeyMeta = 0 // As a `null` + KMCapLocked KeyMeta = 0x100 // SHIFT key locked in CAPS mode. + KMAltLocked KeyMeta = 0x200 // ALT key locked. + KMSymLocked KeyMeta = 0x400 // SYM key locked. + KMSelecting KeyMeta = 0x800 // Text is in selection mode. + // KMAltOn KeyMeta = 0x02 // This mask is used to check whether one of the ALT meta keys is pressed. + // KMAltLeftOn KeyMeta = 0x10 // This mask is used to check whether the left ALT meta key is pressed. + // KMAltRightOn KeyMeta = 0x20 // This mask is used to check whether the right the ALT meta key is pressed. + // KMShiftOn KeyMeta = 0x1 // This mask is used to check whether one of the SHIFT meta keys is pressed. + // KMShiftLeftOn KeyMeta = 0x40 // This mask is used to check whether the left SHIFT meta key is pressed. + // KMShiftRightOn KeyMeta = 0x80 // This mask is used to check whether the right SHIFT meta key is pressed. + // KMSymOn KeyMeta = 0x4 // This mask is used to check whether the SYM meta key is pressed. + // KMFunctionOn KeyMeta = 0x8 // This mask is used to check whether the FUNCTION meta key is pressed. + // KMCtrlOn KeyMeta = 0x1000 // This mask is used to check whether one of the CTRL meta keys is pressed. + // KMCtrlLeftOn KeyMeta = 0x2000 // This mask is used to check whether the left CTRL meta key is pressed. + // KMCtrlRightOn KeyMeta = 0x4000 // This mask is used to check whether the right CTRL meta key is pressed. + // KMMetaOn KeyMeta = 0x10000 // This mask is used to check whether one of the META meta keys is pressed. + // KMMetaLeftOn KeyMeta = 0x20000 // This mask is used to check whether the left META meta key is pressed. + // KMMetaRightOn KeyMeta = 0x40000 // This mask is used to check whether the right META meta key is pressed. + // KMCapsLockOn KeyMeta = 0x100000 // This mask is used to check whether the CAPS LOCK meta key is on. + // KMNumLockOn KeyMeta = 0x200000 // This mask is used to check whether the NUM LOCK meta key is on. + // KMScrollLockOn KeyMeta = 0x400000 // This mask is used to check whether the SCROLL LOCK meta key is on. + // KMShiftMask = KMShiftOn | KMShiftLeftOn | KMShiftRightOn + // KMAltMask = KMAltOn | KMAltLeftOn | KMAltRightOn + // KMCtrlMask = KMCtrlOn | KMCtrlLeftOn | KMCtrlRightOn + // KMMetaMask = KMMetaOn | KMMetaLeftOn | KMMetaRightOn +) + +type KeyFlag int + +const ( + // KFWokeHere This mask is set if the device woke because of this key event. + // Deprecated + KFWokeHere KeyFlag = 0x1 + + // KFSoftKeyboard This mask is set if the key event was generated by a software keyboard. + KFSoftKeyboard KeyFlag = 0x2 + + // KFKeepTouchMode This mask is set if we don't want the key event to cause us to leave touch mode. + KFKeepTouchMode KeyFlag = 0x4 + + // KFFromSystem This mask is set if an event was known to come from a trusted part + // of the system. That is, the event is known to come from the user, + // and could not have been spoofed by a third party component. + KFFromSystem KeyFlag = 0x8 + + // KFEditorAction This mask is used for compatibility, to identify enter keys that are + // coming from an IME whose enter key has been auto-labelled "next" or + // "done". This allows TextView to dispatch these as normal enter keys + // for old applications, but still do the appropriate action when receiving them. + KFEditorAction KeyFlag = 0x10 + + // KFCanceled When associated with up key events, this indicates that the key press + // has been canceled. Typically this is used with virtual touch screen + // keys, where the user can slide from the virtual key area on to the + // display: in that case, the application will receive a canceled up + // event and should not perform the action normally associated with the + // key. Note that for this to work, the application can not perform an + // action for a key until it receives an up or the long press timeout has expired. + KFCanceled KeyFlag = 0x20 + + // KFVirtualHardKey This key event was generated by a virtual (on-screen) hard key area. + // Typically this is an area of the touchscreen, outside of the regular + // display, dedicated to "hardware" buttons. + KFVirtualHardKey KeyFlag = 0x40 + + // KFLongPress This flag is set for the first key repeat that occurs after the long press timeout. + KFLongPress KeyFlag = 0x80 + + // KFCanceledLongPress Set when a key event has `KFCanceled` set because a long + // press action was executed while it was down. + KFCanceledLongPress KeyFlag = 0x100 + + // KFTracking Set for `ACTION_UP` when this event's key code is still being + // tracked from its initial down. That is, somebody requested that tracking + // started on the key down and a long press has not caused + // the tracking to be canceled. + KFTracking KeyFlag = 0x200 + + // KFFallback Set when a key event has been synthesized to implement default behavior + // for an event that the application did not handle. + // Fallback key events are generated by unhandled trackball motions + // (to emulate a directional keypad) and by certain unhandled key presses + // that are declared in the key map (such as special function numeric keypad + // keys when numlock is off). + KFFallback KeyFlag = 0x400 + + // KFPredispatch Signifies that the key is being predispatched. + // KFPredispatch KeyFlag = 0x20000000 + + // KFStartTracking Private control to determine when an app is tracking a key sequence. + // KFStartTracking KeyFlag = 0x40000000 + + // KFTainted Private flag that indicates when the system has detected that this key event + // may be inconsistent with respect to the sequence of previously delivered key events, + // such as when a key up event is sent but the key was not down. + // KFTainted KeyFlag = 0x80000000 +) + +type KeyCode int + +const ( + _ KeyCode = 0 // Unknown key code. + + // KCSoftLeft Soft Left key + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom left + // of the display. + KCSoftLeft KeyCode = 1 + + // KCSoftRight Soft Right key. + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom right + // of the display. + KCSoftRight KeyCode = 2 + + // KCHome Home key. + // This key is handled by the framework and is never delivered to applications. + KCHome KeyCode = 3 + + KCBack KeyCode = 4 // Back key + KCCall KeyCode = 5 // Call key + KCEndCall KeyCode = 6 // End Call key + KC0 KeyCode = 7 // '0' key + KC1 KeyCode = 8 // '1' key + KC2 KeyCode = 9 // '2' key + KC3 KeyCode = 10 // '3' key + KC4 KeyCode = 11 // '4' key + KC5 KeyCode = 12 // '5' key + KC6 KeyCode = 13 // '6' key + KC7 KeyCode = 14 // '7' key + KC8 KeyCode = 15 // '8' key + KC9 KeyCode = 16 // '9' key + KCStar KeyCode = 17 // '*' key + KCPound KeyCode = 18 // '#' key + + // KCDPadUp KeycodeDPadUp Directional Pad Up key. + // May also be synthesized from trackball motions. + KCDPadUp KeyCode = 19 + + // KCDPadDown Directional Pad Down key. + // May also be synthesized from trackball motions. + KCDPadDown KeyCode = 20 + + // KCDPadLeft Directional Pad Left key. + // May also be synthesized from trackball motions. + KCDPadLeft KeyCode = 21 + + // KCDPadRight Directional Pad Right key. + // May also be synthesized from trackball motions. + KCDPadRight KeyCode = 22 + + // KCDPadCenter Directional Pad Center key. + // May also be synthesized from trackball motions. + KCDPadCenter KeyCode = 23 + + // KCVolumeUp Volume Up key. + // Adjusts the speaker volume up. + KCVolumeUp KeyCode = 24 + + // KCVolumeDown Volume Down key. + // Adjusts the speaker volume down. + KCVolumeDown KeyCode = 25 + + // KCPower Power key. + KCPower KeyCode = 26 + + // KCCamera Camera key. + // Used to launch a camera application or take pictures. + KCCamera KeyCode = 27 + + KCClear KeyCode = 28 // Clear key + KCa KeyCode = 29 // 'a' key + KCb KeyCode = 30 // 'b' key + KCc KeyCode = 31 // 'c' key + KCd KeyCode = 32 // 'd' key + KCe KeyCode = 33 // 'e' key + KCf KeyCode = 34 // 'f' key + KCg KeyCode = 35 // 'g' key + KCh KeyCode = 36 // 'h' key + KCi KeyCode = 37 // 'i' key + KCj KeyCode = 38 // 'j' key + KCk KeyCode = 39 // 'k' key + KCl KeyCode = 40 // 'l' key + KCm KeyCode = 41 // 'm' key + KCn KeyCode = 42 // 'n' key + KCo KeyCode = 43 // 'o' key + KCp KeyCode = 44 // 'p' key + KCq KeyCode = 45 // 'q' key + KCr KeyCode = 46 // 'r' key + KCs KeyCode = 47 // 's' key + KCt KeyCode = 48 // 't' key + KCu KeyCode = 49 // 'u' key + KCv KeyCode = 50 // 'v' key + KCw KeyCode = 51 // 'w' key + KCx KeyCode = 52 // 'x' key + KCy KeyCode = 53 // 'y' key + KCz KeyCode = 54 // 'z' key + KCComma KeyCode = 55 // ',' key + KCPeriod KeyCode = 56 // '.' key + KCAltLeft KeyCode = 57 // Left Alt modifier key + KCAltRight KeyCode = 58 // Right Alt modifier key + KCShiftLeft KeyCode = 59 // Left Shift modifier key + KCShiftRight KeyCode = 60 // Right Shift modifier key + KCTab KeyCode = 61 // Tab key + KCSpace KeyCode = 62 // Space key + + // KCSym Symbol modifier key. + // Used to enter alternate symbols. + KCSym KeyCode = 63 + + // KCExplorer Explorer special function key. + // Used to launch a browser application. + KCExplorer KeyCode = 64 + + // KCEnvelope Envelope special function key. + // Used to launch a mail application. + KCEnvelope KeyCode = 65 + + // KCEnter Enter key. + KCEnter KeyCode = 66 + + // KCDel Backspace key. + // Deletes characters before the insertion point, unlike `KCForwardDel`. + KCDel KeyCode = 67 + + KCGrave KeyCode = 68 // '`' (backtick) key + KCMinus KeyCode = 69 // '-' + KCEquals KeyCode = 70 // '=' key + KCLeftBracket KeyCode = 71 // '[' key + KCRightBracket KeyCode = 72 // ']' key + KCBackslash KeyCode = 73 // '\' key + KCSemicolon KeyCode = 74 // '' key + KCApostrophe KeyCode = 75 // ''' (apostrophe) key + KCSlash KeyCode = 76 // '/' key + KCAt KeyCode = 77 // '@' key + + // KCNum Number modifier key. + // Used to enter numeric symbols. + // This key is not Num Lock; it is more like `KCAltLeft` and is + // interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. + KCNum KeyCode = 78 + + // KCHeadsetHook Headset Hook key. + // Used to hang up calls and stop media. + KCHeadsetHook KeyCode = 79 + + // KCFocus Camera Focus key. + // Used to focus the camera. + // *Camera* focus + KCFocus KeyCode = 80 + + KCPlus KeyCode = 81 // '+' key. + KCMenu KeyCode = 82 // Menu key. + KCNotification KeyCode = 83 // Notification key. + KCSearch KeyCode = 84 // Search key. + KCMediaPlayPause KeyCode = 85 // Play/Pause media key. + KCMediaStop KeyCode = 86 // Stop media key. + KCMediaNext KeyCode = 87 // Play Next media key. + KCMediaPrevious KeyCode = 88 // Play Previous media key. + KCMediaRewind KeyCode = 89 // Rewind media key. + KCMediaFastForward KeyCode = 90 // Fast Forward media key. + + // KCMute Mute key. + // Mutes the microphone, unlike `KCVolumeMute` + KCMute KeyCode = 91 + + // KCPageUp Page Up key. + KCPageUp KeyCode = 92 + + // KCPageDown Page Down key. + KCPageDown KeyCode = 93 + + // KCPictSymbols Picture Symbols modifier key. + // Used to switch symbol sets (Emoji, Kao-moji). + // switch symbol-sets (Emoji,Kao-moji) + KCPictSymbols KeyCode = 94 + + // KCSwitchCharset Switch Charset modifier key. + // Used to switch character sets (Kanji, Katakana). + // switch char-sets (Kanji,Katakana) + KCSwitchCharset KeyCode = 95 + + // KCButtonA A Button key. + // On a game controller, the A button should be either the button labeled A + // or the first button on the bottom row of controller buttons. + KCButtonA KeyCode = 96 + + // KCButtonB B Button key. + // On a game controller, the B button should be either the button labeled B + // or the second button on the bottom row of controller buttons. + KCButtonB KeyCode = 97 + + // KCButtonC C Button key. + // On a game controller, the C button should be either the button labeled C + // or the third button on the bottom row of controller buttons. + KCButtonC KeyCode = 98 + + // KCButtonX X Button key. + // On a game controller, the X button should be either the button labeled X + // or the first button on the upper row of controller buttons. + KCButtonX KeyCode = 99 + + // KCButtonY Y Button key. + // On a game controller, the Y button should be either the button labeled Y + // or the second button on the upper row of controller buttons. + KCButtonY KeyCode = 100 + + // KCButtonZ Z Button key. + // On a game controller, the Z button should be either the button labeled Z + // or the third button on the upper row of controller buttons. + KCButtonZ KeyCode = 101 + + // KCButtonL1 L1 Button key. + // On a game controller, the L1 button should be either the button labeled L1 (or L) + // or the top left trigger button. + KCButtonL1 KeyCode = 102 + + // KCButtonR1 R1 Button key. + // On a game controller, the R1 button should be either the button labeled R1 (or R) + // or the top right trigger button. + KCButtonR1 KeyCode = 103 + + // KCButtonL2 L2 Button key. + // On a game controller, the L2 button should be either the button labeled L2 + // or the bottom left trigger button. + KCButtonL2 KeyCode = 104 + + // KCButtonR2 R2 Button key. + // On a game controller, the R2 button should be either the button labeled R2 + // or the bottom right trigger button. + KCButtonR2 KeyCode = 105 + + // KCButtonTHUMBL Left Thumb Button key. + // On a game controller, the left thumb button indicates that the left (or only) + // joystick is pressed. + KCButtonTHUMBL KeyCode = 106 + + // KCButtonTHUMBR Right Thumb Button key. + // On a game controller, the right thumb button indicates that the right + // joystick is pressed. + KCButtonTHUMBR KeyCode = 107 + + // KCButtonStart Start Button key. + // On a game controller, the button labeled Start. + KCButtonStart KeyCode = 108 + + // KCButtonSelect Select Button key. + // On a game controller, the button labeled Select. + KCButtonSelect KeyCode = 109 + + // KCButtonMode Mode Button key. + // On a game controller, the button labeled Mode. + KCButtonMode KeyCode = 110 + + // KCEscape Escape key. + KCEscape KeyCode = 111 + + // KCForwardDel Forward Delete key. + // Deletes characters ahead of the insertion point, unlike `KCDel`. + KCForwardDel KeyCode = 112 + + KCCtrlLeft KeyCode = 113 // Left Control modifier key + KCCtrlRight KeyCode = 114 // Right Control modifier key + KCCapsLock KeyCode = 115 // Caps Lock key + KCScrollLock KeyCode = 116 // Scroll Lock key + KCMetaLeft KeyCode = 117 // Left Meta modifier key + KCMetaRight KeyCode = 118 // Right Meta modifier key + KCFunction KeyCode = 119 // Function modifier key + KCSysRq KeyCode = 120 // System Request / Print Screen key + KCBreak KeyCode = 121 // Break / Pause key + + // KCMoveHome Home Movement key. + // Used for scrolling or moving the cursor around to the start of a line + // or to the top of a list. + KCMoveHome KeyCode = 122 + + // KCMoveEnd End Movement key. + // Used for scrolling or moving the cursor around to the end of a line + // or to the bottom of a list. + KCMoveEnd KeyCode = 123 + + // KCInsert Insert key. + // Toggles insert / overwrite edit mode. + KCInsert KeyCode = 124 + + // KCForward Forward key. + // Navigates forward in the history stack. Complement of `KCBack`. + KCForward KeyCode = 125 + + // KCMediaPlay Play media key. + KCMediaPlay KeyCode = 126 + + // KCMediaPause Pause media key. + KCMediaPause KeyCode = 127 + + // KCMediaClose Close media key. + // May be used to close a CD tray, for example. + KCMediaClose KeyCode = 128 + + // KCMediaEject Eject media key. + // May be used to eject a CD tray, for example. + KCMediaEject KeyCode = 129 + + // KCMediaRecord Record media key. + KCMediaRecord KeyCode = 130 + + KCF1 KeyCode = 131 // F1 key. + KCF2 KeyCode = 132 // F2 key. + KCF3 KeyCode = 133 // F3 key. + KCF4 KeyCode = 134 // F4 key. + KCF5 KeyCode = 135 // F5 key. + KCF6 KeyCode = 136 // F6 key. + KCF7 KeyCode = 137 // F7 key. + KCF8 KeyCode = 138 // F8 key. + KCF9 KeyCode = 139 // F9 key. + KCF10 KeyCode = 140 // F10 key. + KCF11 KeyCode = 141 // F11 key. + KCF12 KeyCode = 142 // F12 key. + + // KCNumLock Num Lock key. + // This is the Num Lock key; it is different from `KCNum`. + // This key alters the behavior of other keys on the numeric keypad. + KCNumLock KeyCode = 143 + + KCNumpad0 KeyCode = 144 // Numeric keypad '0' key + KCNumpad1 KeyCode = 145 // Numeric keypad '1' key + KCNumpad2 KeyCode = 146 // Numeric keypad '2' key + KCNumpad3 KeyCode = 147 // Numeric keypad '3' key + KCNumpad4 KeyCode = 148 // Numeric keypad '4' key + KCNumpad5 KeyCode = 149 // Numeric keypad '5' key + KCNumpad6 KeyCode = 150 // Numeric keypad '6' key + KCNumpad7 KeyCode = 151 // Numeric keypad '7' key + KCNumpad8 KeyCode = 152 // Numeric keypad '8' key + KCNumpad9 KeyCode = 153 // Numeric keypad '9' key + KCNumpadDivide KeyCode = 154 // Numeric keypad '/' key (for division) + KCNumpadMultiply KeyCode = 155 // Numeric keypad '*' key (for multiplication) + KCNumpadSubtract KeyCode = 156 // Numeric keypad '-' key (for subtraction) + KCNumpadAdd KeyCode = 157 // Numeric keypad '+' key (for addition) + KCNumpadDot KeyCode = 158 // Numeric keypad '.' key (for decimals or digit grouping) + KCNumpadComma KeyCode = 159 // Numeric keypad ',' key (for decimals or digit grouping) + KCNumpadEnter KeyCode = 160 // Numeric keypad Enter key + KCNumpadEquals KeyCode = 161 // Numeric keypad 'KeyCode =' key + KCNumpadLeftParen KeyCode = 162 // Numeric keypad '(' key + KCNumpadRightParen KeyCode = 163 // Numeric keypad ')' key + + // KCVolumeMute Volume Mute key. + // Mutes the speaker, unlike `KCMute`. + // This key should normally be implemented as a toggle such that the first press + // mutes the speaker and the second press restores the original volume. + KCVolumeMute KeyCode = 164 + + // KCInfo Info key. + // Common on TV remotes to show additional information related to what is + // currently being viewed. + KCInfo KeyCode = 165 + + // KCChannelUp Channel up key. + // On TV remotes, increments the television channel. + KCChannelUp KeyCode = 166 + + // KCChannelDown Channel down key. + // On TV remotes, decrements the television channel. + KCChannelDown KeyCode = 167 + + // KCZoomIn Zoom in key. + KCZoomIn KeyCode = 168 + + // KCZoomOut Zoom out key. + KCZoomOut KeyCode = 169 + + // KCTv TV key. + // On TV remotes, switches to viewing live TV. + KCTv KeyCode = 170 + + // KCWindow Window key. + // On TV remotes, toggles picture-in-picture mode or other windowing functions. + // On Android Wear devices, triggers a display offset. + KCWindow KeyCode = 171 + + // KCGuide Guide key. + // On TV remotes, shows a programming guide. + KCGuide KeyCode = 172 + + // KCDvr DVR key. + // On some TV remotes, switches to a DVR mode for recorded shows. + KCDvr KeyCode = 173 + + // KCBookmark Bookmark key. + // On some TV remotes, bookmarks content or web pages. + KCBookmark KeyCode = 174 + + // KCCaptions Toggle captions key. + // Switches the mode for closed-captioning text, for example during television shows. + KCCaptions KeyCode = 175 + + // KCSettings Settings key. + // Starts the system settings activity. + KCSettings KeyCode = 176 + + // KCTvPower TV power key. + // On TV remotes, toggles the power on a television screen. + KCTvPower KeyCode = 177 + + // KCTvInput TV input key. + // On TV remotes, switches the input on a television screen. + KCTvInput KeyCode = 178 + + // KCStbPower Set-top-box power key. + // On TV remotes, toggles the power on an external Set-top-box. + KCStbPower KeyCode = 179 + + // KCStbInput Set-top-box input key. + // On TV remotes, switches the input mode on an external Set-top-box. + KCStbInput KeyCode = 180 + + // KCAvrPower A/V Receiver power key. + // On TV remotes, toggles the power on an external A/V Receiver. + KCAvrPower KeyCode = 181 + + // KCAvrInput A/V Receiver input key. + // On TV remotes, switches the input mode on an external A/V Receiver. + KCAvrInput KeyCode = 182 + + // KCProgRed Red "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgRed KeyCode = 183 + + // KCProgGreen Green "programmable" key. + // On TV remotes, actsas a contextual/programmable key. + KCProgGreen KeyCode = 184 + + // KCProgYellow Yellow "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgYellow KeyCode = 185 + + // KCProgBlue Blue "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgBlue KeyCode = 186 + + // KCAppSwitch App switch key. + // Should bring up the application switcher dialog. + KCAppSwitch KeyCode = 187 + + KCButton1 KeyCode = 188 // Generic Game Pad Button #1 + KCButton2 KeyCode = 189 // Generic Game Pad Button #2 + KCButton3 KeyCode = 190 // Generic Game Pad Button #3 + KCButton4 KeyCode = 191 // Generic Game Pad Button #4 + KCButton5 KeyCode = 192 // Generic Game Pad Button #5 + KCButton6 KeyCode = 193 // Generic Game Pad Button #6 + KCButton7 KeyCode = 194 // Generic Game Pad Button #7 + KCButton8 KeyCode = 195 // Generic Game Pad Button #8 + KCButton9 KeyCode = 196 // Generic Game Pad Button #9 + KCButton10 KeyCode = 197 // Generic Game Pad Button #10 + KCButton11 KeyCode = 198 // Generic Game Pad Button #11 + KCButton12 KeyCode = 199 // Generic Game Pad Button #12 + KCButton13 KeyCode = 200 // Generic Game Pad Button #13 + KCButton14 KeyCode = 201 // Generic Game Pad Button #14 + KCButton15 KeyCode = 202 // Generic Game Pad Button #15 + KCButton16 KeyCode = 203 // Generic Game Pad Button #16 + + // KCLanguageSwitch Language Switch key. + // Toggles the current input language such as switching between English and Japanese on + // a QWERTY keyboard. On some devices, the same function may be performed by + // pressing Shift+Spacebar. + KCLanguageSwitch KeyCode = 204 + + // Manner Mode key. + // Toggles silent or vibrate mode on and off to make the device behave more politely + // in certain settings such as on a crowded train. On some devices, the key may only + // operate when long-pressed. + KCMannerMode KeyCode = 205 + + // 3D Mode key. + // Toggles the display between 2D and 3D mode. + KC3dMode KeyCode = 206 + + // Contacts special function key. + // Used to launch an address book application. + KCContacts KeyCode = 207 + + // Calendar special function key. + // Used to launch a calendar application. + KCCalendar KeyCode = 208 + + // Music special function key. + // Used to launch a music player application. + KCMusic KeyCode = 209 + + // Calculator special function key. + // Used to launch a calculator application. + KCCalculator KeyCode = 210 + + // Japanese full-width / half-width key. + KCZenkakuHankaku KeyCode = 211 + + // Japanese alphanumeric key. + KCEisu KeyCode = 212 + + // Japanese non-conversion key. + KCMuhenkan KeyCode = 213 + + // Japanese conversion key. + KCHenkan KeyCode = 214 + + // Japanese katakana / hiragana key. + KCKatakanaHiragana KeyCode = 215 + + // Japanese Yen key. + KCYen KeyCode = 216 + + // Japanese Ro key. + KCRo KeyCode = 217 + + // Japanese kana key. + KCKana KeyCode = 218 + + // Assist key. + // Launches the global assist activity. Not delivered to applications. + KCAssist KeyCode = 219 + + // Brightness Down key. + // Adjusts the screen brightness down. + KCBrightnessDown KeyCode = 220 + + // Brightness Up key. + // Adjusts the screen brightness up. + KCBrightnessUp KeyCode = 221 + + // Audio Track key. + // Switches the audio tracks. + KCMediaAudioTrack KeyCode = 222 + + // Sleep key. + // Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already asleep. + KCSleep KeyCode = 223 + + // Wakeup key. + // Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already awake. + KCWakeup KeyCode = 224 + + // Pairing key. + // Initiates peripheral pairing mode. Useful for pairing remote control + // devices or game controllers, especially if no other input mode is + // available. + KCPairing KeyCode = 225 + + // Media Top Menu key. + // Goes to the top of media menu. + KCMediaTopMenu KeyCode = 226 + + // '11' key. + KC11 KeyCode = 227 + + // '12' key. + KC12 KeyCode = 228 + + // Last Channel key. + // Goes to the last viewed channel. + KCLastChannel KeyCode = 229 + + // TV data service key. + // Displays data services like weather, sports. + KCTvDataService KeyCode = 230 + + // Voice Assist key. + // Launches the global voice assist activity. Not delivered to applications. + KCVoiceAssist KeyCode = 231 + + // Radio key. + // Toggles TV service / Radio service. + KCTvRadioService KeyCode = 232 + + // Teletext key. + // Displays Teletext service. + KCTvTeletext KeyCode = 233 + + // Number entry key. + // Initiates to enter multi-digit channel nubmber when each digit key is assigned + // for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + // User Control Code. + KCTvNumberEntry KeyCode = 234 + + // Analog Terrestrial key. + // Switches to analog terrestrial broadcast service. + KCTvTerrestrialAnalog KeyCode = 235 + + // Digital Terrestrial key. + // Switches to digital terrestrial broadcast service. + KCTvTerrestrialDigital KeyCode = 236 + + // Satellite key. + // Switches to digital satellite broadcast service. + KCTvSatellite KeyCode = 237 + + // BS key. + // Switches to BS digital satellite broadcasting service available in Japan. + KCTvSatelliteBs KeyCode = 238 + + // CS key. + // Switches to CS digital satellite broadcasting service available in Japan. + KCTvSatelliteCs KeyCode = 239 + + // BS/CS key. + // Toggles between BS and CS digital satellite services. + KCTvSatelliteService KeyCode = 240 + + // Toggle Network key. + // Toggles selecting broacast services. + KCTvNetwork KeyCode = 241 + + // Antenna/Cable key. + // Toggles broadcast input source between antenna and cable. + KCTvAntennaCable KeyCode = 242 + + // HDMI #1 key. + // Switches to HDMI input #1. + KCTvInputHdmi1 KeyCode = 243 + + // HDMI #2 key. + // Switches to HDMI input #2. + KCTvInputHdmi2 KeyCode = 244 + + // HDMI #3 key. + // Switches to HDMI input #3. + KCTvInputHdmi3 KeyCode = 245 + + // HDMI #4 key. + // Switches to HDMI input #4. + KCTvInputHdmi4 KeyCode = 246 + + // Composite #1 key. + // Switches to composite video input #1. + KCTvInputComposite1 KeyCode = 247 + + // Composite #2 key. + // Switches to composite video input #2. + KCTvInputComposite2 KeyCode = 248 + + // Component #1 key. + // Switches to component video input #1. + KCTvInputComponent1 KeyCode = 249 + + // Component #2 key. + // Switches to component video input #2. + KCTvInputComponent2 KeyCode = 250 + + // VGA #1 key. + // Switches to VGA (analog RGB) input #1. + KCTvInputVga1 KeyCode = 251 + + // Audio description key. + // Toggles audio description off / on. + KCTvAudioDescription KeyCode = 252 + + // Audio description mixing volume up key. + // Louden audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixUp KeyCode = 253 + + // Audio description mixing volume down key. + // Lessen audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixDown KeyCode = 254 + + // Zoom mode key. + // Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) + KCTvZoomMode KeyCode = 255 + + // Contents menu key. + // Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + // Code + KCTvContentsMenu KeyCode = 256 + + // Media context menu key. + // Goes to the context menu of media contents. Corresponds to Media Context-sensitive + // Menu (0x11) of CEC User Control Code. + KCTvMediaContextMenu KeyCode = 257 + + // Timer programming key. + // Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + // CEC User Control Code. + KCTvTimerProgramming KeyCode = 258 + + // Help key. + KCHelp KeyCode = 259 + + // Navigate to previous key. + // Goes backward by one item in an ordered collection of items. + KCNavigatePrevious KeyCode = 260 + + // Navigate to next key. + // Advances to the next item in an ordered collection of items. + KCNavigateNext KeyCode = 261 + + // Navigate in key. + // Activates the item that currently has focus or expands to the next level of a navigation + // hierarchy. + KCNavigateIn KeyCode = 262 + + // Navigate out key. + // Backs out one level of a navigation hierarchy or collapses the item that currently has + // focus. + KCNavigateOut KeyCode = 263 + + // Primary stem key for Wear + // Main power/reset button on watch. + KCStemPrimary KeyCode = 264 + + // Generic stem key 1 for Wear + KCStem1 KeyCode = 265 + + // Generic stem key 2 for Wear + KCStem2 KeyCode = 266 + + // Generic stem key 3 for Wear + KCStem3 KeyCode = 267 + + // Directional Pad Up-Left + KCDPadUpLeft KeyCode = 268 + + // Directional Pad Down-Left + KCDPadDownLeft KeyCode = 269 + + // Directional Pad Up-Right + KCDPadUpRight KeyCode = 270 + + // Directional Pad Down-Right + KCDPadDownRight KeyCode = 271 + + // Skip forward media key. + KCMediaSkipForward KeyCode = 272 + + // Skip backward media key. + KCMediaSkipBackward KeyCode = 273 + + // Step forward media key. + // Steps media forward, one frame at a time. + KCMediaStepForward KeyCode = 274 + + // Step backward media key. + // Steps media backward, one frame at a time. + KCMediaStepBackward KeyCode = 275 + + // put device to sleep unless a wakelock is held. + KCSoftSleep KeyCode = 276 + + // Cut key. + KCCut KeyCode = 277 + + // Copy key. + KCCopy KeyCode = 278 + + // Paste key. + KCPaste KeyCode = 279 + + // Consumed by the system for navigation up + KCSystemNavigationUp KeyCode = 280 + + // Consumed by the system for navigation down + KCSystemNavigationDown KeyCode = 281 + + // Consumed by the system for navigation left*/ + KCSystemNavigationLeft KeyCode = 282 + + // Consumed by the system for navigation right + KCSystemNavigationRight KeyCode = 283 + + // Show all apps + KCAllApps KeyCode = 284 + + // Refresh key. + KCRefresh KeyCode = 285 +) diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go new file mode 100644 index 00000000..2efa9c66 --- /dev/null +++ b/hrp/internal/uixt/android_test.go @@ -0,0 +1,1384 @@ +package uixt + +import ( + "io/ioutil" + "testing" + "time" +) + +var uiaServerURL = "http://localhost:6790/wd/hub" + +func TestDriver_NewSession(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + firstMatchEntry := make(map[string]interface{}) + firstMatchEntry["package"] = "com.android.settings" + firstMatchEntry["activity"] = "com.android.settings/.Settings" + caps := Capabilities{ + "firstMatch": []interface{}{firstMatchEntry}, + "alwaysMatch": struct{}{}, + } + sessionID, err := driver.NewSession(caps) + if err != nil { + t.Fatal(err) + } + if len(sessionID) == 0 { + t.Fatal("should not be empty") + } +} + +func TestNewDriver(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + t.Log(driver.sessionId) +} + +func TestDriver_Quit(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + if err = driver.Quit(); err != nil { + t.Fatal(err) + } +} + +func TestDriver_Status(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + _, err = driver.Status() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SessionIDs(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + sessions, err := driver.SessionIDs() + if err != nil { + t.Fatal(err) + } + if len(sessions) == 0 { + t.Fatal("should have at least one") + } + t.Log(len(sessions), sessions) +} + +func TestDriver_SessionDetails(t *testing.T) { + // firstMatchEntry := make(map[string]interface{}) + // firstMatchEntry["package"] = "com.android.settings" + // firstMatchEntry["activity"] = "com.android.settings/.Settings" + // caps = Capabilities{ + // "firstMatch": []interface{}{firstMatchEntry}, + // "alwaysMatch": struct{}{}, + // } + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + scrollData, err := driver.SessionDetails() + if err != nil { + t.Fatal(err) + } + + t.Log(scrollData) +} + +func TestDriver_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0o600)) +} + +func TestDriver_Orientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + + t.Log(orientation) +} + +func TestDriver_Rotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal(err) + } + + t.Logf("x = %d\ty = %d\tz = %d", rotation.X, rotation.Y, rotation.Z) +} + +func TestDriver_DeviceSize(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + deviceSize, err := driver.DeviceSize() + if err != nil { + t.Fatal(err) + } + + t.Logf("width = %d\theight = %d", deviceSize.Width, deviceSize.Height) +} + +func TestDriver_Source(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + source, err := driver.Source() + if err != nil { + t.Fatal(err) + } + + t.Log(source) +} + +func TestDriver_StatusBarHeight(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + statusBarHeight, err := driver.StatusBarHeight() + if err != nil { + t.Fatal(err) + } + + t.Log(statusBarHeight) +} + +func TestDriver_BatteryInfo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal(err) + } + + t.Log(batteryInfo) +} + +func TestDriver_GetAppiumSettings(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + + for k := range appiumSettings { + t.Logf("key: %s\tvalue: %v", k, appiumSettings[k]) + } + // t.Log(appiumSettings) +} + +func TestDriver_DeviceScaleRatio(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + scaleRatio, err := driver.DeviceScaleRatio() + if err != nil { + t.Fatal(err) + } + + t.Log(scaleRatio) +} + +func TestDriver_DeviceInfo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + devInfo, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + + t.Logf("api version: %s", devInfo.APIVersion) + t.Logf("platform version: %s", devInfo.PlatformVersion) + t.Logf("bluetooth state: %s", devInfo.Bluetooth.State) +} + +func TestDriver_AlertText(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + alertText, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + + t.Log(alertText) +} + +func TestDriver_Tap(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Tap(150, 340) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapFloat(60.5, 125.5) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapPoint(Point{X: 150, Y: 340}) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapPointF(PointF{X: 60.5, Y: 125.5}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Swipe(400, 1000, 400, 500, 10) + if err != nil { + t.Fatal(err) + } + + err = driver.SwipeFloat(400, 555.5, 400, 1255.5) + if err != nil { + t.Fatal(err) + } + + startPoint := Point{400, 1000} + endPoint := Point{400, 500} + err = driver.SwipePoint(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } + + startPointF := PointF{400, 555.5} + endPointF := PointF{400, 1255.5} + err = driver.SwipePointF(startPointF, endPointF) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Drag(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Drag(400, 260, 400, 500, 10) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + err = driver.DragFloat(400, 501.5, 400, 261.5, 10) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + startPoint := Point{400, 260} + endPoint := Point{400, 500} + err = driver.DragPoint(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + startPointF := PointF{400.5, 501.5} + endPointF := PointF{400.5, 261.5} + err = driver.DragPointF(startPointF, endPointF) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchLongClick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.TouchLongClick(400, 260, 1.2222) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + err = driver.TouchLongClickPoint(Point{X: 400, Y: 260}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SendKeys("abc") + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("def", false) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("\\n") + // err = driver.SendKeys(`\n`, false) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PressBack(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.PressBack() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PressKeyCode(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.PressKeyCodeAsync(KCx) + if err != nil { + t.Fatal(err) + } + err = driver.PressKeyCodeAsync(KCx, KMCapLocked) + if err != nil { + t.Fatal(err) + } + // err = driver.PressKeyCodeAsync(KCExplorer) + // if err != nil { + // t.Fatal(err) + // } + + err = driver.PressKeyCode(KCExplorer, KMEmpty) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_LongPressKeyCode(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.LongPressKeyCode(KCAt, KMEmpty) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchDown(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + doTouchUp := func() { + err = driver.TouchUp(400, 260) + if err != nil { + t.Fatal(err) + } + } + + err = driver.TouchDown(400, 260) + if err != nil { + t.Fatal(err) + } + + // _ = driver.TapPoint(Point{400, 500}) + doTouchUp() + + err = driver.TouchDownPoint(Point{400, 260}) + if err != nil { + t.Fatal(err) + } + + doTouchUp() +} + +func TestDriver_TouchUp(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.TouchDown(400, 260) + if err != nil { + t.Fatal(err) + } + + // err = driver.TouchUp(400, 260) + err = driver.TouchUpPoint(Point{400, 260}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchMove(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + doTouchDown := func(x, y int) { + err = driver.TouchDown(x, y) + if err != nil { + t.Fatal(err) + } + } + + doTouchUp := func(x, y int) { + err = driver.TouchUp(x, y) + if err != nil { + t.Fatal(err) + } + } + + doTouchDown(400, 260) + + err = driver.TouchMove(400, 500) + if err != nil { + t.Fatal(err) + } + + doTouchUp(400, 500) + + doTouchDown(400, 500) + + err = driver.TouchMove(400, 260) + if err != nil { + t.Fatal(err) + } + + doTouchUp(400, 260) +} + +func TestDriver_OpenNotification(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.OpenNotification() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Flick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Flick(50, -100) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_ScrollTo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.ScrollTo(AndroidBySelector{ClassName: "android.widget.SeekBar"}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_MultiPointerGesture(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) + gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) + gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) + _ = gesture3 + + // err = driver.MultiPointerGesture(gesture1, gesture2) + err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PerformW3CActions(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) + // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) + // err = driver.PerformW3CActions(actionKey) + // if err != nil { + // t.Fatal(err) + // } + + // var queryField map[string]string + // queryField = make(map[string]string) + // { + // queryField = map[string]string{ + // "a": "", + // } + // } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) + // actionPointer := NewW3CAction(ATPointer, + // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). + // PointerMove(0, 0, elem.id).Pause(2). + // PointerMove(20, 0, "pointer").Pause(2). + // PointerUp(), + // ) + actionPointer := NewW3CAction(ATPointer, + NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). + PointerMouseOver(0, 0, elem). + PointerMoveRelative(20, 0).PointerUp()) + err = driver.PerformW3CActions(actionPointer) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_GetClipboard(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + text, err := driver.GetClipboard() + if err != nil { + t.Fatal(err) + } + t.Log(text) +} + +func TestDriver_SetClipboard(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + content := "test123" + err = driver.SetClipboard(ClipDataTypePlaintext, content) + if err != nil { + t.Fatal(err) + } + + text, err := driver.GetClipboard() + if err != nil { + t.Fatal(err) + } + if text != content { + t.Fatal("should be the same") + } +} + +func TestDriver_AlertAccept(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.AlertAccept() + // err = driver.AlertAccept("是") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AlertDismiss(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.AlertDismiss() + err = driver.AlertDismiss("否") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SetAppiumSettings(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + sdopd := appiumSettings["shutdownOnPowerDisconnect"] + t.Log("shutdownOnPowerDisconnect:", sdopd) + + err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err = driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { + t.Fatal("should not be equal") + } + t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) +} + +func TestDriver_SetOrientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SetOrientation(OrientationLandscapeLeft) + // err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SetRotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.SetRotation(Rotation{Z: 0}) + err = driver.SetRotation(Rotation{Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_NetworkConnection(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.NetworkConnection(NetworkTypeWifi) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/content"}) + if err != nil { + t.Fatal(err) + } + + t.Log(elem.GetAttribute("class")) +} + +func TestDriver_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // elements, err := driver.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + elements, err := driver.FindElements(AndroidBySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) + if err != nil { + t.Fatal(err) + } + + elem, err := element.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + x := rect.X + int(float64(rect.Width)*2) + y := rect.Y + rect.Height/2 + err = driver.Tap(x, y) + if err != nil { + t.Fatal(err) + } + + by := AndroidBySelector{UiAutomator: "new UiSelector().text(\"科技\");"} + exists := func(d *uiaDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + + err = driver.WaitWithTimeoutAndInterval(exists, 1, 0.1) + if err != nil { + t.Fatal(err) + } + + // element, err = driver.FindElement(by) + // if err != nil { + // t.Fatal(err) + // } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_ActiveElement(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = driver.Dispose() + }() + + element, err := driver.ActiveElement() + if err != nil { + t.Fatal(err) + } + + if err = element.SendKeys("test"); err != nil { + t.Fatal(err) + } +} + +func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { + uiSelector := NewUiSelectorHelper().Text("a").String() + if uiSelector != `new UiSelector().text("a");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Text("a").TextStartsWith("b").String() + if uiSelector != `new UiSelector().text("a").textStartsWith("b");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ClassName("android.widget.LinearLayout").Index(6).String() + if uiSelector != `new UiSelector().className("android.widget.LinearLayout").index(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Focused(false).Instance(6).String() + if uiSelector != `new UiSelector().focused(false).instance(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ChildSelector(NewUiSelectorHelper().Enabled(true)).String() + if uiSelector != `new UiSelector().childSelector(new UiSelector().enabled(true));` { + t.Fatal("[ERROR]", uiSelector) + } +} + +func Test_getFreePort(t *testing.T) { + freePort, err := getFreePort() + if err != nil { + t.Fatal(err) + } + t.Log(freePort) +} + +func TestDeviceList(t *testing.T) { + devices, err := DeviceList() + if err != nil { + t.Fatal(err) + } + for i := range devices { + t.Log(devices[i].Serial()) + } +} + +func TestAndroidNewUSBDriver(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + ready, err := driver.Status() + if err != nil { + t.Fatal(err) + } + if !ready { + t.Fatal("should be 'true'") + } +} + +func TestDriver_ActiveAppPackageName(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + appPackageName, err := driver.ActiveAppPackageName() + if err != nil { + t.Fatal(err) + } + + t.Log(appPackageName) +} + +func TestDriver_AppLaunch(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + // err = driver.AppLaunch("tv.danmaku.bili", AndroidBySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) + err = driver.AppLaunch("com.android.settings", AndroidBySelector{ResourceIdID: "android:id/list"}) + if err != nil { + t.Fatal(err) + } + + // screenshot, err := driver.Screenshot() + // if err != nil { + // t.Fatal(err) + // } + // t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0600)) +} + +func TestDriver_AppTerminate(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppTerminate("tv.danmaku.bili") + if err != nil { + t.Fatal(err) + } +} + +func TestNewWiFiDriver(t *testing.T) { + device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) + driver, err := device.NewHTTPDriver(nil) + if err != nil { + t.Fatal(err) + } + + // SetDebug(false, true) + _, err = driver.ActiveAppActivity() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AppInstall(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AppUninstall(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppUninstall("cn.xuexi.android") + if err != nil { + t.Fatal(err) + } +} + +func TestBySelector_getMethodAndSelector(t *testing.T) { + testVal := "test id" + bySelector := AndroidBySelector{ResourceIdID: testVal} + method, selector := bySelector.getMethodAndSelector() + if method != "id" || selector != testVal { + t.Fatal(method, "=", selector) + } + + bySelector = AndroidBySelector{ContentDescription: testVal} + method, selector = bySelector.getMethodAndSelector() + if method != "accessibility id" || selector != testVal { + t.Fatal(method, "=", selector) + } +} + +func TestElement_Text(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + text, err := elem.Text() + if err != nil { + t.Fatal(err) + } + + t.Log(text) +} + +func TestElement_GetAttribute(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + attribute, err := elem.GetAttribute("class") + if err != nil { + t.Fatal(err) + } + + t.Log(attribute) +} + +func TestElement_ContentDescription(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + + name, err := elem.ContentDescription() + if err != nil { + t.Fatal(err) + } + + t.Log(name) +} + +func TestElement_Size(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + + size, err := elem.Size() + if err != nil { + t.Fatal(err) + } + + t.Log(size) +} + +func TestElement_Rect(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) +} + +func TestElement_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + screenshot, err := elem.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/e1.png", screenshot.Bytes(), 0o600)) +} + +func TestElement_Location(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + location, err := elem.Location() + if err != nil { + t.Fatal(err) + } + + t.Log(location) +} + +func TestElement_Click(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Click() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_Clear(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Clear() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + // return + + // err = elem.SendKeys("abc") + err = elem.SendKeys("456", false) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elements, err := parentElem.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/category"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestElement_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elem, err := parentElem.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + t.Log(elem.Text()) +} + +func TestElement_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) + + var startX, startY, endX, endY int + startX = rect.X + rect.Width/20 + startY = rect.Y + rect.Height/2 + endX = startX + endY = startY - startY/2 + err = elem.Swipe(startX, startY, endX, endY) + if err != nil { + t.Fatal(err) + } + + startPoint := PointF{X: float64(rect.X + rect.Width/20 + 30), Y: float64(startY / 2)} + endPoint := PointF{X: startPoint.X, Y: startPoint.Y + startPoint.Y} + err = elem.SwipePointF(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_Drag(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elements, err := driver.FindElements(AndroidBySelector{ClassName: "android.widget.TextView"}) + if err != nil { + t.Fatal(err) + } + + for i, elem := range elements { + text, _ := elem.Text() + t.Log(i, text) + } + + rect, err := elements[0].Rect() + if err != nil { + t.Fatal(err) + } + + // err = elements[0].Drag(300, 450, 256) + err = elements[0].Drag(300, 450, 256) + if err != nil { + t.Fatal(err) + } + + err = elements[0].DragTo(elements[1], 256) + if err != nil { + t.Fatal(err) + } + + endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} + err = elements[0].DragPointF(endPoint, 256) + if err != nil { + t.Fatal() + } +} + +func TestElement_Flick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Flick(36, 20, 100) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_ScrollTo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // how to make it work? + // parentElem, err := driver.FindElement(AndroidBySelector{ClassName: "android.widget.ScrollView"}) + // parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/dashboard"}) + if err != nil { + t.Fatal(err) + } + + err = parentElem.ScrollTo(AndroidBySelector{ContentDescription: "电池"}) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_ScrollToElement(t *testing.T) { + // android.widget.HorizontalScrollView + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // how to make it work? + parentElem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) + if err != nil { + t.Fatal(err) + } + + element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) + if err != nil { + t.Fatal(err) + } + + err = parentElem.ScrollToElement(element) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/internal/uixt/android_webdriver.go b/hrp/internal/uixt/android_webdriver.go deleted file mode 100644 index 91dd4070..00000000 --- a/hrp/internal/uixt/android_webdriver.go +++ /dev/null @@ -1,3 +0,0 @@ -package uixt - -type uiaWebDriver struct{} diff --git a/hrp/internal/uixt/android_webelment.go b/hrp/internal/uixt/android_webelment.go deleted file mode 100644 index 64c90179..00000000 --- a/hrp/internal/uixt/android_webelment.go +++ /dev/null @@ -1 +0,0 @@ -package uixt diff --git a/hrp/internal/uixt/client.go b/hrp/internal/uixt/client.go new file mode 100644 index 00000000..befa38d6 --- /dev/null +++ b/hrp/internal/uixt/client.go @@ -0,0 +1,104 @@ +package uixt + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +type Driver struct { + urlPrefix *url.URL + sessionId string + client *http.Client +} + +func (wd *Driver) concatURL(u *url.URL, elem ...string) string { + var tmp *url.URL + if u == nil { + u = wd.urlPrefix + } + tmp, _ = url.Parse(u.String()) + tmp.Path = path.Join(append([]string{u.Path}, elem...)...) + return tmp.String() +} + +func (wd *Driver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { + var bsJSON []byte = nil + if data != nil { + if bsJSON, err = json.Marshal(data); err != nil { + return nil, err + } + } + return wd.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) +} + +func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { + log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") + + var req *http.Request + if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + req.Header.Set("Accept", "application/json") + + start := time.Now() + var resp *http.Response + if resp, err = wd.client.Do(req); err != nil { + return nil, err + } + defer func() { + // https://github.com/etcd-io/etcd/blob/v3.3.25/pkg/httputil/httputil.go#L16-L22 + _, _ = io.Copy(ioutil.Discard, resp.Body) + _ = resp.Body.Close() + }() + + rawResp, err = ioutil.ReadAll(resp.Body) + logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String()) + if !strings.HasSuffix(rawURL, "screenshot") { + // avoid printing screenshot data + logger.Str("response", string(rawResp)) + } + logger.Msg("get WDA response") + if err != nil { + return nil, err + } + + if err = rawResp.checkErr(); err != nil { + if resp.StatusCode == http.StatusOK { + return rawResp, nil + } + return nil, err + } + + return +} + +func convertToHTTPClient(conn net.Conn) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return conn, nil + }, + }, + Timeout: 0, + } +} diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index ce3b6b21..7642425c 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -4,6 +4,7 @@ package uixt import ( "image" + "sort" ) func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) { diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index cbe7a0e1..0101d2d9 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -164,6 +164,8 @@ type BatteryInfo struct { // Battery state ( 1: on battery, discharging; 2: plugged in, less than 100%, 3: plugged in, at 100% ) State BatteryState `json:"state"` + + Status BatteryStatus `json:"status"` } type BatteryState int @@ -683,6 +685,11 @@ type Point struct { Y int `json:"y"` // upper left Y coordinate of selected element } +type PointF struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + type Rect struct { Point Size @@ -708,6 +715,11 @@ func WithFrequency(frequency int) DataOption { } } +// current implemeted device: IOSDevice, AndroidDevice +type Device interface { + UUID() string +} + // WebDriver defines methods supported by WebDriver drivers. type WebDriver interface { // NewSession starts a new session and returns the SessionInfo. diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index eeee5e5f..9a243ce3 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -2,7 +2,6 @@ package uixt import ( "bytes" - "context" "encoding/base64" builtinJSON "encoding/json" "fmt" @@ -14,7 +13,6 @@ import ( "net/url" "regexp" "strings" - "sync" "time" giDevice "github.com/electricbubble/gidevice" @@ -40,7 +38,7 @@ const ( ) const ( - defaultPort = 8100 + defaultWDAPort = 8100 defaultMjpegPort = 9100 ) @@ -102,10 +100,6 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { return driverExt, nil } -type Device interface { - UUID() string -} - type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -144,7 +138,7 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } device = &IOSDevice{ - Port: defaultPort, + Port: defaultWDAPort, MjpegPort: defaultMjpegPort, } for _, option := range options { @@ -191,37 +185,33 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver return nil, err } wd.sessionId = sessionInfo.SessionId + wd.client = http.DefaultClient - if wd.mjpegConn, err = net.Dial( + if wd.mjpegHTTPConn, err = net.Dial( "tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), dev.MjpegPort), ); err != nil { return nil, err } - wd.mjpegClient = convertToHTTPClient(wd.mjpegConn) + wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn) return wd, nil } // NewUSBDriver creates new client via USB connected device, this will also start a new session. func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { - wd := &wdaDriver{ - usbCli: &struct { - httpCli *http.Client - defaultConn, mjpegConn giDevice.InnerConn - sync.Mutex - }{}, - } - if wd.usbCli.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { + wd := new(wdaDriver) + + if wd.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { return nil, fmt.Errorf("create connection: %w", err) } - wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn()) + wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) - if wd.usbCli.mjpegConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { + if wd.mjpegUSBConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { return nil, fmt.Errorf("create connection MJPEG: %w", err) } - wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn()) + wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil { return nil, err @@ -245,17 +235,6 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, return wd, err } -func convertToHTTPClient(_conn net.Conn) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return _conn, nil - }, - }, - Timeout: 0, - } -} - type wdaResponse struct { Value string `json:"value"` SessionID string `json:"sessionId"` @@ -374,9 +353,10 @@ type rawResponse []byte func (r rawResponse) checkErr() (err error) { reply := new(struct { Value struct { - Err string `json:"error"` - Message string `json:"message"` - Traceback string `json:"traceback"` + Err string `json:"error"` + Message string `json:"message"` + Traceback string `json:"traceback"` // wda + Stacktrace string `json:"stacktrace"` // uia } }) if err = json.Unmarshal(r, reply); err != nil { diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 6464fd2d..e99e33bc 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -5,57 +5,46 @@ import ( "encoding/base64" builtinJSON "encoding/json" "fmt" - "io" - "io/ioutil" "net" "net/http" "net/url" - "path" "strings" - "sync" "time" giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" - "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) type wdaDriver struct { - urlPrefix *url.URL - sessionId string + Driver - usbCli *struct { - httpCli *http.Client - defaultConn, mjpegConn giDevice.InnerConn - sync.Mutex - } + // default port + defaultConn giDevice.InnerConn - mjpegClient *http.Client - mjpegConn net.Conn + // mjpeg port + mjpegUSBConn giDevice.InnerConn // via USB + mjpegHTTPConn net.Conn // via HTTP + mjpegClient *http.Client } -func (wd *wdaDriver) GetMjpegHTTPClient() *http.Client { +func (wd *wdaDriver) GetMjpegClient() *http.Client { return wd.mjpegClient } func (wd *wdaDriver) Close() error { - if wd.usbCli == nil { + if wd.defaultConn != nil { + wd.defaultConn.Close() + } + if wd.mjpegUSBConn != nil { + wd.mjpegUSBConn.Close() + } + + if wd.mjpegClient != nil { wd.mjpegClient.CloseIdleConnections() - return wd.mjpegConn.Close() } - - wd.usbCli.Lock() - defer wd.usbCli.Unlock() - - if wd.usbCli.defaultConn != nil { - wd.usbCli.defaultConn.Close() - } - if wd.usbCli.mjpegConn != nil { - wd.usbCli.mjpegConn.Close() - } - return nil + return wd.mjpegHTTPConn.Close() } func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { @@ -844,83 +833,3 @@ func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) func (wd *wdaDriver) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } - -func (wd *wdaDriver) concatURL(u *url.URL, elem ...string) string { - var tmp *url.URL - if u == nil { - u = wd.urlPrefix - } - tmp, _ = url.Parse(u.String()) - tmp.Path = path.Join(append([]string{u.Path}, elem...)...) - return tmp.String() -} - -func (wd *wdaDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { - return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) -} - -func (wd *wdaDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { - var bsJSON []byte = nil - if data != nil { - if bsJSON, err = json.Marshal(data); err != nil { - return nil, err - } - } - return wd.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) -} - -func (wd *wdaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { - return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) -} - -func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { - log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") - - var req *http.Request - if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json;charset=UTF-8") - req.Header.Set("Accept", "application/json") - - var httpCli *http.Client - if wd.usbCli != nil { - wd.usbCli.Lock() - defer wd.usbCli.Unlock() - httpCli = wd.usbCli.httpCli - } else { - httpCli = http.DefaultClient - } - httpCli.Timeout = 0 - - start := time.Now() - var resp *http.Response - if resp, err = httpCli.Do(req); err != nil { - return nil, err - } - defer func() { - // https://github.com/etcd-io/etcd/blob/v3.3.25/pkg/httputil/httputil.go#L16-L22 - _, _ = io.Copy(ioutil.Discard, resp.Body) - _ = resp.Body.Close() - }() - - rawResp, err = ioutil.ReadAll(resp.Body) - logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String()) - if !strings.HasSuffix(rawURL, "screenshot") { - // avoid printing screenshot data - logger.Bytes("response", rawResp) - } - logger.Msg("get WDA response") - if err != nil { - return nil, err - } - - if err = rawResp.checkErr(); err != nil { - if resp.StatusCode == http.StatusOK { - return rawResp, nil - } - return nil, err - } - - return -} diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go index 9f6f8236..62c52161 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -351,8 +351,6 @@ func Test_remoteWD_Homescreen(t *testing.T) { func Test_remoteWD_AppLaunch(t *testing.T) { setup(t) - // SetDebug(true) - // bundleId = "com.hustlzp.xcz" // bundleId = "com.github.stormbreaker.prod" // bundleId = "com.360buy.jdmobile" diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 6fc499e1..bde3c861 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -10,8 +10,6 @@ func TestDriverExt_TapWithNumber(t *testing.T) { pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" - // SetDebug(true) - err = driverExt.TapWithNumber(pathSearch, 3) checkErr(t, err) From d4c0fc4cbdc80c71f7ab76714655161f058e0ba6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 26 Sep 2022 14:47:19 +0800 Subject: [PATCH 064/169] fix: screenshot --- hrp/internal/uixt/android_device.go | 12 ++++++------ hrp/internal/uixt/ext.go | 10 +++++++--- hrp/internal/uixt/ios_device.go | 24 ++++++++++++------------ hrp/internal/uixt/ios_driver.go | 4 ++-- hrp/internal/uixt/ocr_on.go | 11 +++++++---- hrp/internal/uixt/ocr_test.go | 2 +- hrp/internal/uixt/swipe.go | 3 --- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index ae394be4..f00880ad 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -101,7 +101,7 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er } device.SerialNumber = dev.Serial() - device.Device = dev + device.d = dev return device, nil } @@ -109,7 +109,7 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er } type AndroidDevice struct { - gadb.Device + d gadb.Device SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` IP string `json:"ip,omitempty" yaml:"ip,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` @@ -136,17 +136,17 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDr if localPort, err = getFreePort(); err != nil { return nil, err } - if err = dev.Forward(localPort, UIA2ServerPort); err != nil { + if err = dev.d.Forward(localPort, UIA2ServerPort); err != nil { return nil, err } rawURL := fmt.Sprintf("http://%s%d:6790/wd/hub", forwardToPrefix, localPort) driver, err = NewUIADriver(capabilities, rawURL) if err != nil { - _ = dev.ForwardKill(localPort) + _ = dev.d.ForwardKill(localPort) return nil, err } - driver.adbDevice = dev.Device + driver.adbDevice = dev.d driver.localPort = localPort conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) @@ -165,7 +165,7 @@ func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver *uiaD if driver, err = NewUIADriver(capabilities, rawURL); err != nil { return nil, err } - driver.adbDevice = dev.Device + driver.adbDevice = dev.d return driver, nil } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 5eb7d0d1..1b44922b 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -152,10 +152,10 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { return dExt.frame, nil } if raw, err = dExt.Driver.Screenshot(); err != nil { - log.Error().Err(err).Msgf("screenshot failed: %v", err) + log.Error().Err(err).Msg("takeScreenShot failed") return nil, err } - return + return raw, nil } // saveScreenShot saves image file to $CWD/screenshots/ folder @@ -203,7 +203,11 @@ func (dExt *DriverExt) ScreenShot(fileName string) (string, error) { return "", errors.Wrap(err, "screenshot by WDA failed") } - return dExt.saveScreenShot(raw, fileName) + path, err := dExt.saveScreenShot(raw, fileName) + if err != nil { + return "", errors.Wrap(err, "save screenshot failed") + } + return path, nil } // isPathExists returns true if path exists, whether path is file or dir diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 9a243ce3..d71abd5a 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -64,7 +64,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { // aviod getting stuck when some super app is activate such as douyin or wexin log.Info().Msg("switch to iOS springboard") bundleID := "com.apple.springboard" - _, err = iosDevice.AppLaunch(bundleID) + _, err = iosDevice.d.AppLaunch(bundleID) if err != nil { return nil, errors.Wrap(err, "launch springboard failed") } @@ -153,7 +153,7 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } device.UDID = dev.Properties().SerialNumber - device.Device = dev + device.d = dev return device, nil } @@ -161,7 +161,7 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } type IOSDevice struct { - giDevice.Device + d giDevice.Device UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` @@ -203,12 +203,12 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { wd := new(wdaDriver) - if wd.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { + if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { return nil, fmt.Errorf("create connection: %w", err) } wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) - if wd.mjpegUSBConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { + if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { return nil, fmt.Errorf("create connection MJPEG: %w", err) } wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) @@ -377,7 +377,7 @@ func (r rawResponse) checkErr() (err error) { func (r rawResponse) valueConvertToString() (s string, err error) { reply := new(struct{ Value string }) if err = json.Unmarshal(r, reply); err != nil { - return "", err + return "", errors.Wrapf(err, "json.Unmarshal failed, rawResponse: %s", string(r)) } s = reply.Value return @@ -411,13 +411,13 @@ func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, } func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) { - var str string - if str, err = r.valueConvertToString(); err != nil { - return nil, err + str, err := r.valueConvertToString() + if err != nil { + return nil, errors.Wrap(err, "failed to convert value to string") } - var decodeString []byte - if decodeString, err = base64.StdEncoding.DecodeString(str); err != nil { - return nil, err + decodeString, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, errors.Wrap(err, "failed to decode base64 string") } raw = bytes.NewBuffer(decodeString) return diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index e99e33bc..9e5eda75 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -699,11 +699,11 @@ func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) { // [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionId, "/screenshot"); err != nil { - return nil, err + return nil, errors.Wrap(err, "get WDA screenshot data failed") } if raw, err = rawResp.valueDecodeAsBase64(); err != nil { - return nil, err + return nil, errors.Wrap(err, "decode WDA screenshot data failed") } return } diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 5809020b..282780de 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -13,6 +13,8 @@ import ( "strings" "time" + "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -21,8 +23,8 @@ var client = &http.Client{ } type OCRResult struct { - Text string `json:"text"` - Points []Point `json:"points"` + Text string `json:"text"` + Points []PointF `json:"points"` } type ResponseOCR struct { @@ -134,14 +136,15 @@ type OCRService interface { func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { - err = fmt.Errorf("screenshot error: %v", err) + err = fmt.Errorf("takeScreenShot error: %v", err) return } service := &veDEMOCRService{} rect, err := service.FindText(ocrText, bufSource.Bytes()) if err != nil { - err = fmt.Errorf("find text failed: %v", err) + log.Warn().Err(err).Msg("FindText failed") + err = fmt.Errorf("FindText failed: %v", err) return } diff --git a/hrp/internal/uixt/ocr_test.go b/hrp/internal/uixt/ocr_test.go index 922c9e0b..59b19160 100644 --- a/hrp/internal/uixt/ocr_test.go +++ b/hrp/internal/uixt/ocr_test.go @@ -7,7 +7,7 @@ import ( ) func TestDriverExtOCR(t *testing.T) { - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) x, y, width, height, err := driverExt.FindTextByOCR("抖音") diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 7973b53a..c329263d 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -2,7 +2,6 @@ package uixt import ( "fmt" - "time" "github.com/rs/zerolog/log" ) @@ -82,8 +81,6 @@ func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, act if err := dExt.SwipeTo(direction); err != nil { log.Error().Err(err).Msgf("swipe %s failed", direction) } - // wait for swipe done - time.Sleep(500 * time.Millisecond) } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } From 0fd271f19e2f4a4dea3d0439c89731d87f329aea Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 26 Sep 2022 17:41:09 +0800 Subject: [PATCH 065/169] fix: screenshot wait for swipe done --- hrp/internal/uixt/ios_device.go | 6 ++++-- hrp/internal/uixt/ocr_on.go | 9 +++++---- hrp/internal/uixt/swipe.go | 3 +++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index d71abd5a..7218e368 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -204,12 +204,14 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, wd := new(wdaDriver) if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { - return nil, fmt.Errorf("create connection: %w", err) + return nil, fmt.Errorf("connect port %d failed: %w", + dev.Port, err) } wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { - return nil, fmt.Errorf("create connection MJPEG: %w", err) + return nil, fmt.Errorf("connect MJPEG port %d failed: %w", + dev.MjpegPort, err) } wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 282780de..6583c3f5 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -68,15 +68,15 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected response status code: %d", resp.StatusCode) - } - results, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response body error: %v", err) } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response status code: %d, results: %v", resp.StatusCode, string(results)) + } + var ocrResult ResponseOCR err = json.Unmarshal(results, &ocrResult) if err != nil { @@ -148,6 +148,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float6 return } + log.Info().Str("ocrText", ocrText).Msgf("FindText success") x, y, width, height = dExt.MappingToRectInUIKit(rect) return } diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index c329263d..7973b53a 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -2,6 +2,7 @@ package uixt import ( "fmt" + "time" "github.com/rs/zerolog/log" ) @@ -81,6 +82,8 @@ func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, act if err := dExt.SwipeTo(direction); err != nil { log.Error().Err(err).Msgf("swipe %s failed", direction) } + // wait for swipe done + time.Sleep(500 * time.Millisecond) } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } From 498cc9432aa3e3d19929379f724bf721e2e559b6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 26 Sep 2022 22:25:13 +0800 Subject: [PATCH 066/169] feat: locate ocr text with index --- examples/uitest/demo_douyin_live.json | 89 +++++++++++++++++++++++++++ examples/uitest/demo_douyin_live.yaml | 45 ++++++++++++++ examples/uitest/demo_douyin_test.go | 52 ++++++++++++++++ examples/uitest/demo_weixin_live.json | 3 +- examples/uitest/demo_weixin_live.yaml | 1 + examples/uitest/demo_weixin_test.go | 4 +- hrp/internal/uixt/ext.go | 21 ++++--- hrp/internal/uixt/gesture_test.go | 3 +- hrp/internal/uixt/ocr_off.go | 2 +- hrp/internal/uixt/ocr_on.go | 42 +++++++++---- hrp/internal/uixt/opencv_off.go | 2 +- hrp/internal/uixt/opencv_on.go | 2 +- hrp/internal/uixt/tap.go | 14 ++--- hrp/step.go | 1 + 14 files changed, 249 insertions(+), 32 deletions(-) create mode 100644 examples/uitest/demo_douyin_live.json create mode 100644 examples/uitest/demo_douyin_live.yaml create mode 100644 examples/uitest/demo_douyin_test.go diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json new file mode 100644 index 00000000..77cd4d9d --- /dev/null +++ b/examples/uitest/demo_douyin_live.json @@ -0,0 +1,89 @@ +{ + "config": { + "name": "通过 feed 卡片进入微信直播间", + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "在推荐页上划,直到出现「点击进入直播间」", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": "点击进入直播间", + "identifier": "进入直播间", + "max_retry_times": 100 + } + ] + } + }, + { + "name": "向上滑动,等待 10s", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第一次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": "up", + "identifier": "第二次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "screenshot" + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml new file mode 100644 index 00000000..f2c29f62 --- /dev/null +++ b/examples/uitest/demo_douyin_live.yaml @@ -0,0 +1,45 @@ +config: + name: 通过 feed 卡片进入微信直播间 + ios: + - port: 8700 + mjpeg_port: 8800 + log_on: true +teststeps: + - name: 启动抖音 + ios: + actions: + - method: home + - method: app_terminate + params: com.ss.iphone.ugc.Aweme + - method: swipe_to_tap_app + params: 抖音 + max_retry_times: 5 + - method: sleep + params: 5 + validate: + - check: ui_ocr + assert: exists + expect: 推荐 + msg: 抖音启动失败,「推荐」不存在 + - name: 在推荐页上划,直到出现「点击进入直播间」 + ios: + actions: + - method: swipe_to_tap_text + params: 点击进入直播间 + identifier: 进入直播间 + max_retry_times: 100 + - name: 向上滑动,等待 10s + ios: + actions: + - method: swipe + params: up + identifier: 第一次上划 + - method: sleep + params: 2 + - method: screenshot + - method: swipe + params: up + identifier: 第二次上划 + - method: sleep + params: 2 + - method: screenshot diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go new file mode 100644 index 00000000..2880b4de --- /dev/null +++ b/examples/uitest/demo_douyin_test.go @@ -0,0 +1,52 @@ +package uitest + +import ( + "fmt" + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouyinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). + SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + // hrp.NewStep("处理青少年弹窗"). + // IOS(). + // TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」"). + IOS(). + SwipeToTapText("点击进入直播间", hrp.WithMaxRetryTimes(100), hrp.WithIdentifier("进入直播间")), + hrp.NewStep("向上滑动,等待 10s"). + IOS(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(2).ScreenShot(). // 上划 1 次,等待 2s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(2).ScreenShot(), // 再上划 1 次,等待 2s,截图保存 + }, + } + + if err := testCase.Dump2JSON("demo_douyin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_douyin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t) + sessionRunner, err := runner.NewSessionRunner(testCase) + if err != nil { + t.Fatal(err) + } + if err := sessionRunner.Start(nil); err != nil { + t.Fatal(err) + } + summary := sessionRunner.GetSummary() + fmt.Println(summary) +} diff --git a/examples/uitest/demo_weixin_live.json b/examples/uitest/demo_weixin_live.json index f5f3f23a..618ec703 100644 --- a/examples/uitest/demo_weixin_live.json +++ b/examples/uitest/demo_weixin_live.json @@ -48,7 +48,8 @@ { "method": "tap_ocr", "params": "视频号", - "identifier": "进入视频号" + "identifier": "进入视频号", + "index": -1 } ] } diff --git a/examples/uitest/demo_weixin_live.yaml b/examples/uitest/demo_weixin_live.yaml index 44b19b50..3b064f09 100644 --- a/examples/uitest/demo_weixin_live.yaml +++ b/examples/uitest/demo_weixin_live.yaml @@ -27,6 +27,7 @@ teststeps: - method: tap_ocr params: 视频号 identifier: 进入视频号 + index: -1 - name: 处理青少年弹窗 ios: actions: diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index c692ddf1..f157e27c 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -21,8 +21,8 @@ func TestIOSWeixinLive(t *testing.T) { AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), hrp.NewStep("进入直播页"). IOS(). - Tap("发现"). // 进入「发现页」 - TapByOCR("视频号", hrp.WithIdentifier("进入视频号")), // 通过 OCR 识别「视频号」 + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号", hrp.WithIdentifier("进入视频号"), hrp.WithIndex(-1)), // 通过 OCR 识别「视频号」 hrp.NewStep("处理青少年弹窗"). IOS(). TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 1b44922b..a89385a2 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -63,6 +63,7 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found } @@ -75,6 +76,12 @@ func WithIdentifier(identifier string) ActionOption { } } +func WithIndex(index int) ActionOption { + return func(o *MobileAction) { + o.Index = index + } +} + func WithMaxRetryTimes(maxRetryTimes int) ActionOption { return func(o *MobileAction) { o.MaxRetryTimes = maxRetryTimes @@ -235,13 +242,13 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { return dExt.Driver.FindElement(selector) } -func (dExt *DriverExt) FindUIRectInUIKit(search string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindUIRectInUIKit(search string, index ...int) (x, y, width, height float64, err error) { // click on text, using OCR if !isPathExists(search) { - return dExt.FindTextByOCR(search) + return dExt.FindTextByOCR(search, index...) } // click on image, using opencv - return dExt.FindImageRectInUIKit(search) + return dExt.FindImageRectInUIKit(search, index...) } func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { @@ -310,7 +317,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var x, y, width, height float64 findApp := func(d *DriverExt) error { var err error - x, y, width, height, err = d.FindTextByOCR(appName) + x, y, width, height, err = d.FindTextByOCR(appName, action.Index) return err } foundAppAction := func(d *DriverExt) error { @@ -384,17 +391,17 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) case ACTION_Tap: if param, ok := action.Params.(string); ok { - return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError) + return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError, action.Index) } return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) case ACTION_TapByOCR: if ocrText, ok := action.Params.(string); ok { - return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError) + return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, action.Index) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) case ACTION_TapByCV: if imagePath, ok := action.Params.(string); ok { - return dExt.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError) + return dExt.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError, action.Index) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params) case ACTION_DoubleTapXY: diff --git a/hrp/internal/uixt/gesture_test.go b/hrp/internal/uixt/gesture_test.go index 5e4ce57b..31664954 100644 --- a/hrp/internal/uixt/gesture_test.go +++ b/hrp/internal/uixt/gesture_test.go @@ -3,6 +3,7 @@ package uixt import ( + "strconv" "strings" "testing" ) @@ -14,7 +15,7 @@ func TestDriverExt_GesturePassword(t *testing.T) { password[i], _ = strconv.Atoi(split[i]) } - driverExt, err := InitWDAClient() + driverExt, err := InitWDAClient(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" diff --git a/hrp/internal/uixt/ocr_off.go b/hrp/internal/uixt/ocr_off.go index 7c3536b5..669dbe46 100644 --- a/hrp/internal/uixt/ocr_off.go +++ b/hrp/internal/uixt/ocr_off.go @@ -4,7 +4,7 @@ package uixt import "github.com/rs/zerolog/log" -func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) { log.Fatal().Msg("OCR is not supported") return } diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 6583c3f5..327bc373 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -86,7 +86,11 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return ocrResult.OCRResult, nil } -func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rectangle, err error) { +func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) { + if len(index) == 0 { + index = []int{0} // index not specified + } + ocrResults, err := s.getOCRResult(imageBuf) if err != nil { return @@ -110,30 +114,46 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte) (rect image.Rec Y: int(ocrResult.Points[2].Y), }, } + rects = append(rects, rect) // contains text while not match exactly if ocrResult.Text != text { - rects = append(rects, rect) continue } - // match exactly - return rect, nil + // match exactly, and not specify index, return the first one + if index[0] == 0 { + return rect, nil + } } - // only find the first matched one - if len(rects) > 0 { - return rects[0], nil + if len(rects) == 0 { + return image.Rectangle{}, fmt.Errorf("text %s not found", text) } - return image.Rectangle{}, fmt.Errorf("text %s not found", text) + // get index + idx := index[0] + if idx > 0 { + // NOTICE: index start from 1 + idx = idx - 1 + } else if idx < 0 { + idx = len(rects) + idx + } + + // index out of range + if idx >= len(rects) { + return image.Rectangle{}, fmt.Errorf("text %s found %d, index %d out of range", + text, len(rects), idx) + } + + return rects[idx], nil } type OCRService interface { - FindText(text string, imageBuf []byte) (rect image.Rectangle, err error) + FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) } -func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) @@ -141,7 +161,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string) (x, y, width, height float6 } service := &veDEMOCRService{} - rect, err := service.FindText(ocrText, bufSource.Bytes()) + rect, err := service.FindText(ocrText, bufSource.Bytes(), index...) if err != nil { log.Warn().Err(err).Msg("FindText failed") err = fmt.Errorf("FindText failed: %v", err) diff --git a/hrp/internal/uixt/opencv_off.go b/hrp/internal/uixt/opencv_off.go index 4fc747c1..db1865ae 100644 --- a/hrp/internal/uixt/opencv_off.go +++ b/hrp/internal/uixt/opencv_off.go @@ -17,7 +17,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, return } -func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) { log.Fatal().Msg("opencv is not supported") return } diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 5fdc7197..d4acda45 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -111,7 +111,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, return } -func (dExt *DriverExt) FindImageRectInUIKit(imagePath string) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) { var bufSource, bufSearch *bytes.Buffer if bufSearch, err = getBufFromDisk(imagePath); err != nil { return 0, 0, 0, 0, err diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index 1d4eb168..4acff134 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -27,8 +27,8 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { return dExt.tapFloat(x, y, identifier) } -func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool) error { - x, y, width, height, err := dExt.FindTextByOCR(ocrText) +func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error { + x, y, width, height, err := dExt.FindTextByOCR(ocrText, index...) if err != nil { if ignoreNotFoundError { return nil @@ -39,7 +39,7 @@ func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoun return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) } -func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool) error { +func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool, index ...int) error { x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath) if err != nil { if ignoreNotFoundError { @@ -51,18 +51,18 @@ func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFou return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) } -func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool) error { - return dExt.TapOffset(param, 0.5, 0.5, identifier, ignoreNotFoundError) +func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool, index ...int) error { + return dExt.TapOffset(param, 0.5, 0.5, identifier, ignoreNotFoundError, index...) } -func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identifier string, ignoreNotFoundError bool) (err error) { +func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identifier string, ignoreNotFoundError bool, index ...int) (err error) { // click on element, find by name attribute ele, err := dExt.FindUIElement(param) if err == nil { return ele.Click() } - x, y, width, height, err := dExt.FindUIRectInUIKit(param) + x, y, width, height, err := dExt.FindUIRectInUIKit(param, index...) if err != nil { if ignoreNotFoundError { return nil diff --git a/hrp/step.go b/hrp/step.go index f3585e40..13b6da61 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -19,6 +19,7 @@ const ( var ( WithIdentifier = uixt.WithIdentifier WithMaxRetryTimes = uixt.WithMaxRetryTimes + WithIndex = uixt.WithIndex WithTimeout = uixt.WithTimeout WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError ) From 6e405e515f99f9e417c28cdb7236f1e89f72f4c8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 15:00:59 +0800 Subject: [PATCH 067/169] fix: swipe to tap with identifier --- hrp/internal/uixt/ext.go | 12 ++++++------ hrp/internal/uixt/swipe_test.go | 10 +++++----- hrp/internal/uixt/tap.go | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index a89385a2..edcabd51 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -314,15 +314,15 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { AppLaunchUnattached, action.Params) case ACTION_SwipeToTapApp: if appName, ok := action.Params.(string); ok { - var x, y, width, height float64 + var point PointF findApp := func(d *DriverExt) error { var err error - x, y, width, height, err = d.FindTextByOCR(appName, action.Index) + point, err = d.GetTextCoordinate(appName, action.Index) return err } foundAppAction := func(d *DriverExt) error { // click app to launch - return d.Driver.TapFloat(x+width*0.5, y+height*0.5-20) + return d.tapFloat(point.X, point.Y-20, action.Identifier) } // go to home screen @@ -346,15 +346,15 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: if text, ok := action.Params.(string); ok { - var x, y, width, height float64 + var point PointF findText := func(d *DriverExt) error { var err error - x, y, width, height, err = d.FindTextByOCR(text) + point, err = d.GetTextCoordinate(text, action.Index) return err } foundTextAction := func(d *DriverExt) error { // tap text - return d.Driver.TapFloat(x+width*0.5, y+height*0.5) + return d.tapFloat(point.X, point.Y, action.Identifier) } // default to retry 10 times diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index cb25b76c..e52610c0 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -8,15 +8,15 @@ func TestSwipeUntil(t *testing.T) { driverExt, err := InitWDAClient(nil) checkErr(t, err) - var x, y, width, height float64 + var point PointF findApp := func(d *DriverExt) error { var err error - x, y, width, height, err = d.FindTextByOCR("抖音") + point, err = d.GetTextCoordinate("抖音") return err } foundAppAction := func(d *DriverExt) error { // click app, launch douyin - return d.Driver.TapFloat(x+width*0.5, y+height*0.5-20) + return d.tapFloat(point.X, point.Y, "") } driverExt.Driver.Homescreen() @@ -32,12 +32,12 @@ func TestSwipeUntil(t *testing.T) { findLive := func(d *DriverExt) error { var err error - x, y, width, height, err = d.FindTextByOCR("点击进入直播间") + point, err = d.GetTextCoordinate("点击进入直播间") return err } foundLiveAction := func(d *DriverExt) error { // enter live room - return d.Driver.TapFloat(x+width*0.5, y+height*0.5) + return d.tapFloat(point.X, point.Y, "") } // swipe until live room found diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index 4acff134..a4105418 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -27,8 +27,21 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { return dExt.tapFloat(x, y, identifier) } -func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error { +func (dExt *DriverExt) GetTextCoordinate(ocrText string, index ...int) (point PointF, err error) { x, y, width, height, err := dExt.FindTextByOCR(ocrText, index...) + if err != nil { + return PointF{}, err + } + + point = PointF{ + X: x + width*0.5, + Y: y + height*0.5, + } + return point, nil +} + +func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error { + point, err := dExt.GetTextCoordinate(ocrText, index...) if err != nil { if ignoreNotFoundError { return nil @@ -36,7 +49,7 @@ func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoun return err } - return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) + return dExt.tapFloat(point.X, point.Y, identifier) } func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool, index ...int) error { From 6608d1bdec8465c37ef871176c2a0999efaf8000 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 17:32:56 +0800 Subject: [PATCH 068/169] feat: add TapAbsXY --- hrp/internal/uixt/ext.go | 18 ++++++++++++++---- hrp/internal/uixt/swipe_test.go | 8 ++++---- hrp/internal/uixt/tap.go | 32 +++++++++++++++++++++++--------- hrp/internal/uixt/tap_test.go | 8 ++++++++ hrp/step_ios_ui.go | 13 +++++++++++++ 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index edcabd51..d0bd92df 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -44,6 +44,7 @@ const ( // UI handling ACTION_Home MobileMethod = "home" ACTION_TapXY MobileMethod = "tap_xy" + ACTION_TapAbsXY MobileMethod = "tap_abs_xy" ACTION_TapByOCR MobileMethod = "tap_ocr" ACTION_TapByCV MobileMethod = "tap_cv" ACTION_Tap MobileMethod = "tap" @@ -317,12 +318,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var point PointF findApp := func(d *DriverExt) error { var err error - point, err = d.GetTextCoordinate(appName, action.Index) + point, err = d.GetTextXY(appName, action.Index) return err } foundAppAction := func(d *DriverExt) error { // click app to launch - return d.tapFloat(point.X, point.Y-20, action.Identifier) + return d.TapAbsXY(point.X, point.Y-20, action.Identifier) } // go to home screen @@ -349,12 +350,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var point PointF findText := func(d *DriverExt) error { var err error - point, err = d.GetTextCoordinate(text, action.Index) + point, err = d.GetTextXY(text, action.Index) return err } foundTextAction := func(d *DriverExt) error { // tap text - return d.tapFloat(point.X, point.Y, action.Identifier) + return d.TapAbsXY(point.X, point.Y, action.Identifier) } // default to retry 10 times @@ -389,6 +390,15 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return dExt.TapXY(location[0], location[1], action.Identifier) } return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) + case ACTION_TapAbsXY: + if location, ok := action.Params.([]float64); ok { + // absolute coordinates x,y of window size: [100, 300] + if len(location) != 2 { + return fmt.Errorf("invalid tap location params: %v", location) + } + return dExt.TapAbsXY(location[0], location[1], action.Identifier) + } + return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params) case ACTION_Tap: if param, ok := action.Params.(string); ok { return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError, action.Index) diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index e52610c0..5314a663 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -11,12 +11,12 @@ func TestSwipeUntil(t *testing.T) { var point PointF findApp := func(d *DriverExt) error { var err error - point, err = d.GetTextCoordinate("抖音") + point, err = d.GetTextXY("抖音") return err } foundAppAction := func(d *DriverExt) error { // click app, launch douyin - return d.tapFloat(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y, "") } driverExt.Driver.Homescreen() @@ -32,12 +32,12 @@ func TestSwipeUntil(t *testing.T) { findLive := func(d *DriverExt) error { var err error - point, err = d.GetTextCoordinate("点击进入直播间") + point, err = d.GetTextXY("点击进入直播间") return err } foundLiveAction := func(d *DriverExt) error { // enter live room - return d.tapFloat(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y, "") } // swipe until live room found diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index a4105418..ada1ae73 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -4,7 +4,8 @@ import ( "fmt" ) -func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { +func (dExt *DriverExt) TapAbsXY(x, y float64, identifier string) error { + // tap on absolute coordinate [x, y] if len(identifier) > 0 { option := WithCustomOption("log", map[string]interface{}{ "enable": true, @@ -16,7 +17,7 @@ func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error { } func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { - // tap on coordinate: [x, y] should be relative + // tap on [x, y] percent of window size if x > 1 || y > 1 { return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) } @@ -24,10 +25,10 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { x = x * float64(dExt.windowSize.Width) y = y * float64(dExt.windowSize.Height) - return dExt.tapFloat(x, y, identifier) + return dExt.TapAbsXY(x, y, identifier) } -func (dExt *DriverExt) GetTextCoordinate(ocrText string, index ...int) (point PointF, err error) { +func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, err error) { x, y, width, height, err := dExt.FindTextByOCR(ocrText, index...) if err != nil { return PointF{}, err @@ -40,8 +41,21 @@ func (dExt *DriverExt) GetTextCoordinate(ocrText string, index ...int) (point Po return point, nil } +func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, err error) { + x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, index...) + if err != nil { + return PointF{}, err + } + + point = PointF{ + X: x + width*0.5, + Y: y + height*0.5, + } + return point, nil +} + func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error { - point, err := dExt.GetTextCoordinate(ocrText, index...) + point, err := dExt.GetTextXY(ocrText, index...) if err != nil { if ignoreNotFoundError { return nil @@ -49,11 +63,11 @@ func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoun return err } - return dExt.tapFloat(point.X, point.Y, identifier) + return dExt.TapAbsXY(point.X, point.Y, identifier) } func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool, index ...int) error { - x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath) + point, err := dExt.GetImageXY(imagePath, index...) if err != nil { if ignoreNotFoundError { return nil @@ -61,7 +75,7 @@ func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFou return err } - return dExt.tapFloat(x+width*0.5, y+height*0.5, identifier) + return dExt.TapAbsXY(point.X, point.Y, identifier) } func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool, index ...int) error { @@ -83,7 +97,7 @@ func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identif return err } - return dExt.tapFloat(x+width*xOffset, y+height*yOffset, identifier) + return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, identifier) } func (dExt *DriverExt) DoubleTapXY(x, y float64) error { diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index bde3c861..faaaa14e 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -25,6 +25,14 @@ func TestDriverExt_TapXY(t *testing.T) { checkErr(t, err) } +func TestDriverExt_TapAbsXY(t *testing.T) { + driverExt, err := InitWDAClient(nil) + checkErr(t, err) + + err = driverExt.TapAbsXY(100, 300, "") + checkErr(t, err) +} + func TestDriverExt_TapWithOCR(t *testing.T) { driverExt, err := InitWDAClient(nil) checkErr(t, err) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 588b3728..69c63d05 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -85,6 +85,19 @@ func (s *StepIOS) TapXY(x, y float64, options ...uixt.ActionOption) *StepIOS { return &StepIOS{step: s.step} } +// TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates +func (s *StepIOS) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapAbsXY, + Params: []float64{x, y}, + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + // Tap taps on the target element func (s *StepIOS) Tap(params string, options ...uixt.ActionOption) *StepIOS { action := uixt.MobileAction{ From 8b2ee45c45b88b703f6a0a1dc77a0803528095a1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 18:18:33 +0800 Subject: [PATCH 069/169] fix: step type --- .gitignore | 1 + examples/uitest/demo_douyin_live.json | 2 +- examples/uitest/demo_douyin_live.yaml | 2 +- examples/uitest/demo_douyin_test.go | 2 +- examples/uitest/demo_weixin_test.go | 5 ++++- hrp/step_ios_ui.go | 4 ++-- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 0de9fa23..6d2852b5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ logs reports *.xml htmlcov/ +screenshots/ # built plugins debugtalk.bin diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json index 77cd4d9d..5ad7f471 100644 --- a/examples/uitest/demo_douyin_live.json +++ b/examples/uitest/demo_douyin_live.json @@ -1,6 +1,6 @@ { "config": { - "name": "通过 feed 卡片进入微信直播间", + "name": "通过 feed 卡片进入抖音直播间", "ios": [ { "port": 8700, diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml index f2c29f62..b5252943 100644 --- a/examples/uitest/demo_douyin_live.yaml +++ b/examples/uitest/demo_douyin_live.yaml @@ -1,5 +1,5 @@ config: - name: 通过 feed 卡片进入微信直播间 + name: 通过 feed 卡片进入抖音直播间 ios: - port: 8700 mjpeg_port: 8800 diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index 2880b4de..b5adf5ff 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -9,7 +9,7 @@ import ( func TestIOSDouyinLive(t *testing.T) { testCase := &hrp.TestCase{ - Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). + Config: hrp.NewConfig("通过 feed 卡片进入抖音直播间"). SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index f157e27c..dc7dfd95 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -44,7 +44,10 @@ func TestIOSWeixinLive(t *testing.T) { } runner := hrp.NewRunner(t) - sessionRunner, _ := runner.NewSessionRunner(testCase) + sessionRunner, err := runner.NewSessionRunner(testCase) + if err != nil { + t.Fatal(err) + } if err := sessionRunner.Start(nil); err != nil { t.Fatal(err) } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 69c63d05..46b43a04 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -316,7 +316,7 @@ func (s *StepIOS) Name() string { } func (s *StepIOS) Type() StepType { - return stepTypeAndroid + return stepTypeIOS } func (s *StepIOS) Struct() *TStep { @@ -457,7 +457,7 @@ func (s *StepIOSValidation) Name() string { } func (s *StepIOSValidation) Type() StepType { - return stepTypeAndroid + return stepTypeIOS } func (s *StepIOSValidation) Struct() *TStep { From 8710213ff863b7dc3da3230d2975178cf634ac5d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 18:45:56 +0800 Subject: [PATCH 070/169] change: remove ios health check --- hrp/internal/uixt/interface.go | 5 ++--- hrp/internal/uixt/ios_device.go | 15 --------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index 0101d2d9..03fd7a19 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -10,9 +10,8 @@ import ( ) var ( - DefaultWaitTimeout = 60 * time.Second - DefaultWaitInterval = 400 * time.Millisecond - DefaultKeepAliveInterval = 30 * time.Second + DefaultWaitTimeout = 60 * time.Second + DefaultWaitInterval = 400 * time.Millisecond ) type AlertAction string diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 7218e368..98912a26 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -13,7 +13,6 @@ import ( "net/url" "regexp" "strings" - "time" giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" @@ -220,20 +219,6 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, } _, err = wd.NewSession(capabilities) - go func() { - if DefaultKeepAliveInterval <= 0 { - return - } - ticker := time.NewTicker(DefaultKeepAliveInterval) - for { - <-ticker.C - if healthy, err := wd.IsHealthy(); err != nil || !healthy { - ticker.Stop() - return - } - } - }() - return wd, err } From 9b71617c3aae6c0893f729c3611adcbdce0875ad Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 20:32:02 +0800 Subject: [PATCH 071/169] feat: parse ios action params with variables --- examples/uitest/demo_douyin_live.json | 5 ++++- examples/uitest/demo_douyin_live.yaml | 4 +++- examples/uitest/demo_douyin_test.go | 5 ++++- go.mod | 2 +- hrp/internal/boomer/runner.go | 2 +- hrp/step_ios_ui.go | 11 +++++++++++ 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json index 5ad7f471..c86466ad 100644 --- a/examples/uitest/demo_douyin_live.json +++ b/examples/uitest/demo_douyin_live.json @@ -1,6 +1,9 @@ { "config": { "name": "通过 feed 卡片进入抖音直播间", + "variables": { + "app_name": "抖音" + }, "ios": [ { "port": 8700, @@ -23,7 +26,7 @@ }, { "method": "swipe_to_tap_app", - "params": "抖音", + "params": "$app_name", "max_retry_times": 5 }, { diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml index b5252943..1b4b276b 100644 --- a/examples/uitest/demo_douyin_live.yaml +++ b/examples/uitest/demo_douyin_live.yaml @@ -1,5 +1,7 @@ config: name: 通过 feed 卡片进入抖音直播间 + variables: + app_name: 抖音 ios: - port: 8700 mjpeg_port: 8800 @@ -12,7 +14,7 @@ teststeps: - method: app_terminate params: com.ss.iphone.ugc.Aweme - method: swipe_to_tap_app - params: 抖音 + params: $app_name max_retry_times: 5 - method: sleep params: 5 diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index b5adf5ff..7c9a3c59 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -10,13 +10,16 @@ import ( func TestIOSDouyinLive(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("通过 feed 卡片进入抖音直播间"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). IOS(). Home(). AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 - SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)).Sleep(5). + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5)).Sleep(5). Validate(). AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), // hrp.NewStep("处理青少年弹窗"). diff --git a/go.mod b/go.mod index 9148a7aa..3b02532a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 github.com/getsentry/sentry-go v0.13.0 - github.com/go-errors/errors v1.4.2 github.com/go-openapi/spec v0.20.7 github.com/go-ping/ping v1.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 @@ -43,6 +42,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // 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 diff --git a/hrp/internal/boomer/runner.go b/hrp/internal/boomer/runner.go index 506e3dc5..4bde3a03 100644 --- a/hrp/internal/boomer/runner.go +++ b/hrp/internal/boomer/runner.go @@ -10,9 +10,9 @@ import ( "sync/atomic" "time" - "github.com/go-errors/errors" "github.com/jinzhu/copier" "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 46b43a04..c53b06ca 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" @@ -512,6 +513,13 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } screenshots := make([]string, 0) + // override step variables + stepVariables, err := s.MergeStepVariables(step.Variables) + if err != nil { + return + } + parser := s.GetParser() + // init wdaClient driver wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.IOSDevice) if err != nil { @@ -557,6 +565,9 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro // run actions for _, action := range actions { + if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { + return stepResult, errors.Wrap(err, "parse action params failed") + } if err := wdaClient.DoAction(action); err != nil { return stepResult, err } From 8a8c075654d03420a45daad08ff18f6b93c2964a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 27 Sep 2022 21:05:44 +0800 Subject: [PATCH 072/169] fix: check WDA summary logs --- examples/uitest/demo_douyin_live.json | 29 ++++++++++++++------------- examples/uitest/demo_douyin_live.yaml | 17 ++++++++-------- examples/uitest/demo_douyin_test.go | 20 +++++++----------- examples/uitest/demo_weixin_test.go | 10 ++------- hrp/internal/uixt/ext.go | 1 + hrp/internal/uixt/ios_device.go | 1 + hrp/runner.go | 6 +++--- hrp/session.go | 17 ++++++---------- hrp/step_ios_ui.go | 2 +- hrp/step_request_test.go | 2 +- hrp/step_testcase.go | 18 +++++++++++++---- hrp/summary.go | 2 +- 12 files changed, 61 insertions(+), 64 deletions(-) diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json index c86466ad..94fe4496 100644 --- a/examples/uitest/demo_douyin_live.json +++ b/examples/uitest/demo_douyin_live.json @@ -27,6 +27,7 @@ { "method": "swipe_to_tap_app", "params": "$app_name", + "identifier": "启动抖音", "max_retry_times": 5 }, { @@ -45,20 +46,7 @@ ] }, { - "name": "在推荐页上划,直到出现「点击进入直播间」", - "ios": { - "actions": [ - { - "method": "swipe_to_tap_text", - "params": "点击进入直播间", - "identifier": "进入直播间", - "max_retry_times": 100 - } - ] - } - }, - { - "name": "向上滑动,等待 10s", + "name": "向上滑动 2 次", "ios": { "actions": [ { @@ -87,6 +75,19 @@ } ] } + }, + { + "name": "在推荐页上划,直到出现「点击进入直播间」", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": "点击进入直播间", + "identifier": "进入直播间", + "max_retry_times": 10 + } + ] + } } ] } diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml index 1b4b276b..d2463772 100644 --- a/examples/uitest/demo_douyin_live.yaml +++ b/examples/uitest/demo_douyin_live.yaml @@ -15,6 +15,7 @@ teststeps: params: com.ss.iphone.ugc.Aweme - method: swipe_to_tap_app params: $app_name + identifier: 启动抖音 max_retry_times: 5 - method: sleep params: 5 @@ -23,14 +24,7 @@ teststeps: assert: exists expect: 推荐 msg: 抖音启动失败,「推荐」不存在 - - name: 在推荐页上划,直到出现「点击进入直播间」 - ios: - actions: - - method: swipe_to_tap_text - params: 点击进入直播间 - identifier: 进入直播间 - max_retry_times: 100 - - name: 向上滑动,等待 10s + - name: 向上滑动 2 次 ios: actions: - method: swipe @@ -45,3 +39,10 @@ teststeps: - method: sleep params: 2 - method: screenshot + - name: 在推荐页上划,直到出现「点击进入直播间」 + ios: + actions: + - method: swipe_to_tap_text + params: 点击进入直播间 + identifier: 进入直播间 + max_retry_times: 10 diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index 7c9a3c59..9abc72d6 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -1,7 +1,6 @@ package uitest import ( - "fmt" "testing" "github.com/httprunner/httprunner/v4/hrp" @@ -19,19 +18,19 @@ func TestIOSDouyinLive(t *testing.T) { IOS(). Home(). AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 - SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5)).Sleep(5). + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). Validate(). AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), // hrp.NewStep("处理青少年弹窗"). // IOS(). // TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), - hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」"). - IOS(). - SwipeToTapText("点击进入直播间", hrp.WithMaxRetryTimes(100), hrp.WithIdentifier("进入直播间")), - hrp.NewStep("向上滑动,等待 10s"). + hrp.NewStep("向上滑动 2 次"). IOS(). SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(2).ScreenShot(). // 上划 1 次,等待 2s,截图保存 SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(2).ScreenShot(), // 再上划 1 次,等待 2s,截图保存 + hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」"). + IOS(). + SwipeToTapText("点击进入直播间", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), }, } @@ -42,14 +41,9 @@ func TestIOSDouyinLive(t *testing.T) { t.Fatal(err) } - runner := hrp.NewRunner(t) - sessionRunner, err := runner.NewSessionRunner(testCase) + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) if err != nil { t.Fatal(err) } - if err := sessionRunner.Start(nil); err != nil { - t.Fatal(err) - } - summary := sessionRunner.GetSummary() - fmt.Println(summary) } diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index dc7dfd95..112b6ffd 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -1,7 +1,6 @@ package uitest import ( - "fmt" "testing" "github.com/httprunner/httprunner/v4/hrp" @@ -43,14 +42,9 @@ func TestIOSWeixinLive(t *testing.T) { t.Fatal(err) } - runner := hrp.NewRunner(t) - sessionRunner, err := runner.NewSessionRunner(testCase) + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) if err != nil { t.Fatal(err) } - if err := sessionRunner.Start(nil); err != nil { - t.Fatal(err) - } - summary := sessionRunner.GetSummary() - fmt.Println(summary) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index d0bd92df..c4aaad79 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -124,6 +124,7 @@ func WithThreshold(threshold float64) CVOption { } type DriverExt struct { + Device Device Driver WebDriver windowSize Size frame *bytes.Buffer diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 98912a26..8313aa89 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -96,6 +96,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } } + driverExt.Device = iosDevice return driverExt, nil } diff --git a/hrp/runner.go b/hrp/runner.go index dfb02551..d7d59202 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -210,10 +210,10 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { } for it := sessionRunner.parametersIterator; it.HasNext(); { - err = sessionRunner.Start(it.Next()) - caseSummary := sessionRunner.GetSummary() + err1 := sessionRunner.Start(it.Next()) + caseSummary, err2 := sessionRunner.GetSummary() s.appendCaseSummary(caseSummary) - if err != nil { + if err1 != nil || err2 != nil { log.Error().Err(err).Msg("[Run] run testcase failed") runErr = err break diff --git a/hrp/session.go b/hrp/session.go index d640f2f2..10b0d76a 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -7,8 +7,6 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // SessionRunner is used to run testcase and its steps. @@ -151,7 +149,7 @@ func (r *SessionRunner) updateSessionVariables(parameters map[string]interface{} } } -func (r *SessionRunner) GetSummary() *TestCaseSummary { +func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { caseSummary := r.summary caseSummary.Name = r.parsedConfig.Name caseSummary.Time.StartAt = r.startTime @@ -163,19 +161,16 @@ func (r *SessionRunner) GetSummary() *TestCaseSummary { caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ConfigVars = r.parsedConfig.Variables + // add WDA/UIA logs to summary logs := make(map[string]string) for udid, client := range r.hrpRunner.uiClients { log, err := client.GetLogs() if err != nil { - logs[udid] = err.Error() - } else { - logs[udid] = log + return nil, err } - + logs[udid] = log } - logsStr, _ := json.Marshal(logs) - caseSummary.Logs = string(logsStr) + caseSummary.Logs = logs - // caseSummary.Log - return caseSummary + return caseSummary, nil } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index c53b06ca..be9a566d 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -499,7 +499,7 @@ func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, er if r.uiClients == nil { r.uiClients = make(map[string]*uixt.DriverExt) } - r.uiClients[uuid] = client + r.uiClients[client.Device.UUID()] = client return client, nil } diff --git a/hrp/step_request_test.go b/hrp/step_request_test.go index 338d7e7e..d49a9253 100644 --- a/hrp/step_request_test.go +++ b/hrp/step_request_test.go @@ -103,7 +103,7 @@ func TestRunRequestStatOn(t *testing.T) { if err := sessionRunner.Start(nil); err != nil { t.Fatal() } - summary := sessionRunner.GetSummary() + summary, _ := sessionRunner.GetSummary() stat := summary.Records[0].HttpStat if !assert.GreaterOrEqual(t, stat["DNSLookup"], int64(0)) { diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index 0bcc1ae3..72f2c340 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -5,6 +5,7 @@ import ( "time" "github.com/jinzhu/copier" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -89,11 +90,17 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe start := time.Now() // run referenced testcase with step variables err = sessionRunner.Start(stepVariables) - if err == nil { - stepResult.Success = true - } stepResult.Elapsed = time.Since(start).Milliseconds() - summary := sessionRunner.GetSummary() + + summary, err2 := sessionRunner.GetSummary() + if err2 != nil { + log.Error().Err(err).Msg("get summary failed") + if err != nil { + err = errors.Wrap(err, err2.Error()) + } else { + err = err2 + } + } // update step names for _, record := range summary.Records { record.Name = fmt.Sprintf("%s - %s", stepResult.Name, record.Name) @@ -108,5 +115,8 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe r.summary.Stat.Successes += summary.Stat.Successes r.summary.Stat.Failures += summary.Stat.Failures + if err == nil { + stepResult.Success = true + } return stepResult, err } diff --git a/hrp/summary.go b/hrp/summary.go index a944d6a3..da3b0197 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Logs string `json:"logs,omitempty" yaml:"logs,omitempty"` // TODO + Logs interface{} `json:"logs,omitempty" yaml:"logs,omitempty"` Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` } From 59e501a453eb2b9c07a4c63e184484813201b521 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 28 Sep 2022 11:50:34 +0800 Subject: [PATCH 073/169] change: update wda logs --- examples/uitest/demo_douyin_live.json | 12 ++++++++++++ examples/uitest/demo_douyin_live.yaml | 6 ++++++ examples/uitest/demo_douyin_test.go | 6 +++--- hrp/internal/uixt/ext.go | 2 +- hrp/internal/uixt/ios_device.go | 11 +++++++---- hrp/internal/uixt/ocr_on.go | 2 +- hrp/runner.go | 2 +- hrp/session.go | 11 ++++++++--- hrp/step_ios_ui.go | 2 +- hrp/summary.go | 2 +- 10 files changed, 41 insertions(+), 15 deletions(-) diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json index 94fe4496..e9dd2f84 100644 --- a/examples/uitest/demo_douyin_live.json +++ b/examples/uitest/demo_douyin_live.json @@ -45,6 +45,18 @@ } ] }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, { "name": "向上滑动 2 次", "ios": { diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml index d2463772..b1eccd04 100644 --- a/examples/uitest/demo_douyin_live.yaml +++ b/examples/uitest/demo_douyin_live.yaml @@ -24,6 +24,12 @@ teststeps: assert: exists expect: 推荐 msg: 抖音启动失败,「推荐」不存在 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true - name: 向上滑动 2 次 ios: actions: diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index 9abc72d6..b9ca8fa9 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -21,9 +21,9 @@ func TestIOSDouyinLive(t *testing.T) { SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). Validate(). AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), - // hrp.NewStep("处理青少年弹窗"). - // IOS(). - // TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), hrp.NewStep("向上滑动 2 次"). IOS(). SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(2).ScreenShot(). // 上划 1 次,等待 2s,截图保存 diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index c4aaad79..bed62ffc 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -124,7 +124,7 @@ func WithThreshold(threshold float64) CVOption { } type DriverExt struct { - Device Device + UUID string // ios udid or android serial Driver WebDriver windowSize Size frame *bytes.Buffer diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 8313aa89..bfacfb72 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -96,7 +96,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } } - driverExt.Device = iosDevice + driverExt.UUID = iosDevice.UUID() return driverExt, nil } @@ -224,8 +224,8 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, } type wdaResponse struct { - Value string `json:"value"` - SessionID string `json:"sessionId"` + Value interface{} `json:"value"` + SessionID string `json:"sessionId"` } func (dExt *DriverExt) StartLogRecording(identifier string) error { @@ -239,11 +239,12 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error { return nil } -func (dExt *DriverExt) GetLogs() (string, error) { +func (dExt *DriverExt) GetLogs() (interface{}, error) { log.Info().Msg("stop WDA log recording") data := map[string]interface{}{"action": "stop"} reply, err := dExt.triggerWDALog(data) if err != nil { + log.Error().Err(err).Msg("failed to get WDA logs") return "", errors.Wrap(err, "failed to get WDA logs") } @@ -275,8 +276,10 @@ func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, reply := new(wdaResponse) if err = json.Unmarshal(rawResp, reply); err != nil { + log.Info().Bytes("rawResp", rawResp).Msg("get unexpected WDA log response") return nil, err } + log.Info().Interface("value", reply.Value).Msg("get WDA log response") return reply, nil } diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 327bc373..13f1b213 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -163,7 +163,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, service := &veDEMOCRService{} rect, err := service.FindText(ocrText, bufSource.Bytes(), index...) if err != nil { - log.Warn().Err(err).Msg("FindText failed") + log.Warn().Msgf("FindText failed: %s", err.Error()) err = fmt.Errorf("FindText failed: %v", err) return } diff --git a/hrp/runner.go b/hrp/runner.go index d7d59202..2524ff8c 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -214,7 +214,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { caseSummary, err2 := sessionRunner.GetSummary() s.appendCaseSummary(caseSummary) if err1 != nil || err2 != nil { - log.Error().Err(err).Msg("[Run] run testcase failed") + log.Error().Err(err1).Msg("[Run] run testcase failed") runErr = err break } diff --git a/hrp/session.go b/hrp/session.go index 10b0d76a..83c0429b 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -5,8 +5,11 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // SessionRunner is used to run testcase and its steps. @@ -162,15 +165,17 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { caseSummary.InOut.ConfigVars = r.parsedConfig.Variables // add WDA/UIA logs to summary - logs := make(map[string]string) + logs := make(map[string]interface{}) for udid, client := range r.hrpRunner.uiClients { log, err := client.GetLogs() if err != nil { - return nil, err + return caseSummary, err } logs[udid] = log } - caseSummary.Logs = logs + + logsBytes, _ := json.Marshal(logs) + caseSummary.Logs = string(logsBytes) return caseSummary, nil } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index be9a566d..7b2c0c9e 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -499,7 +499,7 @@ func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, er if r.uiClients == nil { r.uiClients = make(map[string]*uixt.DriverExt) } - r.uiClients[client.Device.UUID()] = client + r.uiClients[client.UUID] = client return client, nil } diff --git a/hrp/summary.go b/hrp/summary.go index da3b0197..841f7fc5 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Logs interface{} `json:"logs,omitempty" yaml:"logs,omitempty"` + Logs string `json:"logs,omitempty" yaml:"logs,omitempty"` Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` } From af54e011d2bdbc24601f9aa75bb0bca754a91956 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 28 Sep 2022 14:07:40 +0800 Subject: [PATCH 074/169] fix: wait for action done before screenshot --- hrp/internal/uixt/ext.go | 3 +++ hrp/internal/uixt/ocr_on.go | 7 ++++++- hrp/internal/uixt/swipe.go | 3 --- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index bed62ffc..e333ed48 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -155,6 +155,9 @@ func extend(driver WebDriver) (dExt *DriverExt, err error) { } func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { + // wait for action done + time.Sleep(500 * time.Millisecond) + // 优先使用 MJPEG 流进行截图,性能最优 // 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口 if dExt.frame != nil { diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 13f1b213..fe0620c9 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -93,11 +93,15 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( ocrResults, err := s.getOCRResult(imageBuf) if err != nil { + log.Error().Err(err).Msg("getOCRResult failed") return } var rects []image.Rectangle + var ocrTexts []string for _, ocrResult := range ocrResults { + ocrTexts = append(ocrTexts, ocrResult.Text) + // not contains text if !strings.Contains(ocrResult.Text, text) { continue @@ -128,7 +132,8 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( } if len(rects) == 0 { - return image.Rectangle{}, fmt.Errorf("text %s not found", text) + return image.Rectangle{}, + fmt.Errorf("text %s not found in %v", text, ocrTexts) } // get index diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 7973b53a..c329263d 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -2,7 +2,6 @@ package uixt import ( "fmt" - "time" "github.com/rs/zerolog/log" ) @@ -82,8 +81,6 @@ func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, act if err := dExt.SwipeTo(direction); err != nil { log.Error().Err(err).Msgf("swipe %s failed", direction) } - // wait for swipe done - time.Sleep(500 * time.Millisecond) } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } From fe41b20828930e78e293cd45519cfafe1122baef Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 17:50:08 +0800 Subject: [PATCH 075/169] fix: run ocr alone without opencv --- hrp/internal/uixt/opencv_on.go | 1 - 1 file changed, 1 deletion(-) diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index d4acda45..2153283f 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -5,7 +5,6 @@ package uixt import ( "bytes" "image" - "io/ioutil" "os" cvHelper "github.com/electricbubble/opencv-helper" From dd9cf7dc6ee196b52dd70ba8607209cbd994ce00 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 13:46:34 +0800 Subject: [PATCH 076/169] refactor: android ui automation --- examples/uitest/demo_android_douyin_test.go | 48 + hrp/config.go | 21 + hrp/internal/uixt/android_action.go | 4 +- hrp/internal/uixt/android_device.go | 27 +- hrp/internal/uixt/android_driver.go | 1581 ++++++++----------- hrp/internal/uixt/android_elment.go | 423 ++--- hrp/internal/uixt/android_test.go | 1165 +++++++------- hrp/internal/uixt/client.go | 4 +- hrp/internal/uixt/ext.go | 22 +- hrp/internal/uixt/interface.go | 87 + hrp/internal/uixt/ios_device.go | 17 +- hrp/internal/uixt/ios_driver.go | 28 + hrp/step_android_ui.go | 257 ++- 13 files changed, 1912 insertions(+), 1772 deletions(-) create mode 100644 examples/uitest/demo_android_douyin_test.go diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go new file mode 100644 index 00000000..4bf95693 --- /dev/null +++ b/examples/uitest/demo_android_douyin_test.go @@ -0,0 +1,48 @@ +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouYinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). + SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + Android(). + Home().StartCamera().Sleep(10).StopCamera(). + AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)). + Sleep(10), + hrp.NewStep("处理青少年弹窗"). + Android(). + Tap("推荐"). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)). + Validate(). + AssertOCRExists("首页", "抖音启动失败,「首页」不存在"), + hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」"). + Android(). + SwipeToTapText("直播", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), + hrp.NewStep("向上滑动,等待 10s"). + Android(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + if err := testCase.Dump2JSON("android_demo_douyin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("android_demo_douyin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/config.go b/hrp/config.go index d5caffcf..3d1e1aa1 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -123,6 +123,27 @@ func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig { return c } +func (c *TConfig) SetAndroid(options ...uixt.AndroidDeviceOption) *TConfig { + uiaOptions := &uixt.AndroidDevice{} + for _, option := range options { + option(uiaOptions) + } + + // each device can have its own settings + if uiaOptions.SerialNumber != "" { + c.Android = append(c.Android, uiaOptions) + return c + } + + // device UDID is not specified, settings will be shared + if len(c.Android) == 0 { + c.Android = append(c.Android, uiaOptions) + } else { + c.Android[0] = uiaOptions + } + return c +} + type ThinkTimeConfig struct { Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5 diff --git a/hrp/internal/uixt/android_action.go b/hrp/internal/uixt/android_action.go index 254972d3..b8081614 100644 --- a/hrp/internal/uixt/android_action.go +++ b/hrp/internal/uixt/android_action.go @@ -42,7 +42,7 @@ func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchActio return ta.AddFloat(point.X, point.Y, startTime...) } -func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { +func (ud *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { // Must provide coordinates for at least 2 pointers actions := make([]*TouchAction, 0) actions = append(actions, gesture1, gesture2) @@ -53,7 +53,7 @@ func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAc "actions": actions, } // register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/touch/multi/perform") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/touch/multi/perform") return } diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index f00880ad..275e4cb9 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -44,11 +44,15 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { fmt.Println(driver) var driverExt *DriverExt - // TODO - // driverExt, err = Extend(driver) - // if err != nil { - // return nil, errors.Wrap(err, "failed to extend UIA Driver") - // } + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend UIA Driver") + } + + if device.LogOn { + // TODO + } return driverExt, nil } @@ -61,6 +65,12 @@ func WithSerialNumber(serial string) AndroidDeviceOption { } } +func WithMjpegPortA(port int) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.MjpegPort = port + } +} + func WithAdbIP(ip string) AndroidDeviceOption { return func(device *AndroidDevice) { device.IP = ip @@ -113,6 +123,7 @@ type AndroidDevice struct { SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` IP string `json:"ip,omitempty" yaml:"ip,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } @@ -149,12 +160,6 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDr driver.adbDevice = dev.d driver.localPort = localPort - conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) - if err != nil { - return nil, fmt.Errorf("adb forward: %w", err) - } - driver.client = convertToHTTPClient(conn) - return driver, nil } diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 7851a3da..77501047 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -6,18 +6,18 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/url" - "os" - "path" - "path/filepath" - "regexp" "strconv" "strings" "time" "github.com/electricbubble/gadb" + "github.com/rs/zerolog/log" ) +var errDriverNotImplemented = errors.New("driver method not implemented") + type uiaDriver struct { Driver @@ -33,339 +33,24 @@ func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDrive if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { return nil, err } - if driver.sessionId, err = driver.NewSession(capabilities); err != nil { - return nil, err - } - return -} - -func (d *uiaDriver) NewSession(capabilities Capabilities) (sessionID string, err error) { - // register(postHandler, new NewSession("/wd/hub/session")) - var rawResp rawResponse - data := map[string]interface{}{"capabilities": capabilities} - if rawResp, err = d.httpPOST(data, "/session"); err != nil { - return "", err - } - reply := new(struct{ Value struct{ SessionId string } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - sessionID = reply.Value.SessionId - // d.sessionIdCache[sessionID] = true - return -} - -func (d *uiaDriver) Quit() (err error) { - // register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId")) - if d.sessionId == "" { - return nil - } - if _, err = d.httpDELETE("/session", d.sessionId); err == nil { - d.sessionId = "" - } - - return err -} - -func (d *uiaDriver) ActiveSessionID() string { - return d.sessionId -} - -func (d *uiaDriver) SessionIDs() (sessionIDs []string, err error) { - // register(getHandler, new GetSessions("/wd/hub/sessions")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/sessions"); err != nil { - return nil, err - } - reply := new(struct{ Value []struct{ SessionId string } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - sessionIDs = make([]string, len(reply.Value)) - for i := range reply.Value { - sessionIDs[i] = reply.Value[i].SessionId - } - return -} - -func (d *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { - // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId); err != nil { - return nil, err - } - reply := new(struct{ Value map[string]interface{} }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - scrollData = reply.Value - return -} - -func (d *uiaDriver) Status() (ready bool, err error) { - // register(getHandler, new Status("/wd/hub/status")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/status"); err != nil { - return false, err - } - reply := new(struct { - Value struct { - // Message string - Ready bool - } - }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return false, err - } - ready = reply.Value.Ready - return -} - -// Screenshot grab device screenshot -func (d *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { - // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "screenshot"); err != nil { - return nil, err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - var decodeStr []byte - if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { - return nil, err - } - - raw = bytes.NewBuffer(decodeStr) - return -} - -func (d *uiaDriver) Orientation() (orientation Orientation, err error) { - // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "orientation"); err != nil { - return "", err - } - reply := new(struct{ Value Orientation }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - orientation = reply.Value - return -} - -func (d *uiaDriver) Rotation() (rotation Rotation, err error) { - // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "rotation"); err != nil { - return Rotation{}, err - } - reply := new(struct{ Value Rotation }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Rotation{}, err - } - - rotation = reply.Value - return -} - -// DeviceSize get window size of the device -func (d *uiaDriver) DeviceSize() (deviceSize Size, err error) { - // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "window/:windowHandle/size"); err != nil { - return Size{}, err - } - reply := new(struct{ Value Size }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{}, err - } - - deviceSize = reply.Value - return -} - -// Source get page source -func (d *uiaDriver) Source() (sXML string, err error) { - // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "source"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - sXML = reply.Value - return -} - -// StatusBarHeight get status bar height of the device -func (d *uiaDriver) StatusBarHeight() (height int, err error) { - // register(getHandler, new GetSystemBars("/wd/hub/session/:sessionId/appium/device/system_bars")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/system_bars"); err != nil { - return 0, err - } - reply := new(struct{ Value struct{ StatusBar int } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return 0, err - } - - height = reply.Value.StatusBar - return -} - -func (d *uiaDriver) check() error { - if d.adbDevice.Serial() == "" { - return errors.New("adb daemon: the device is not ready") - } - return nil -} - -// Dispose corresponds to the command: -// adb -s $serial forward --remove $localPort -func (d *uiaDriver) Dispose() (err error) { - if err = d.check(); err != nil { - return err - } - if d.localPort == 0 { - return nil - } - return d.adbDevice.ForwardKill(d.localPort) -} - -func (d *uiaDriver) ActiveAppActivity() (appActivity string, err error) { - if err = d.check(); err != nil { - return "", err - } - - var sOutput string - if sOutput, err = d.adbDevice.RunShellCommand("dumpsys activity activities | grep mResumedActivity"); err != nil { - return "", err - } - re := regexp.MustCompile(`\{(.+?)\}`) - if !re.MatchString(sOutput) { - return "", fmt.Errorf("active app activity: %s", strings.TrimSpace(sOutput)) - } - fields := strings.Fields(re.FindStringSubmatch(sOutput)[1]) - appActivity = fields[2] - return -} - -func (d *uiaDriver) ActiveAppPackageName() (appPackageName string, err error) { - var activity string - if activity, err = d.ActiveAppActivity(); err != nil { - return "", err - } - appPackageName = strings.Split(activity, "/")[0] - return -} - -func (d *uiaDriver) AppLaunch(appPackageName string, waitForComplete ...AndroidBySelector) (err error) { - if err = d.check(); err != nil { - return err - } - - var sOutput string - if sOutput, err = d.adbDevice.RunShellCommand("monkey -p", appPackageName, "-c android.intent.category.LAUNCHER 1"); err != nil { - return err - } - if strings.Contains(sOutput, "monkey aborted") { - return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) - } - - if len(waitForComplete) != 0 { - var ce error - exists := func(d *uiaDriver) (bool, error) { - for i := range waitForComplete { - _, ce = d.FindElement(waitForComplete[i]) - if ce == nil { - return true, nil - } - } - return false, nil - } - if err = d.WaitWithTimeoutAndInterval(exists, 45, 1.5); err != nil { - return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + var localPort int + { + tmpURL, _ := url.Parse(driver.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) } } - return -} - -func (d *uiaDriver) AppTerminate(appPackageName string) (err error) { - if err = d.check(); err != nil { - return err - } - - _, err = d.adbDevice.RunShellCommand("am force-stop", appPackageName) - return -} - -func (d *uiaDriver) AppInstall(apkPath string, reinstall ...bool) (err error) { - if err = d.check(); err != nil { - return err - } - - apkName := filepath.Base(apkPath) - if !strings.HasSuffix(strings.ToLower(apkName), ".apk") { - return fmt.Errorf("apk file must have an extension of '.apk': %s", apkPath) - } - - var apkFile *os.File - if apkFile, err = os.Open(apkPath); err != nil { - return fmt.Errorf("apk file: %w", err) - } - - remotePath := path.Join(DeviceTempPath, apkName) - if err = d.adbDevice.PushFile(apkFile, remotePath); err != nil { - return fmt.Errorf("apk push: %w", err) - } - - var shellOutput string - if len(reinstall) != 0 && reinstall[0] { - shellOutput, err = d.adbDevice.RunShellCommand("pm install", "-r", remotePath) - } else { - shellOutput, err = d.adbDevice.RunShellCommand("pm install", remotePath) - } - + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) if err != nil { - return fmt.Errorf("apk install: %w", err) + return nil, fmt.Errorf("adb forward: %w", err) } - - if !strings.Contains(shellOutput, "Success") { - return fmt.Errorf("apk installed: %s", shellOutput) - } - - return -} - -func (d *uiaDriver) AppUninstall(appPackageName string, keepDataAndCache ...bool) (err error) { - if err = d.check(); err != nil { - return err - } - - var shellOutput string - if len(keepDataAndCache) != 0 && keepDataAndCache[0] { - shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", "-k", appPackageName) + driver.client = convertToHTTPClient(conn) + if session, err := driver.NewSession(capabilities); err != nil { + return nil, err } else { - shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", appPackageName) + driver.sessionId = session.SessionId } - - if err != nil { - return fmt.Errorf("apk uninstall: %w", err) - } - - if !strings.Contains(shellOutput, "Success") { - return fmt.Errorf("apk uninstalled: %s", shellOutput) - } - return } @@ -397,132 +82,267 @@ func (bs BatteryStatus) String() string { } } -func (d *uiaDriver) BatteryInfo() (info BatteryInfo, err error) { - // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/battery_info"); err != nil { - return BatteryInfo{}, err +func (ud *uiaDriver) Close() (err error) { + if ud.sessionId == "" { + return nil } - reply := new(struct{ Value BatteryInfo }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return BatteryInfo{}, err + if _, err = ud.httpDELETE("/session", ud.sessionId); err == nil { + ud.sessionId = "" } - info = reply.Value - if info.Level == -1 || info.Status == -1 { - return info, errors.New("cannot be retrieved from the system") - } - return + return err } -func (d *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { - // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) +func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { + // register(postHandler, new NewSession("/wd/hub/session")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/settings"); err != nil { + data := map[string]interface{}{"capabilities": capabilities} + if rawResp, err = ud.httpPOST(data, "/session"); err != nil { + return SessionInfo{SessionId: ""}, err + } + reply := new(struct{ Value struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return SessionInfo{SessionId: ""}, err + } + sessionID := reply.Value.SessionId + // d.sessionIdCache[sessionID] = true + return SessionInfo{SessionId: sessionID}, nil +} + +func (ud *uiaDriver) ActiveSession() (sessionInfo SessionInfo, err error) { + // [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)] + return SessionInfo{SessionId: ud.sessionId}, nil +} + +func (ud *uiaDriver) SessionIDs() (sessionIDs []string, err error) { + // register(getHandler, new GetSessions("/wd/hub/sessions")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/sessions"); err != nil { return nil, err } - reply := new(struct{ Value map[string]interface{} }) + reply := new(struct{ Value []struct{ SessionId string } }) if err = json.Unmarshal(rawResp, reply); err != nil { return nil, err } - settings = reply.Value + sessionIDs = make([]string, len(reply.Value)) + for i := range reply.Value { + sessionIDs[i] = reply.Value[i].SessionId + } return } -// DeviceScaleRatio get device pixel ratio -func (d *uiaDriver) DeviceScaleRatio() (scale float64, err error) { - // register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio")) +func (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { + // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/pixel_ratio"); err != nil { - return 0, err + if rawResp, err = ud.httpGET("/session", ud.sessionId); err != nil { + return nil, err } - reply := new(struct{ Value float64 }) + var reply = new(struct{ Value map[string]interface{} }) if err = json.Unmarshal(rawResp, reply); err != nil { - return 0, err + return nil, err } - scale = reply.Value + scrollData = reply.Value return } -type ( - AndroidDeviceInfo struct { - // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user - // first sets up the device and should remain constant for the lifetime of the user's device. The value - // may change if a factory reset is performed on the device. - AndroidID string `json:"androidId"` - // Build.MANUFACTURER value - Manufacturer string `json:"manufacturer"` - // Build.MODEL value - Model string `json:"model"` - // Build.BRAND value - Brand string `json:"brand"` - // Current running OS's API VERSION - APIVersion string `json:"apiVersion"` - // The current version string, for example "1.0" or "3.4b5" - PlatformVersion string `json:"platformVersion"` - // the name of the current celluar network carrier - CarrierName string `json:"carrierName"` - // the real size of the default display - RealDisplaySize string `json:"realDisplaySize"` - // The logical density of the display in Density Independent Pixel units. - DisplayDensity int `json:"displayDensity"` - // available networks - Networks []networkInfo `json:"networks"` - // current system locale - Locale string `json:"locale"` - // current system timezone - // e.g. "Asia/Tokyo", "America/Caracas", "Asia/Shanghai" - TimeZone string `json:"timeZone"` - Bluetooth struct { - State string `json:"state"` - } `json:"bluetooth"` - } - networkCapabilities struct { - TransportTypes string `json:"transportTypes"` - NetworkCapabilities string `json:"networkCapabilities"` - LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` - LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` - SignalStrength int `json:"signalStrength"` - SSID string `json:"SSID"` - } - networkInfo struct { - Type int `json:"type"` - TypeName string `json:"typeName"` - Subtype int `json:"subtype"` - SubtypeName string `json:"subtypeName"` - IsConnected bool `json:"isConnected"` - DetailedState string `json:"detailedState"` - State string `json:"state"` - ExtraInfo string `json:"extraInfo"` - IsAvailable bool `json:"isAvailable"` - IsRoaming bool `json:"isRoaming"` - IsFailover bool `json:"isFailover"` - Capabilities networkCapabilities `json:"capabilities"` - } -) +func (ud *uiaDriver) DeleteSession() (err error) { + // TODO + return errDriverNotImplemented +} -func (d *uiaDriver) DeviceInfo() (info AndroidDeviceInfo, err error) { +func (ud *uiaDriver) Status() (deviceStatus DeviceStatus, err error) { + // register(getHandler, new Status("/wd/hub/status")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/status"); err != nil { + return DeviceStatus{Ready: false}, err + } + reply := new(struct { + Value struct { + // Message string + Ready bool + } + }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return DeviceStatus{Ready: false}, err + } + return DeviceStatus{Ready: true}, nil +} + +func (ud *uiaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { // register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/info"); err != nil { - return AndroidDeviceInfo{}, err + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/info"); err != nil { + return DeviceInfo{}, err } - reply := new(struct{ Value AndroidDeviceInfo }) + reply := new(struct{ Value struct{ DeviceInfo } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return AndroidDeviceInfo{}, err + return DeviceInfo{}, err } - - info = reply.Value + deviceInfo = reply.Value.DeviceInfo return } -// AlertText get text of the on-screen dialog -func (d *uiaDriver) AlertText() (text string, err error) { +func (ud *uiaDriver) Location() (location Location, err error) { + // TODO + return location, errDriverNotImplemented +} + +func (ud *uiaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { + // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/battery_info"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value struct{ BatteryInfo } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + if reply.Value.Level == -1 || reply.Value.Status == -1 { + return reply.Value.BatteryInfo, errors.New("cannot be retrieved from the system") + } + batteryInfo = reply.Value.BatteryInfo + return +} + +func (ud *uiaDriver) WindowSize() (size Size, err error) { + // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "window/:windowHandle/size"); err != nil { + return Size{}, err + } + reply := new(struct{ Value struct{ Size } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + size = reply.Value.Size + return +} + +func (ud *uiaDriver) Screen() (screen Screen, err error) { + // TODO + return screen, errDriverNotImplemented +} + +func (ud *uiaDriver) Scale() (scale float64, err error) { + return 1, nil +} + +// PressBack simulates a short press on the BACK button. +func (ud *uiaDriver) PressBack() (err error) { + // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) + _, err = ud.httpPOST(nil, "/session", ud.sessionId, "back") + return +} + +func (ud *uiaDriver) StartCamera() (err error) { + if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil { + return err + } + return +} + +func (ud *uiaDriver) StopCamera() (err error) { + err = ud.PressBack() + if err != nil { + return err + } + err = ud.Homescreen() + if err != nil { + return err + } + + // kill samsung shell command + if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.sec.android.app.camera"); err != nil { + return err + } + + // kill other camera (huawei mi) + if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.android.camera2"); err != nil { + return err + } + return +} + +func (ud *uiaDriver) StartRecording() (err error) { + var res string + if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + log.Info().Str("shell", res) + return +} + +func (ud *uiaDriver) StopRecording() (err error) { + var res string + if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + log.Info().Str("shell", res) + return +} + +func (ud *uiaDriver) ActiveAppInfo() (info AppInfo, err error) { + // TODO + return info, errDriverNotImplemented +} + +func (ud *uiaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { + // TODO + return appsList, errDriverNotImplemented +} + +func (ud *uiaDriver) AppState(bundleId string) (runState AppState, err error) { + // TODO + return runState, errDriverNotImplemented +} + +func (ud *uiaDriver) IsLocked() (locked bool, err error) { + // TODO + return locked, errDriverNotImplemented +} + +func (ud *uiaDriver) Unlock() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Lock() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Homescreen() (err error) { + return ud.PressKeyCode(KCHome, KMEmpty) +} + +func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + return ud._pressKeyCode(keyCode, metaState, KFFromSystem) +} + +func (ud *uiaDriver) _pressKeyCode(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, + } + if metaState != KMEmpty { + data["metastate"] = metaState + } + if len(flags) != 0 { + data["flags"] = flags[0] + } + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/press_keycode") + return +} + +func (ud *uiaDriver) AlertText() (text string, err error) { // register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "alert/text"); err != nil { + if rawResp, err = ud.httpGET("/session", ud.sessionId, "alert/text"); err != nil { return "", err } reply := new(struct{ Value string }) @@ -534,30 +354,202 @@ func (d *uiaDriver) AlertText() (text string, err error) { return } -// Tap perform a click at arbitrary coordinates specified -func (d *uiaDriver) Tap(x, y int) (err error) { - return d.TapFloat(float64(x), float64(y)) +func (ud *uiaDriver) AlertButtons() (btnLabels []string, err error) { + // TODO + return btnLabels, errDriverNotImplemented } -func (d *uiaDriver) TapFloat(x, y float64) (err error) { +func (ud *uiaDriver) AlertAccept(label ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(label) != 0 { + data["buttonLabel"] = label[0] + } + // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/accept") + return +} + +func (ud *uiaDriver) AlertDismiss(label ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(label) != 0 { + data["buttonLabel"] = label[0] + } + // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/dismiss") + return +} + +func (ud *uiaDriver) AlertSendKeys(text string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) check() error { + if ud.adbDevice.Serial() == "" { + return errors.New("adb daemon: the device is not ready") + } + return nil +} + +func (ud *uiaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) { + if err = ud.check(); err != nil { + return err + } + + var sOutput string + if sOutput, err = ud.adbDevice.RunShellCommand("monkey -p", bundleId, "-c android.intent.category.LAUNCHER 1"); err != nil { + return err + } + if strings.Contains(sOutput, "monkey aborted") { + return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) + } + + if len(launchOpt) != 0 { + var ce error + exists := func(ud WebDriver) (bool, error) { + for _, opt := range launchOpt { + if waitForComplete, ok := opt["androidBySelector"]; ok { + for _, e := range waitForComplete.([]BySelector) { + _, ce = ud.FindElement(e) + if ce == nil { + return true, nil + } + } + } + } + return false, nil + } + if err = ud.WaitWithTimeoutAndInterval(exists, 45, 1); err != nil { + return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + } + } + return +} + +func (ud *uiaDriver) AppLaunchUnattached(bundleId string) (err error) { + // TODO + return errDriverNotImplemented +} + +// Dispose corresponds to the command: +// adb -s $serial forward --remove $localPort +func (ud *uiaDriver) Dispose() (err error) { + if err = ud.check(); err != nil { + return err + } + if ud.localPort == 0 { + return nil + } + return ud.adbDevice.ForwardKill(ud.localPort) +} + +func (ud *uiaDriver) AppTerminate(bundleId string) (successful bool, err error) { + if err = ud.check(); err != nil { + return false, err + } + + _, err = ud.adbDevice.RunShellCommand("am force-stop", bundleId) + return err == nil, err +} + +func (ud *uiaDriver) AppActivate(bundleId string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) AppDeactivate(second float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) AppAuthReset(resource ProtectedResource) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error { + return ud.TapFloat(float64(x), float64(y)) +} + +func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { // register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap")) data := map[string]interface{}{ "x": x, "y": y, } - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/tap") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap") return } -func (d *uiaDriver) TapPoint(point Point) (err error) { - return d.Tap(point.X, point.Y) +func (ud *uiaDriver) DoubleTap(x, y int) error { + return ud.DoubleTapFloat(float64(x), float64(y)) } -func (d *uiaDriver) TapPointF(point PointF) (err error) { - return d.TapFloat(point.X, point.Y) +func (ud *uiaDriver) DoubleTapFloat(x, y float64) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { +func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) { + return ud.TouchAndHoldFloat(float64(x), float64(y), second...) +} + +func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) { + if len(second) == 0 { + second = []float64{1.0} + } + // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + "duration": int(second[0] * 1000), + }, + } + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/longclick") + return +} + +func (ud *uiaDriver) _drag(data map[string]interface{}) (err error) { + // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/drag") + return +} + +// Drag performs a swipe from one coordinate to another coordinate. You can control +// the smoothness and speed of the swipe by specifying the number of steps. +// Each step execution is throttled to 5 milliseconds per step, so for a 100 +// steps, the swipe will take around 0.5 seconds to complete. +func (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error { + return ud.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) +} + +func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) { + data := map[string]interface{}{ + "startX": fromX, + "startY": fromY, + "endX": toX, + "endY": toY, + } + + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) + } + + if _, ok := data["steps"]; !ok { + data["steps"] = 12 // default steps + } + + return ud._drag(data) +} + +func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) data := map[string]interface{}{ "startX": startX, @@ -569,7 +561,7 @@ func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, el if len(elementID) != 0 { data["elementId"] = elementID[0] } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/perform") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/perform") return } @@ -577,412 +569,52 @@ func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, el // to determine smoothness and speed. Each step execution is throttled to 5ms // per step. So for a 100 steps, the swipe will take about 1/2 second to complete. // `steps` is the number of move steps sent to the system -func (d *uiaDriver) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { - return d.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { + options = append(options, WithPressDuration(0)) + return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } -func (d *uiaDriver) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { - if len(steps) == 0 { - steps = []int{12} +func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { + data := map[string]interface{}{} + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) } - return d._swipe(startX, startY, endX, endY, steps[0]) -} -func (d *uiaDriver) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { - return d.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { - return d.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) _drag(data map[string]interface{}) (err error) { - // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/drag") - return -} - -// Drag performs a swipe from one coordinate to another coordinate. You can control -// the smoothness and speed of the swipe by specifying the number of steps. -// Each step execution is throttled to 5 milliseconds per step, so for a 100 -// steps, the swipe will take around 0.5 seconds to complete. -func (d *uiaDriver) Drag(startX, startY, endX, endY int, steps ...int) (err error) { - return d.DragFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) -} - -func (d *uiaDriver) DragFloat(startX, startY, endX, endY float64, steps ...int) error { - if len(steps) == 0 { - steps = []int{12} + if _, ok := data["steps"]; !ok { + data["steps"] = 12 // default steps } + + return ud._swipe(fromX, fromY, toX, toY, data["steps"].(int)) +} + +func (ud *uiaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error { + return ud.ForceTouchFloat(float64(x), float64(y), pressure, second...) +} + +func (ud *uiaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) PerformW3CActions(actions *W3CActions) (err error) { data := map[string]interface{}{ - "startX": startX, - "startY": startY, - "endX": endX, - "endY": endY, - "steps": steps[0], - } - return d._drag(data) -} - -func (d *uiaDriver) DragPoint(startPoint Point, endPoint Point, steps ...int) error { - return d.Drag(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) DragPointF(startPoint PointF, endPoint PointF, steps ...int) (err error) { - return d.DragFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) TouchLongClick(x, y int, duration ...float64) (err error) { - if len(duration) == 0 { - duration = []float64{1.0} - } - // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - "duration": int(duration[0] * 1000), - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/longclick") - return -} - -func (d *uiaDriver) TouchLongClickPoint(point Point, duration ...float64) (err error) { - return d.TouchLongClick(point.X, point.Y, duration...) -} - -func (d *uiaDriver) SendKeys(text string, isReplace ...bool) (err error) { - if len(isReplace) == 0 { - isReplace = []bool{true} - } - // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) - // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 - data := map[string]interface{}{ - "text": text, - "replace": isReplace[0], - } - _, err = d.httpPOST(data, "/session", d.sessionId, "keys") - return -} - -// PressBack simulates a short press on the BACK button. -func (d *uiaDriver) PressBack() (err error) { - // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) - _, err = d.httpPOST(nil, "/session", d.sessionId, "back") - return -} - -// public class KeyCodeModel extends BaseModel { -// @RequiredField -// public Integer keycode; -// public Integer metastate; -// public Integer flags; -// } -func (d *uiaDriver) LongPressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { - if len(flags) == 0 { - flags = []KeyFlag{KFFromSystem} - } - data := map[string]interface{}{ - "keycode": keyCode, - "metastate": metaState, - "flags": flags[0], - } - // register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/appium/device/long_press_keycode") - return -} - -func (d *uiaDriver) _pressKeyCode(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, - } - if metaState != KMEmpty { - data["metastate"] = metaState - } - if len(flags) != 0 { - data["flags"] = flags[0] - } - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/press_keycode") - return -} - -func (d *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { - if len(flags) == 0 { - flags = []KeyFlag{KFFromSystem} - } - return d._pressKeyCode(keyCode, metaState, KFFromSystem) -} - -// PressKeyCodeAsync simulates a short press using a key code. -func (d *uiaDriver) PressKeyCodeAsync(keyCode KeyCode, metaState ...KeyMeta) (err error) { - if len(metaState) == 0 { - metaState = []KeyMeta{KMEmpty} - } - return d._pressKeyCode(keyCode, metaState[0]) -} - -func (d *uiaDriver) TouchDown(x, y int) (err error) { - // register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/down") - return -} - -func (d *uiaDriver) TouchDownPoint(point Point) error { - return d.TouchDown(point.X, point.Y) -} - -func (d *uiaDriver) TouchUp(x, y int) (err error) { - // register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/up") - return -} - -func (d *uiaDriver) TouchUpPoint(point Point) error { - return d.TouchUp(point.X, point.Y) -} - -func (d *uiaDriver) TouchMove(x, y int) (err error) { - // register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/move") - return -} - -func (d *uiaDriver) TouchMovePoint(point Point) error { - return d.TouchMove(point.X, point.Y) -} - -// OpenNotification opens the notification shade. -func (d *uiaDriver) OpenNotification() (err error) { - // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) - _, err = d.httpPOST(nil, "/session", d.sessionId, "appium/device/open_notifications") - return -} - -func (d *uiaDriver) _flick(data map[string]interface{}) (err error) { - // register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick")) - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/flick") - return -} - -func (d *uiaDriver) Flick(xSpeed, ySpeed int) (err error) { - data := map[string]interface{}{ - "xspeed": xSpeed, - "yspeed": ySpeed, - } - if xSpeed == 0 && ySpeed == 0 { - return errors.New("both 'xSpeed' and 'ySpeed' cannot be zero") - } - - return d._flick(data) -} - -func (d *uiaDriver) _scrollTo(method, selector string, maxSwipes int, elementID ...string) (err error) { - // register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll")) - params := map[string]interface{}{ - "strategy": method, - "selector": selector, - } - if maxSwipes > 0 { - params["maxSwipes"] = maxSwipes - } - data := map[string]interface{}{"params": params} - if len(elementID) != 0 { - data["origin"] = map[string]string{ - legacyWebElementIdentifier: elementID[0], - webElementIdentifier: elementID[0], - } - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/scroll") - return -} - -func (d *uiaDriver) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { - if len(maxSwipes) == 0 { - maxSwipes = []int{0} - } - method, selector := by.getMethodAndSelector() - return d._scrollTo(method, selector, maxSwipes[0]) -} - -type W3CMouseButtonType int - -const ( - MBTLeft W3CMouseButtonType = 0 - MBTMiddle W3CMouseButtonType = 1 - MBTRight W3CMouseButtonType = 2 -) - -func (g *W3CGestures) PointerDown(button ...W3CMouseButtonType) *W3CGestures { - if len(button) == 0 { - button = []W3CMouseButtonType{MBTLeft} - } - *g = append(*g, _newW3CGesture().pointerDown(int(button[0]))) - return g -} - -func (g *W3CGestures) PointerUp(button ...W3CMouseButtonType) *W3CGestures { - if len(button) == 0 { - button = []W3CMouseButtonType{MBTLeft} - } - *g = append(*g, _newW3CGesture().pointerUp(int(button[0]))) - return g -} - -type W3CPointerMoveType string - -const ( - PMTViewport W3CPointerMoveType = "viewport" - PMTPointer W3CPointerMoveType = "pointer" -) - -func (g *W3CGestures) PointerMove(x, y float64, origin interface{}, duration float64, pressure, size float64) *W3CGestures { - val := "" - switch v := origin.(type) { - case string: - val = v - case W3CPointerMoveType: - val = string(v) - case *uiaElement: - val = v.id - default: - val = string(PMTViewport) - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, val, duration, pressure, size)) - return g -} - -func (g *W3CGestures) PointerMoveTo(x, y float64, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTViewport), duration[0]*1000)) - return g -} - -func (g *W3CGestures) PointerMoveRelative(x, y float64, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTPointer), duration[0]*1000)) - return g -} - -func (g *W3CGestures) PointerMouseOver(x, y float64, element *uiaElement, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, element.id, duration[0]*1000)) - return g -} - -type W3CAction map[string]interface{} - -type W3CActionType string - -const ( - _ W3CActionType = "none" - ATKey W3CActionType = "key" - ATPointer W3CActionType = "pointer" -) - -type W3CPointerType string - -const ( - PTMouse W3CPointerType = "mouse" - PTPen W3CPointerType = "pen" - PTTouch W3CPointerType = "touch" -) - -func NewW3CAction(actionType W3CActionType, gestures *W3CGestures, pointerType ...W3CPointerType) W3CAction { - w3cAction := make(W3CAction) - w3cAction["type"] = actionType - w3cAction["actions"] = gestures - if actionType != ATPointer { - return w3cAction - } - - if len(pointerType) == 0 { - pointerType = []W3CPointerType{PTTouch} - } - type W3CItemParameters struct { - PointerType W3CPointerType `json:"pointerType"` - } - w3cAction["parameters"] = W3CItemParameters{PointerType: pointerType[0]} - return w3cAction -} - -func (d *uiaDriver) PerformW3CActions(action W3CAction, acts ...W3CAction) (err error) { - var actionId uint64 = 1 - acts = append([]W3CAction{action}, acts...) - for i := range acts { - item := acts[i] - item["id"] = strconv.FormatUint(actionId, 10) - actionId++ - acts[i] = item - } - data := map[string]interface{}{ - "actions": acts, + "actions": actions, } // register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/actions") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions") return } -type ClipDataType string - -const ClipDataTypePlaintext ClipDataType = "PLAINTEXT" - -func (d *uiaDriver) GetClipboard(contentType ...ClipDataType) (content string, err error) { - if len(contentType) == 0 { - contentType = []ClipDataType{ClipDataTypePlaintext} - } - // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) - data := map[string]interface{}{ - "contentType": contentType[0], - } - var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/get_clipboard"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - content = reply.Value - if data, err := base64.StdEncoding.DecodeString(content); err != nil { - return content, err - } else { - content = string(data) - } - return +func (ud *uiaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label ...string) (err error) { +func (ud *uiaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { lbl := content - if len(label) != 0 { - lbl = label[0] - } + const defaultLabelLen = 10 if len(lbl) > defaultLabelLen { lbl = lbl[:defaultLabelLen] @@ -994,89 +626,132 @@ func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label "content": base64.StdEncoding.EncodeToString([]byte(content)), } // register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard")) - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/set_clipboard") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/set_clipboard") return } -func (d *uiaDriver) AlertAccept(buttonLabel ...string) (err error) { +func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { + if len(contentType) == 0 { + contentType = PasteboardTypePlaintext + } + // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) data := map[string]interface{}{ - "buttonLabel": nil, + "contentType": contentType[0], } - if len(buttonLabel) != 0 { - data["buttonLabel"] = buttonLabel[0] + var rawResp rawResponse + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/get_clipboard"); err != nil { + return + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return + } + + if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil { + raw.Write([]byte(reply.Value)) + } else { + raw.Write(data) } - // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) - _, err = d.httpPOST(data, "/session", d.sessionId, "alert/accept") return } -func (d *uiaDriver) AlertDismiss(buttonLabel ...string) (err error) { +func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) + // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 data := map[string]interface{}{ - "buttonLabel": nil, + "text": text, } - if len(buttonLabel) != 0 { - data["buttonLabel"] = buttonLabel[0] + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) } - // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) - _, err = d.httpPOST(data, "/session", d.sessionId, "alert/dismiss") + + if _, ok := data["isReplace"]; !ok { + data["isReplace"] = true // default true + } + + _, err = ud.httpPOST(data, "/session", ud.sessionId, "keys") return } -func (d *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (err error) { - data := map[string]interface{}{ - "settings": settings, - } - // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/settings") +func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) PressButton(devBtn DeviceButton) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { + // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) + _, err = ud.httpPOST(nil, "/session", ud.sessionId, "appium/device/open_notifications") return } -func (d *uiaDriver) SetOrientation(orientation Orientation) (err error) { - data := map[string]interface{}{ - "orientation": orientation, +func (ud *uiaDriver) SiriActivate(text string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) SiriOpenUrl(url string) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Orientation() (orientation Orientation, err error) { + // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "orientation"); err != nil { + return "", err } - // register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation")) - _, err = d.httpPOST(data, "/session", d.sessionId, "orientation") + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + orientation = reply.Value return } -// SetRotation -// `x` and `y` are ignored. We only care about `z` -// 0/90/180/270 -func (d *uiaDriver) SetRotation(rotation Rotation) (err error) { - data := map[string]interface{}{ - "z": rotation.Z, +func (ud *uiaDriver) SetOrientation(orientation Orientation) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) Rotation() (rotation Rotation, err error) { + // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "rotation"); err != nil { + return Rotation{}, err } - // register(postHandler, new SetRotation("/wd/hub/session/:sessionId/rotation")) - _, err = d.httpPOST(data, "/session", d.sessionId, "rotation") + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + + rotation = reply.Value return } -type NetworkType int - -const ( - NetworkTypeWifi NetworkType = 2 - - // NetworkTypeNone NetworkType = iota - // NetworkTypeAirplane - // NetworkTypeWifi - // _ - // NetworkTypeData - // _ - // NetworkTypeAll -) - -// NetworkConnection always turn on -func (d *uiaDriver) NetworkConnection(networkType NetworkType) (err error) { - // register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection")) - data := map[string]interface{}{ - "type": networkType, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "network_connection") - return +func (ud *uiaDriver) SetRotation(rotation Rotation) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []*uiaElement, err error) { +func (ud *uiaDriver) MatchTouchID(isMatch bool) (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []WebElement, err error) { // register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements")) data := map[string]interface{}{ "strategy": method, @@ -1086,7 +761,7 @@ func (d *uiaDriver) _findElements(method, selector string, elementID ...string) data["context"] = elementID[0] } var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/elements"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { return nil, err } reply := new(struct{ Value []map[string]string }) @@ -1096,18 +771,19 @@ func (d *uiaDriver) _findElements(method, selector string, elementID ...string) if len(reply.Value) == 0 { return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) } - elements = make([]*uiaElement, len(reply.Value)) + elements = make([]WebElement, len(reply.Value)) for i, elem := range reply.Value { var id string if id = elementIDFromValue(elem); id == "" { return nil, fmt.Errorf("invalid element returned: %+v", reply) } - elements[i] = &uiaElement{parent: d, id: id} + uie := WebElement(uiaElement{parent: ud, id: id}) + elements[i] = uie } return } -func (d *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { +func (ud *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { // register(postHandler, new FindElement("/wd/hub/session/:sessionId/element")) data := map[string]interface{}{ "strategy": method, @@ -1117,7 +793,7 @@ func (d *uiaDriver) _findElement(method, selector string, elementID ...string) ( data["context"] = elementID[0] } var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/element"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/element"); err != nil { return nil, err } reply := new(struct{ Value map[string]string }) @@ -1131,45 +807,122 @@ func (d *uiaDriver) _findElement(method, selector string, elementID ...string) ( if id = elementIDFromValue(reply.Value); id == "" { return nil, fmt.Errorf("invalid element returned: %+v", reply) } - elem = &uiaElement{parent: d, id: id} + elem = &uiaElement{parent: ud, id: id} return } -func (d *uiaDriver) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { - return d._findElements(by.getMethodAndSelector()) +func (ud *uiaDriver) ActiveElement() (element WebElement, err error) { + // TODO + return element, errDriverNotImplemented } -func (d *uiaDriver) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { - return d._findElement(by.getMethodAndSelector()) +func (ud *uiaDriver) FindElement(by BySelector) (element WebElement, err error) { + return ud._findElement(by.getUsingAndValue()) } -func (d *uiaDriver) ActiveElement() (elem *uiaElement, err error) { - // register(getHandler, new ActiveElement("/wd/hub/session/:sessionId/element/active")) +func (ud *uiaDriver) FindElements(by BySelector) (elements []WebElement, err error) { + // [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)] + using, value := by.getUsingAndValue() + data := map[string]interface{}{ + "using": using, + "value": value, + } var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "/element/active"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { return nil, err } - reply := new(struct{ Value map[string]string }) + var elementIDs []string + if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil { + if errors.Is(err, errNoSuchElement) { + return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value) + } + return nil, err + } + elements = make([]WebElement, len(elementIDs)) + for i := range elementIDs { + elements[i] = WebElement(uiaElement{parent: ud, id: elementIDs[i]}) + } + return +} + +func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { + // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) if err = json.Unmarshal(rawResp, reply); err != nil { return nil, err } - if len(reply.Value) == 0 { - return nil, errors.New("no such element") + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err } - var id string - if id = elementIDFromValue(reply.Value); id == "" { - return nil, fmt.Errorf("invalid element returned: %+v", reply) - } - elem = &uiaElement{parent: d, id: id} + + raw = bytes.NewBuffer(decodeStr) return } -type AndroidCondition func(d *uiaDriver) (bool, error) +func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) { + // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "source"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } -func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval time.Duration) (err error) { + source = reply.Value + return +} + +func (ud *uiaDriver) AccessibleSource() (source string, err error) { + // TODO + return source, errDriverNotImplemented +} + +func (ud *uiaDriver) HealthCheck() (err error) { + // TODO + return errDriverNotImplemented +} + +func (ud *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { + // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) + var rawResp rawResponse + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + settings = reply.Value + return +} + +func (ud *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { + data := map[string]interface{}{ + "settings": settings, + } + // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/settings") + return +} + +func (ud *uiaDriver) IsHealthy() (healthy bool, err error) { + // TODO + return healthy, errDriverNotImplemented +} + +func (ud *uiaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { startTime := time.Now() for { - done, err := condition(d) + done, err := condition(ud) if err != nil { return err } @@ -1184,20 +937,10 @@ func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, time } } -// WaitWithTimeoutAndInterval waits for the condition to evaluate to true. -func (d *uiaDriver) WaitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval float64) (err error) { - dTimeout := time.Millisecond * time.Duration(timeout*1000) - dInterval := time.Millisecond * time.Duration(interval*1000) - return d._waitWithTimeoutAndInterval(condition, dTimeout, dInterval) +func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error { + return ud.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } -// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. -func (d *uiaDriver) WaitWithTimeout(condition AndroidCondition, timeout float64) error { - dTimeout := time.Millisecond * time.Duration(timeout*1000) - return d._waitWithTimeoutAndInterval(condition, dTimeout, DefaultWaitInterval) -} - -// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. -func (d *uiaDriver) Wait(condition AndroidCondition) error { - return d._waitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +func (ud *uiaDriver) Wait(condition Condition) error { + return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } diff --git a/hrp/internal/uixt/android_elment.go b/hrp/internal/uixt/android_elment.go index 533217f0..f14fac40 100644 --- a/hrp/internal/uixt/android_elment.go +++ b/hrp/internal/uixt/android_elment.go @@ -4,73 +4,179 @@ import ( "bytes" "encoding/base64" "encoding/json" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) +var errElementNotImplemented = errors.New("element method not implemented") + type uiaElement struct { parent *uiaDriver id string } -func (e *uiaElement) Text() (text string, err error) { - // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/text"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - text = reply.Value +func (ue uiaElement) Click() (err error) { + // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) + _, err = ue.parent.httpPOST(nil, "/session", ue.parent.sessionId, "/element", ue.id, "/click") return } -func (e *uiaElement) GetAttribute(name string) (attribute string, err error) { - // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/attribute", name); err != nil { - return "", err +func (ue uiaElement) SendKeys(text string, isReplace ...int) (err error) { + if len(isReplace) == 0 { + isReplace = []int{1} } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) + // https://github.com/appium/appium-uiutomator2-server/blob/master/app/src/main/java/io/appium/uiutomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + "replace": isReplace[0] == 1, } - attribute = reply.Value + _, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") return } -func (e *uiaElement) ContentDescription() (name string, err error) { - // register(getHandler, new GetName("/wd/hub/session/:sessionId/element/:id/name")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/name"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - name = reply.Value +func (ue uiaElement) Clear() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Tap(x, y int) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TapFloat(x, y float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) DoubleTap() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TouchAndHold(second ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TwoFingerTap() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { + //Todo: implement + log.Fatal().Msg("not support") return } -func (e *uiaElement) Size() (size Size, err error) { - // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/size"); err != nil { - return Size{-1, -1}, err - } - reply := new(struct{ Value Size }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{-1, -1}, err - } - size = reply.Value - return +func (ue uiaElement) ForceTouch(pressure float64, second ...float64) (err error) { + // TODO + return errElementNotImplemented } -func (e *uiaElement) Rect() (rect Rect, err error) { +func (ue uiaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Drag(fromX, fromY, toX, toY int, steps ...float64) (err error) { + return ue.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), steps...) +} + +func (ue uiaElement) DragFloat(fromX, fromY, toX, toY float64, steps ...float64) (err error) { + if len(steps) == 0 { + steps = []float64{12 * 10} + } else { + steps[0] = 12 * 10 + } + data := map[string]interface{}{ + "elementId": ue.id, + "endX": toX, + "endY": toY, + "steps": steps[0], + } + return ue.parent._drag(data) +} + +func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error { + return ue.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY)) +} + +func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { + return ue.parent._swipe(fromX, fromY, toX, toY, 12, ue.id) +} + +func (ue uiaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Pinch(scale, velocity float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) Rotate(rotation float64, velocity ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) scroll(data interface{}) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollElementByName(name string) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollElementByPredicate(predicate string) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollToVisible() (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) ScrollDirection(direction Direction, distance ...float64) (err error) { + // TODO + return errElementNotImplemented +} + +func (ue uiaElement) FindElement(by BySelector) (element WebElement, err error) { + method, selector := by.getMethodAndSelector() + return ue.parent._findElement(method, selector, ue.id) +} + +func (ue uiaElement) FindElements(by BySelector) (elements []WebElement, err error) { + method, selector := by.getMethodAndSelector() + return ue.parent._findElements(method, selector, ue.id) +} + +func (ue uiaElement) FindVisibleCells() (elements []WebElement, err error) { + // TODO + return elements, errElementNotImplemented +} + +func (ue uiaElement) Rect() (rect Rect, err error) { // register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect")) var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/rect"); err != nil { + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/rect"); err != nil { return Rect{}, err } reply := new(struct{ Value Rect }) @@ -81,13 +187,103 @@ func (e *uiaElement) Rect() (rect Rect, err error) { return } -func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { +func (ue uiaElement) Location() (point Point, err error) { + // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/location"); err != nil { + return Point{-1, -1}, err + } + reply := new(struct{ Value Point }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Point{-1, -1}, err + } + point = reply.Value + return +} + +func (ue uiaElement) Size() (size Size, err error) { + // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/size"); err != nil { + return Size{-1, -1}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{-1, -1}, err + } + size = reply.Value + return +} + +func (ue uiaElement) Text() (text string, err error) { + // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + text = reply.Value + return +} + +func (ue uiaElement) Type() (elemType string, err error) { + // TODO + return elemType, errElementNotImplemented +} + +func (ue uiaElement) IsEnabled() (enabled bool, err error) { + // TODO + return enabled, errElementNotImplemented +} + +func (ue uiaElement) IsDisplayed() (displayed bool, err error) { + // TODO + return displayed, errElementNotImplemented +} + +func (ue uiaElement) IsSelected() (selected bool, err error) { + // TODO + return selected, errElementNotImplemented +} + +func (ue uiaElement) IsAccessible() (accessible bool, err error) { + // TODO + return accessible, errElementNotImplemented +} + +func (ue uiaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) { + // TODO + return isAccessibilityContainer, errElementNotImplemented +} + +func (ue uiaElement) GetAttribute(attr ElementAttribute) (value string, err error) { + // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) + var rawResp rawResponse + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/attribute", attr.getAttributeName()); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + value = reply.Value + return +} + +func (ue uiaElement) UID() (uid string) { + return ue.id +} + +func (ue uiaElement) Screenshot() (raw *bytes.Buffer, err error) { // W3C endpoint // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/element/:id/screenshot")) // JSONWP endpoint // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/screenshot/:id")) var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/screenshot"); err != nil { + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/screenshot"); err != nil { return nil, err } reply := new(struct{ Value string }) @@ -103,136 +299,3 @@ func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { raw = bytes.NewBuffer(decodeStr) return } - -func (e *uiaElement) Location() (point Point, err error) { - // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/location"); err != nil { - return Point{-1, -1}, err - } - reply := new(struct{ Value Point }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Point{-1, -1}, err - } - point = reply.Value - return -} - -func (e *uiaElement) Click() (err error) { - // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/click") - return -} - -func (e *uiaElement) Clear() (err error) { - // register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/clear") - return -} - -func (e *uiaElement) SendKeys(text string, isReplace ...bool) (err error) { - if len(isReplace) == 0 { - isReplace = []bool{true} - } - // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) - // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 - data := map[string]interface{}{ - "text": text, - "replace": isReplace[0], - } - _, err = e.parent.httpPOST(data, "/session", e.parent.sessionId, "/element", e.id, "/value") - return -} - -func (e *uiaElement) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { - method, selector := by.getMethodAndSelector() - return e.parent._findElements(method, selector, e.id) -} - -func (e *uiaElement) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { - method, selector := by.getMethodAndSelector() - return e.parent._findElement(method, selector, e.id) -} - -func (e *uiaElement) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { - return e.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) -} - -func (e *uiaElement) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { - if len(steps) == 0 { - steps = []int{12} - } - return e.parent._swipe(startX, startY, endX, endY, steps[0], e.id) -} - -func (e *uiaElement) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { - return e.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { - return e.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) Drag(endX, endY int, steps ...int) (err error) { - return e.DragFloat(float64(endX), float64(endY), steps...) -} - -func (e *uiaElement) DragFloat(endX, endY float64, steps ...int) error { - if len(steps) == 0 { - steps = []int{12 * 10} - } else { - steps[0] = 12 * 10 - } - data := map[string]interface{}{ - "elementId": e.id, - "endX": endX, - "endY": endY, - "steps": steps[0], - } - return e.parent._drag(data) -} - -func (e *uiaElement) DragPoint(endPoint Point, steps ...int) error { - return e.Drag(endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) DragPointF(endPoint PointF, steps ...int) (err error) { - return e.DragFloat(endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) DragTo(destElem *uiaElement, steps ...int) error { - if len(steps) == 0 { - steps = []int{12} - } - data := map[string]interface{}{ - "elementId": e.id, - "destElId": destElem.id, - "steps": steps[0], - } - return e.parent._drag(data) -} - -func (e *uiaElement) Flick(xOffset, yOffset, speed int) (err error) { - data := map[string]interface{}{ - legacyWebElementIdentifier: e.id, - webElementIdentifier: e.id, - "xoffset": xOffset, - "yoffset": yOffset, - "speed": speed, - } - return e.parent._flick(data) -} - -func (e *uiaElement) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { - if len(maxSwipes) == 0 { - maxSwipes = []int{0} - } - method, selector := by.getMethodAndSelector() - return e.parent._scrollTo(method, selector, maxSwipes[0], e.id) -} - -func (e *uiaElement) ScrollToElement(element *uiaElement) (err error) { - // register(postHandler, new ScrollToElement("/wd/hub/session/:sessionId/appium/element/:id/scroll_to/:id2")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/appium/element", e.id, "/scroll_to", element.id) - return -} diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go index 2efa9c66..08a71c17 100644 --- a/hrp/internal/uixt/android_test.go +++ b/hrp/internal/uixt/android_test.go @@ -21,11 +21,11 @@ func TestDriver_NewSession(t *testing.T) { "firstMatch": []interface{}{firstMatchEntry}, "alwaysMatch": struct{}{}, } - sessionID, err := driver.NewSession(caps) + session, err := driver.NewSession(caps) if err != nil { t.Fatal(err) } - if len(sessionID) == 0 { + if len(session.SessionId) == 0 { t.Fatal("should not be empty") } } @@ -45,7 +45,7 @@ func TestDriver_Quit(t *testing.T) { t.Fatal(err) } - if err = driver.Quit(); err != nil { + if err = driver.Close(); err != nil { t.Fatal(err) } } @@ -147,7 +147,7 @@ func TestDriver_DeviceSize(t *testing.T) { t.Fatal(err) } - deviceSize, err := driver.DeviceSize() + deviceSize, err := driver.WindowSize() if err != nil { t.Fatal(err) } @@ -169,20 +169,6 @@ func TestDriver_Source(t *testing.T) { t.Log(source) } -func TestDriver_StatusBarHeight(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - statusBarHeight, err := driver.StatusBarHeight() - if err != nil { - t.Fatal(err) - } - - t.Log(statusBarHeight) -} - func TestDriver_BatteryInfo(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) if err != nil { @@ -220,7 +206,7 @@ func TestDriver_DeviceScaleRatio(t *testing.T) { t.Fatal(err) } - scaleRatio, err := driver.DeviceScaleRatio() + scaleRatio, err := driver.Scale() if err != nil { t.Fatal(err) } @@ -275,17 +261,6 @@ func TestDriver_Tap(t *testing.T) { t.Fatal(err) } time.Sleep(time.Second) - - err = driver.TapPoint(Point{X: 150, Y: 340}) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - - err = driver.TapPointF(PointF{X: 60.5, Y: 125.5}) - if err != nil { - t.Fatal(err) - } } func TestDriver_Swipe(t *testing.T) { @@ -294,7 +269,7 @@ func TestDriver_Swipe(t *testing.T) { t.Fatal(err) } - err = driver.Swipe(400, 1000, 400, 500, 10) + err = driver.Swipe(400, 1000, 400, 500) if err != nil { t.Fatal(err) } @@ -303,20 +278,6 @@ func TestDriver_Swipe(t *testing.T) { if err != nil { t.Fatal(err) } - - startPoint := Point{400, 1000} - endPoint := Point{400, 500} - err = driver.SwipePoint(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } - - startPointF := PointF{400, 555.5} - endPointF := PointF{400, 1255.5} - err = driver.SwipePointF(startPointF, endPointF) - if err != nil { - t.Fatal(err) - } } func TestDriver_Drag(t *testing.T) { @@ -325,50 +286,17 @@ func TestDriver_Drag(t *testing.T) { t.Fatal(err) } - err = driver.Drag(400, 260, 400, 500, 10) + err = driver.Drag(400, 260, 400, 500) if err != nil { t.Fatal(err) } time.Sleep(time.Millisecond * 200) - err = driver.DragFloat(400, 501.5, 400, 261.5, 10) + err = driver.DragFloat(400, 501.5, 400, 261.5) if err != nil { t.Fatal(err) } time.Sleep(time.Millisecond * 200) - - startPoint := Point{400, 260} - endPoint := Point{400, 500} - err = driver.DragPoint(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Millisecond * 200) - - startPointF := PointF{400.5, 501.5} - endPointF := PointF{400.5, 261.5} - err = driver.DragPointF(startPointF, endPointF) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchLongClick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.TouchLongClick(400, 260, 1.2222) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Millisecond * 200) - - err = driver.TouchLongClickPoint(Point{X: 400, Y: 260}) - if err != nil { - t.Fatal(err) - } } func TestDriver_SendKeys(t *testing.T) { @@ -383,7 +311,7 @@ func TestDriver_SendKeys(t *testing.T) { } time.Sleep(time.Second * 2) - err = driver.SendKeys("def", false) + err = driver.SendKeys("def") if err != nil { t.Fatal(err) } @@ -396,270 +324,270 @@ func TestDriver_SendKeys(t *testing.T) { } } -func TestDriver_PressBack(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestDriver_PressBack(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressBack() +// if err != nil { +// t.Fatal(err) +// } +//} - err = driver.PressBack() - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_PressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressKeyCodeAsync(KCx) +// if err != nil { +// t.Fatal(err) +// } +// err = driver.PressKeyCodeAsync(KCx, KMCapLocked) +// if err != nil { +// t.Fatal(err) +// } +// // err = driver.PressKeyCodeAsync(KCExplorer) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// err = driver.PressKeyCode(KCExplorer, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} -func TestDriver_PressKeyCode(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestDriver_LongPressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.LongPressKeyCode(KCAt, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchDown(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp := func() { +// err = driver.TouchUp(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // _ = driver.TapPoint(Point{400, 500}) +// doTouchUp() +// +// err = driver.TouchDownPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp() +//} +// +//func TestDriver_TouchUp(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // err = driver.TouchUp(400, 260) +// err = driver.TouchUpPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchMove(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchDown := func(x, y int) { +// err = driver.TouchDown(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchUp := func(x, y int) { +// err = driver.TouchUp(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchDown(400, 260) +// +// err = driver.TouchMove(400, 500) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 500) +// +// doTouchDown(400, 500) +// +// err = driver.TouchMove(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 260) +//} +// +//func TestDriver_OpenNotification(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.OpenNotification() +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.Flick(50, -100) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.ScrollTo(BySelector{ClassName: "android.widget.SeekBar"}) +// if err != nil { +// t.Fatal(err) +// } +//} - err = driver.PressKeyCodeAsync(KCx) - if err != nil { - t.Fatal(err) - } - err = driver.PressKeyCodeAsync(KCx, KMCapLocked) - if err != nil { - t.Fatal(err) - } - // err = driver.PressKeyCodeAsync(KCExplorer) - // if err != nil { - // t.Fatal(err) - // } - - err = driver.PressKeyCode(KCExplorer, KMEmpty) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_LongPressKeyCode(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.LongPressKeyCode(KCAt, KMEmpty) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchDown(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - doTouchUp := func() { - err = driver.TouchUp(400, 260) - if err != nil { - t.Fatal(err) - } - } - - err = driver.TouchDown(400, 260) - if err != nil { - t.Fatal(err) - } - - // _ = driver.TapPoint(Point{400, 500}) - doTouchUp() - - err = driver.TouchDownPoint(Point{400, 260}) - if err != nil { - t.Fatal(err) - } - - doTouchUp() -} - -func TestDriver_TouchUp(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.TouchDown(400, 260) - if err != nil { - t.Fatal(err) - } - - // err = driver.TouchUp(400, 260) - err = driver.TouchUpPoint(Point{400, 260}) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchMove(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - doTouchDown := func(x, y int) { - err = driver.TouchDown(x, y) - if err != nil { - t.Fatal(err) - } - } - - doTouchUp := func(x, y int) { - err = driver.TouchUp(x, y) - if err != nil { - t.Fatal(err) - } - } - - doTouchDown(400, 260) - - err = driver.TouchMove(400, 500) - if err != nil { - t.Fatal(err) - } - - doTouchUp(400, 500) - - doTouchDown(400, 500) - - err = driver.TouchMove(400, 260) - if err != nil { - t.Fatal(err) - } - - doTouchUp(400, 260) -} - -func TestDriver_OpenNotification(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.OpenNotification() - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_Flick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.Flick(50, -100) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_ScrollTo(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.ScrollTo(AndroidBySelector{ClassName: "android.widget.SeekBar"}) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_MultiPointerGesture(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) - gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) - gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) - _ = gesture3 - - // err = driver.MultiPointerGesture(gesture1, gesture2) - err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_PerformW3CActions(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) - // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) - // err = driver.PerformW3CActions(actionKey) - // if err != nil { - // t.Fatal(err) - // } - - // var queryField map[string]string - // queryField = make(map[string]string) - // { - // queryField = map[string]string{ - // "a": "", - // } - // } - - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) - if err != nil { - t.Fatal(err) - } - // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) - // actionPointer := NewW3CAction(ATPointer, - // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). - // PointerMove(0, 0, elem.id).Pause(2). - // PointerMove(20, 0, "pointer").Pause(2). - // PointerUp(), - // ) - actionPointer := NewW3CAction(ATPointer, - NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). - PointerMouseOver(0, 0, elem). - PointerMoveRelative(20, 0).PointerUp()) - err = driver.PerformW3CActions(actionPointer) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_GetClipboard(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - text, err := driver.GetClipboard() - if err != nil { - t.Fatal(err) - } - t.Log(text) -} - -func TestDriver_SetClipboard(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - content := "test123" - err = driver.SetClipboard(ClipDataTypePlaintext, content) - if err != nil { - t.Fatal(err) - } - - text, err := driver.GetClipboard() - if err != nil { - t.Fatal(err) - } - if text != content { - t.Fatal("should be the same") - } -} +//func TestDriver_MultiPointerGesture(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) +// gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) +// gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) +// _ = gesture3 +// +// // err = driver.MultiPointerGesture(gesture1, gesture2) +// err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_PerformW3CActions(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) +// // err = driver.PerformW3CActions(actionKey) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// // var queryField map[string]string +// // queryField = make(map[string]string) +// // { +// // queryField = map[string]string{ +// // "a": "", +// // } +// // } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) +// // actionPointer := NewW3CAction(ATPointer, +// // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). +// // PointerMove(0, 0, elem.id).Pause(2). +// // PointerMove(20, 0, "pointer").Pause(2). +// // PointerUp(), +// // ) +// actionPointer := NewW3CAction(ATPointer, +// NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). +// PointerMouseOver(0, 0, elem). +// PointerMoveRelative(20, 0).PointerUp()) +// err = driver.PerformW3CActions(actionPointer) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_GetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// t.Log(text) +//} +// +//func TestDriver_SetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// content := "test123" +// err = driver.SetClipboard(ClipDataTypePlaintext, content) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// if text != content { +// t.Fatal("should be the same") +// } +//} func TestDriver_AlertAccept(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -687,33 +615,33 @@ func TestDriver_AlertDismiss(t *testing.T) { } } -func TestDriver_SetAppiumSettings(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - appiumSettings, err := driver.GetAppiumSettings() - if err != nil { - t.Fatal(err) - } - sdopd := appiumSettings["shutdownOnPowerDisconnect"] - t.Log("shutdownOnPowerDisconnect:", sdopd) - - err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) - if err != nil { - t.Fatal(err) - } - - appiumSettings, err = driver.GetAppiumSettings() - if err != nil { - t.Fatal(err) - } - if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { - t.Fatal("should not be equal") - } - t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) -} +//func TestDriver_SetAppiumSettings(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err := driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// sdopd := appiumSettings["shutdownOnPowerDisconnect"] +// t.Log("shutdownOnPowerDisconnect:", sdopd) +// +// err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err = driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { +// t.Fatal("should not be equal") +// } +// t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) +//} func TestDriver_SetOrientation(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -741,17 +669,17 @@ func TestDriver_SetRotation(t *testing.T) { } } -func TestDriver_NetworkConnection(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.NetworkConnection(NetworkTypeWifi) - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_NetworkConnection(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.NetworkConnection(NetworkTypeWifi) +// if err != nil { +// t.Fatal(err) +// } +//} func TestDriver_FindElement(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -759,12 +687,12 @@ func TestDriver_FindElement(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/content"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/content"}) if err != nil { t.Fatal(err) } - - t.Log(elem.GetAttribute("class")) + e := ElementAttribute{}.WithLabel("class") + t.Log(elem.GetAttribute(e)) } func TestDriver_FindElements(t *testing.T) { @@ -773,8 +701,8 @@ func TestDriver_FindElements(t *testing.T) { t.Fatal(err) } - // elements, err := driver.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) - elements, err := driver.FindElements(AndroidBySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) + // elements, err := driver.FindElements(BySelector{ResourceIdID: "com.android.settings:id/title"}) + elements, err := driver.FindElements(BySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) if err != nil { t.Fatal(err) } @@ -786,12 +714,12 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { if err != nil { t.Fatal(err) } - element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) + element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) if err != nil { t.Fatal(err) } - elem, err := element.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) + elem, err := element.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) if err != nil { t.Fatal(err) } @@ -808,8 +736,8 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { t.Fatal(err) } - by := AndroidBySelector{UiAutomator: "new UiSelector().text(\"科技\");"} - exists := func(d *uiaDriver) (bool, error) { + by := BySelector{UiAutomator: "new UiSelector().text(\"科技\");"} + exists := func(d WebDriver) (bool, error) { element, err = d.FindElement(by) if err == nil { return true, nil @@ -817,7 +745,7 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { return false, nil } - err = driver.WaitWithTimeoutAndInterval(exists, 1, 0.1) + err = driver.WaitWithTimeoutAndInterval(exists, 1, 1) if err != nil { t.Fatal(err) } @@ -833,25 +761,25 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { } } -func TestDriver_ActiveElement(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = driver.Dispose() - }() - - element, err := driver.ActiveElement() - if err != nil { - t.Fatal(err) - } - - if err = element.SendKeys("test"); err != nil { - t.Fatal(err) - } -} +//func TestDriver_ActiveElement(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer func() { +// _ = driver.Dispose() +// }() +// +// element, err := driver.ActiveElement() +// if err != nil { +// t.Fatal(err) +// } +// +// if err = element.SendKeys("test"); err != nil { +// t.Fatal(err) +// } +//} func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { uiSelector := NewUiSelectorHelper().Text("a").String() @@ -898,38 +826,38 @@ func TestDeviceList(t *testing.T) { } } -func TestAndroidNewUSBDriver(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() +//func TestAndroidNewUSBDriver(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// ready, err := driver.Status() +// if err != nil { +// t.Fatal(err) +// } +// if !ready { +// t.Fatal("should be 'true'") +// } +//} - ready, err := driver.Status() - if err != nil { - t.Fatal(err) - } - if !ready { - t.Fatal("should be 'true'") - } -} - -func TestDriver_ActiveAppPackageName(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - appPackageName, err := driver.ActiveAppPackageName() - if err != nil { - t.Fatal(err) - } - - t.Log(appPackageName) -} +//func TestDriver_ActiveAppPackageName(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// appPackageName, err := driver.ActiveAppPackageName() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(appPackageName) +//} func TestDriver_AppLaunch(t *testing.T) { device, _ := NewAndroidDevice() @@ -937,10 +865,9 @@ func TestDriver_AppLaunch(t *testing.T) { if err != nil { t.Fatal(err) } - defer driver.Dispose() - // err = driver.AppLaunch("tv.danmaku.bili", AndroidBySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) - err = driver.AppLaunch("com.android.settings", AndroidBySelector{ResourceIdID: "android:id/list"}) + // err = driver.AppLaunch("tv.danmaku.bili", BySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) + err = driver.AppLaunch("com.android.settings", AppLaunchOption{}.WithAndroidBySelector(AndroidBySelector{ResourceIdID: "android:id/list"})) if err != nil { t.Fatal(err) } @@ -960,63 +887,63 @@ func TestDriver_AppTerminate(t *testing.T) { } defer driver.Dispose() - err = driver.AppTerminate("tv.danmaku.bili") + _, err = driver.AppTerminate("tv.danmaku.bili") if err != nil { t.Fatal(err) } } -func TestNewWiFiDriver(t *testing.T) { - device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) - driver, err := device.NewHTTPDriver(nil) - if err != nil { - t.Fatal(err) - } +//func TestNewWiFiDriver(t *testing.T) { +// device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) +// driver, err := device.NewHTTPDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// +// // SetDebug(false, true) +// _, err = driver.ActiveAppActivity() +// if err != nil { +// t.Fatal(err) +// } +//} - // SetDebug(false, true) - _, err = driver.ActiveAppActivity() - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_AppInstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") +// if err != nil { +// t.Fatal(err) +// } +//} -func TestDriver_AppInstall(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_AppUninstall(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - err = driver.AppUninstall("cn.xuexi.android") - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_AppUninstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppUninstall("cn.xuexi.android") +// if err != nil { +// t.Fatal(err) +// } +//} func TestBySelector_getMethodAndSelector(t *testing.T) { testVal := "test id" - bySelector := AndroidBySelector{ResourceIdID: testVal} + bySelector := BySelector{ResourceIdID: testVal} method, selector := bySelector.getMethodAndSelector() if method != "id" || selector != testVal { t.Fatal(method, "=", selector) } - bySelector = AndroidBySelector{ContentDescription: testVal} + bySelector = BySelector{ContentDescription: testVal} method, selector = bySelector.getMethodAndSelector() if method != "accessibility id" || selector != testVal { t.Fatal(method, "=", selector) @@ -1029,7 +956,7 @@ func TestElement_Text(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1048,12 +975,13 @@ func TestElement_GetAttribute(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } - attribute, err := elem.GetAttribute("class") + e := ElementAttribute{}.WithName("class") + attribute, err := elem.GetAttribute(e) if err != nil { t.Fatal(err) } @@ -1061,24 +989,24 @@ func TestElement_GetAttribute(t *testing.T) { t.Log(attribute) } -func TestElement_ContentDescription(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) - if err != nil { - t.Fatal(err) - } - - name, err := elem.ContentDescription() - if err != nil { - t.Fatal(err) - } - - t.Log(name) -} +//func TestElement_ContentDescription(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// +// name, err := elem.ContentDescription() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(name) +//} func TestElement_Size(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -1086,7 +1014,7 @@ func TestElement_Size(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) if err != nil { t.Fatal(err) } @@ -1105,7 +1033,7 @@ func TestElement_Rect(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1124,7 +1052,7 @@ func TestElement_Screenshot(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1143,7 +1071,7 @@ func TestElement_Location(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1162,7 +1090,7 @@ func TestElement_Click(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/title"}) if err != nil { t.Fatal(err) } @@ -1179,7 +1107,7 @@ func TestElement_Clear(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) if err != nil { t.Fatal(err) } @@ -1196,7 +1124,7 @@ func TestElement_SendKeys(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) if err != nil { t.Fatal(err) } @@ -1204,7 +1132,7 @@ func TestElement_SendKeys(t *testing.T) { // return // err = elem.SendKeys("abc") - err = elem.SendKeys("456", false) + err = elem.SendKeys("456", 0) if err != nil { t.Fatal(err) } @@ -1216,12 +1144,12 @@ func TestElement_FindElements(t *testing.T) { t.Fatal(err) } - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) if err != nil { t.Fatal(err) } - elements, err := parentElem.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/category"}) + elements, err := parentElem.FindElements(BySelector{ResourceIdID: "com.android.settings:id/category"}) if err != nil { t.Fatal(err) } @@ -1234,12 +1162,12 @@ func TestElement_FindElement(t *testing.T) { t.Fatal(err) } - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) if err != nil { t.Fatal(err) } - elem, err := parentElem.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := parentElem.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1253,7 +1181,7 @@ func TestElement_Swipe(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1274,111 +1202,104 @@ func TestElement_Swipe(t *testing.T) { if err != nil { t.Fatal(err) } - - startPoint := PointF{X: float64(rect.X + rect.Width/20 + 30), Y: float64(startY / 2)} - endPoint := PointF{X: startPoint.X, Y: startPoint.Y + startPoint.Y} - err = elem.SwipePointF(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } } -func TestElement_Drag(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestElement_Drag(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elements, err := driver.FindElements(BySelector{ClassName: "android.widget.TextView"}) +// if err != nil { +// t.Fatal(err) +// } +// +// for i, elem := range elements { +// text, _ := elem.Text() +// t.Log(i, text) +// } +// +// rect, err := elements[0].Rect() +// if err != nil { +// t.Fatal(err) +// } +// +// // err = elements[0].Drag(300, 450, 256) +// err = elements[0].Drag(300, 450, 256) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elements[0].DragTo(elements[1], 256) +// if err != nil { +// t.Fatal(err) +// } +// +// endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} +// err = elements[0].DragPointF(endPoint, 256) +// if err != nil { +// t.Fatal() +// } +//} - elements, err := driver.FindElements(AndroidBySelector{ClassName: "android.widget.TextView"}) - if err != nil { - t.Fatal(err) - } +//func TestElement_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elem.Flick(36, 20, 100) +// if err != nil { +// t.Fatal(err) +// } +//} - for i, elem := range elements { - text, _ := elem.Text() - t.Log(i, text) - } +//func TestElement_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// // parentElem, err := driver.FindElement(BySelector{ClassName: "android.widget.ScrollView"}) +// // parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) +// parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/dashboard"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollTo(BySelector{ContentDescription: "电池"}) +// if err != nil { +// t.Fatal(err) +// } +//} - rect, err := elements[0].Rect() - if err != nil { - t.Fatal(err) - } - - // err = elements[0].Drag(300, 450, 256) - err = elements[0].Drag(300, 450, 256) - if err != nil { - t.Fatal(err) - } - - err = elements[0].DragTo(elements[1], 256) - if err != nil { - t.Fatal(err) - } - - endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} - err = elements[0].DragPointF(endPoint, 256) - if err != nil { - t.Fatal() - } -} - -func TestElement_Flick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - elem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) - if err != nil { - t.Fatal(err) - } - - err = elem.Flick(36, 20, 100) - if err != nil { - t.Fatal(err) - } -} - -func TestElement_ScrollTo(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // how to make it work? - // parentElem, err := driver.FindElement(AndroidBySelector{ClassName: "android.widget.ScrollView"}) - // parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/dashboard"}) - if err != nil { - t.Fatal(err) - } - - err = parentElem.ScrollTo(AndroidBySelector{ContentDescription: "电池"}) - if err != nil { - t.Fatal(err) - } -} - -func TestElement_ScrollToElement(t *testing.T) { - // android.widget.HorizontalScrollView - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // how to make it work? - parentElem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) - if err != nil { - t.Fatal(err) - } - - element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) - if err != nil { - t.Fatal(err) - } - - err = parentElem.ScrollToElement(element) - if err != nil { - t.Fatal(err) - } -} +//func TestElement_ScrollToElement(t *testing.T) { +// // android.widget.HorizontalScrollView +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// parentElem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollToElement(element) +// if err != nil { +// t.Fatal(err) +// } +//} diff --git a/hrp/internal/uixt/client.go b/hrp/internal/uixt/client.go index befa38d6..5ebd9309 100644 --- a/hrp/internal/uixt/client.go +++ b/hrp/internal/uixt/client.go @@ -51,7 +51,7 @@ func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error } func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { - log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") + log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request driver agent") var req *http.Request if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { @@ -77,7 +77,7 @@ func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (raw // avoid printing screenshot data logger.Str("response", string(rawResp)) } - logger.Msg("get WDA response") + logger.Msg("get driver agent response") if err != nil { return nil, err } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index e333ed48..41dea6d9 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -237,6 +237,11 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { selector = BySelector{ XPath: param, } + } else if strings.HasPrefix(param, "com.") { + // name + selector = BySelector{ + ResourceIdID: param, + } } else { // name selector = BySelector{ @@ -473,18 +478,13 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath) return err case CtlStartCamera: - // start camera, alias for app_launch com.apple.camera - return dExt.Driver.AppLaunch("com.apple.camera") + return dExt.Driver.StartCamera() case CtlStopCamera: - // stop camera, alias for app_terminate com.apple.camera - success, err := dExt.Driver.AppTerminate("com.apple.camera") - if err != nil { - return errors.Wrap(err, "failed to terminate camera") - } - if !success { - log.Warn().Msg("camera was not running") - } - return nil + return dExt.Driver.StopCamera() + case RecordStart: + return dExt.Driver.StartRecording() + case RecordStop: + return dExt.Driver.StopRecording() } return nil } diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index 03fd7a19..346233f4 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -148,6 +148,55 @@ type DeviceInfo struct { Name string `json:"name"` IsSimulator bool `json:"isSimulator"` ThermalState int `json:"thermalState"` + // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user + // first sets up the device and should remain constant for the lifetime of the user's device. The value + // may change if a factory reset is performed on the device. + AndroidID string `json:"androidId"` + // Build.MANUFACTURER value + Manufacturer string `json:"manufacturer"` + // Build.BRAND value + Brand string `json:"brand"` + // Current running OS's API VERSION + APIVersion string `json:"apiVersion"` + // The current version string, for example "1.0" or "3.4b5" + PlatformVersion string `json:"platformVersion"` + // the name of the current celluar network carrier + CarrierName string `json:"carrierName"` + // the real size of the default display + RealDisplaySize string `json:"realDisplaySize"` + // The logical density of the display in Density Independent Pixel units. + DisplayDensity int `json:"displayDensity"` + // available networks + Networks []networkInfo `json:"networks"` + // current system locale + Locale string `json:"locale"` + Bluetooth struct { + State string `json:"state"` + } `json:"bluetooth"` +} + +type networkCapabilities struct { + TransportTypes string `json:"transportTypes"` + NetworkCapabilities string `json:"networkCapabilities"` + LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` + LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` + SignalStrength int `json:"signalStrength"` + SSID string `json:"SSID"` +} + +type networkInfo struct { + Type int `json:"type"` + TypeName string `json:"typeName"` + Subtype int `json:"subtype"` + SubtypeName string `json:"subtypeName"` + IsConnected bool `json:"isConnected"` + DetailedState string `json:"detailedState"` + State string `json:"state"` + ExtraInfo string `json:"extraInfo"` + IsAvailable bool `json:"isAvailable"` + IsRoaming bool `json:"isRoaming"` + IsFailover bool `json:"isFailover"` + Capabilities networkCapabilities `json:"capabilities"` } type Location struct { @@ -267,6 +316,11 @@ func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOptio return opt } +func (opt AppLaunchOption) WithAndroidBySelector(waitForComplete ...AndroidBySelector) AppLaunchOption { + opt["androidBySelector"] = waitForComplete + return opt +} + // PasteboardType The type of the item on the pasteboard. type PasteboardType string @@ -426,6 +480,13 @@ type BySelector struct { ClassChain string `json:"class chain"` XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively + + // Set the search criteria to match the given resource ResourceIdID. + ResourceIdID string `json:"id"` + // Set the search criteria to match the content-description property for a widget. + ContentDescription string `json:"accessibility id"` + + UiAutomator string `json:"-android uiautomator"` } func (wl BySelector) getUsingAndValue() (using, value string) { @@ -449,6 +510,24 @@ func (wl BySelector) getUsingAndValue() (using, value string) { return } +func (by BySelector) getMethodAndSelector() (method, selector string) { + vBy := reflect.ValueOf(by) + tBy := reflect.TypeOf(by) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + // switch vi := vi.(type) { + // case string: + // selector = vi + // } + selector = vi.(string) + if selector != "" && selector != "UNKNOWN" { + method = tBy.Field(i).Tag.Get("json") + return + } + } + return +} + type ElementAttribute map[string]interface{} func (ea ElementAttribute) String() string { @@ -800,6 +879,14 @@ type WebDriver interface { // AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4 AppAuthReset(ProtectedResource) error + // StartCamera Starts a new camera for recording + StartCamera() error + // StopCamera Stops the camera for recording + StopCamera() error + + StartRecording() error + StopRecording() error + // Tap Sends a tap event at the coordinate. Tap(x, y int, options ...DataOption) error TapFloat(x, y float64, options ...DataOption) error diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index bfacfb72..da9de53e 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -241,14 +241,17 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error { func (dExt *DriverExt) GetLogs() (interface{}, error) { log.Info().Msg("stop WDA log recording") - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - log.Error().Err(err).Msg("failed to get WDA logs") - return "", errors.Wrap(err, "failed to get WDA logs") + if _, ok := dExt.Driver.(*wdaDriver); ok { + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + return reply.Value, nil + } else { + // TODO: Android log recording } - - return reply.Value, nil + return "", nil } func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 9e5eda75..771d7d36 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -13,6 +13,7 @@ import ( giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -557,6 +558,33 @@ func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, durati return } +func (wd *wdaDriver) StartCamera() (err error) { + // start camera, alias for app_launch com.apple.camera + return wd.AppLaunch("com.apple.camera") +} + +func (wd *wdaDriver) StopCamera() (err error) { + // stop camera, alias for app_terminate com.apple.camera + success, err := wd.AppTerminate("com.apple.camera") + if err != nil { + return errors.Wrap(err, "failed to terminate camera") + } + if !success { + log.Warn().Msg("camera was not running") + } + return nil +} + +func (wd *wdaDriver) StartRecording() (err error) { + // TODO + return errDriverNotImplemented +} + +func (wd *wdaDriver) StopRecording() (err error) { + // TODO + return errDriverNotImplemented +} + func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { // [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)] if len(second) == 0 { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index b5b74f72..5397f1b2 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -9,6 +9,14 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) +var ( + WithSerialNumber = uixt.WithSerialNumber + WithAdbIP = uixt.WithAdbIP + WithAdbPort = uixt.WithAdbPort + WithAdbLogOn = uixt.WithAdbLogOn + WithMjpegPortA = uixt.WithMjpegPortA +) + type AndroidStep struct { uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal uixt.MobileAction @@ -33,6 +41,38 @@ func (s *StepAndroid) InstallApp(path string) *StepAndroid { return &StepAndroid{step: s.step} } +func (s *StepAndroid) AppLaunch(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppLaunch, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) AppLaunchUnattached(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppLaunchUnattached, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) AppTerminate(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppTerminate, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) Home() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Home, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ Method: uixt.AppStart, @@ -81,51 +121,101 @@ func (s *StepAndroid) Tap(params interface{}) *StepAndroid { return &StepAndroid{step: s.step} } -func (s *StepAndroid) DoubleTap(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +// Tap taps on the target element by OCR recognition +func (s *StepAndroid) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByOCR, + Params: ocrText, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +// Tap taps on the target element by CV recognition +func (s *StepAndroid) TapByCV(imagePath string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByCV, + Params: imagePath, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) DoubleTap(params string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_DoubleTap, Params: params, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) Swipe(sx, sy, ex, ey int) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: []int{sx, sy, ex, ey}, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeUp() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeUp(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "up", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeDown() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeDown(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "down", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeLeft() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeLeft(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "left", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeRight() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeRight(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "right", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } @@ -137,6 +227,47 @@ func (s *StepAndroid) Input(text string) *StepAndroid { return &StepAndroid{step: s.step} } +// Sleep specify sleep seconds after last action +func (s *StepAndroid) Sleep(n float64) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlSleep, + Params: n, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) ScreenShot() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlScreenShot, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapApp, + Params: appName, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapText, + Params: text, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + // Validate switches to step validation. func (s *StepAndroid) Validate() *StepAndroidValidation { return &StepAndroidValidation{ @@ -195,6 +326,96 @@ func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ... return s } +func (s *StepAndroidValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionNotExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertOCRExists(expectedText string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionNotExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionNotExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepAndroidValidation) Name() string { return s.step.Name } From 32027c3ef51ad2c64e021ecb30d682c497f5fe9e Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 14:36:00 +0800 Subject: [PATCH 077/169] update: android douyin demo --- examples/uitest/demo_android_douyin_test.go | 13 ++++++++++--- hrp/internal/uixt/ext.go | 2 +- hrp/internal/uixt/ios_device.go | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index 4bf95693..9e5f2477 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -11,9 +11,16 @@ func TestIOSDouYinLive(t *testing.T) { Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), TestSteps: []hrp.IStep{ + hrp.NewStep("打开网页"). + Android(). + Home(). + AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("Search").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). + Validate(). + AssertOCRExists("1664", "网页打开失败"), hrp.NewStep("启动抖音"). Android(). - Home().StartCamera().Sleep(10).StopCamera(). + Home(). AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)). Sleep(10), @@ -33,10 +40,10 @@ func TestIOSDouYinLive(t *testing.T) { }, } - if err := testCase.Dump2JSON("android_demo_douyin_live.json"); err != nil { + if err := testCase.Dump2JSON("demo_android_douyin_live.json"); err != nil { t.Fatal(err) } - if err := testCase.Dump2YAML("android_demo_douyin_live.yaml"); err != nil { + if err := testCase.Dump2YAML("demo_android_douyin_live.yaml"); err != nil { t.Fatal(err) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 41dea6d9..b2daad9c 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -304,7 +304,7 @@ func (dExt *DriverExt) IsImageExist(text string) bool { var errActionNotImplemented = errors.New("UI action not implemented") func (dExt *DriverExt) DoAction(action MobileAction) error { - log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action") + log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action") switch action.Method { case AppInstall: diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index da9de53e..9a5f6272 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -240,7 +240,7 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error { } func (dExt *DriverExt) GetLogs() (interface{}, error) { - log.Info().Msg("stop WDA log recording") + log.Info().Msg("stop log recording") if _, ok := dExt.Driver.(*wdaDriver); ok { data := map[string]interface{}{"action": "stop"} reply, err := dExt.triggerWDALog(data) From 4dbff93f522dfa73721104692065baf463d57a71 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 14:57:41 +0800 Subject: [PATCH 078/169] fix: remove android mjpeg option --- examples/uitest/demo_android_douyin_test.go | 6 +++--- hrp/internal/uixt/android_device.go | 6 ------ hrp/internal/uixt/opencv_on.go | 1 + hrp/step_android_ui.go | 1 - 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index 9e5f2477..a0d946f2 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -6,16 +6,16 @@ import ( "github.com/httprunner/httprunner/v4/hrp" ) -func TestIOSDouYinLive(t *testing.T) { +func TestAndroidDouYinLive(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). - SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), + SetAndroid(hrp.WithAdbLogOn(true)), TestSteps: []hrp.IStep{ hrp.NewStep("打开网页"). Android(). Home(). AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 - SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("Search").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). + SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("搜索").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). Validate(). AssertOCRExists("1664", "网页打开失败"), hrp.NewStep("启动抖音"). diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index 275e4cb9..568adcdf 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -65,12 +65,6 @@ func WithSerialNumber(serial string) AndroidDeviceOption { } } -func WithMjpegPortA(port int) AndroidDeviceOption { - return func(device *AndroidDevice) { - device.MjpegPort = port - } -} - func WithAdbIP(ip string) AndroidDeviceOption { return func(device *AndroidDevice) { device.IP = ip diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 2153283f..d4acda45 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -5,6 +5,7 @@ package uixt import ( "bytes" "image" + "io/ioutil" "os" cvHelper "github.com/electricbubble/opencv-helper" diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 5397f1b2..53168cf2 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -14,7 +14,6 @@ var ( WithAdbIP = uixt.WithAdbIP WithAdbPort = uixt.WithAdbPort WithAdbLogOn = uixt.WithAdbLogOn - WithMjpegPortA = uixt.WithMjpegPortA ) type AndroidStep struct { From 16daf7a250a57c3a1ecb536d26a9c12262a46dcc Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:14:00 +0800 Subject: [PATCH 079/169] fix: update unittest for action --- .github/workflows/unittest.yml | 2 -- hrp/internal/uixt/android_test.go | 2 ++ hrp/internal/uixt/drag_test.go | 2 ++ hrp/internal/uixt/ios_test.go | 1 + hrp/internal/uixt/swipe_test.go | 2 ++ hrp/internal/uixt/tap_test.go | 2 ++ hrp/internal/uixt/touch_test.go | 2 ++ hrp/step_android_ui_test.go | 2 ++ hrp/step_ios_ui_test.go | 3 ++- 9 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 77e221fc..e8b5d983 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -63,8 +63,6 @@ jobs: fail-fast: false matrix: go-version: - - 1.16.x - - 1.17.x - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go index 08a71c17..71c3790c 100644 --- a/hrp/internal/uixt/android_test.go +++ b/hrp/internal/uixt/android_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go index 258c515c..f2a628c5 100644 --- a/hrp/internal/uixt/drag_test.go +++ b/hrp/internal/uixt/drag_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go index 62c52161..efc61518 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -1,3 +1,4 @@ +//go:build localtest package uixt import ( diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index 5314a663..61ded0af 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index faaaa14e..c5dcafa7 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index 9ec38aee..a4b25ade 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go index 7ca077f9..7162eef9 100644 --- a/hrp/step_android_ui_test.go +++ b/hrp/step_android_ui_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package hrp import ( diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 3de1f6db..4b60b04c 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -1,3 +1,4 @@ +//go:build localtest package hrp import ( @@ -85,7 +86,7 @@ func TestIOSWeixinLive(t *testing.T) { NewStep("进入直播页"). IOS(). Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 - TapByOCR("直播"). // 通过 OCR 识别「直播」 + TapByOCR("直播"). // 通过 OCR 识别「直播」 Validate(). AssertLabelExists("直播"), NewStep("向上滑动 5 次"). From c9f107c0bbcef13d506729b6464b4265f24cf842 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:39:23 +0800 Subject: [PATCH 080/169] fix: unittest --- hrp/runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrp/runner.go b/hrp/runner.go index 2524ff8c..bbfd8655 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -215,7 +215,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { s.appendCaseSummary(caseSummary) if err1 != nil || err2 != nil { log.Error().Err(err1).Msg("[Run] run testcase failed") - runErr = err + runErr = err1 break } } From 2fef14a22f2752cb71978f87a7fd42ab1d025e23 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:43:54 +0800 Subject: [PATCH 081/169] fix: workflows runs only in go version 1.18 --- .github/workflows/hrp-scaffold.yml | 6 ++--- .github/workflows/smoketest.yml | 2 +- .../plugin/debugtalk_gen.go | 16 ------------- examples/demo-with-go-plugin/proj.json | 4 ++-- .../demo-with-py-plugin/.debugtalk_gen.py | 23 ------------------- examples/demo-with-py-plugin/proj.json | 4 ++-- 6 files changed, 8 insertions(+), 47 deletions(-) delete mode 100644 examples/demo-with-go-plugin/plugin/debugtalk_gen.go delete mode 100644 examples/demo-with-py-plugin/.debugtalk_gen.py diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index e513435a..2d0ab07d 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -44,7 +44,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index f914858a..efca234e 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go b/examples/demo-with-go-plugin/plugin/debugtalk_gen.go deleted file mode 100644 index 0ee1ae22..00000000 --- a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go +++ /dev/null @@ -1,16 +0,0 @@ -// NOTE: Generated By hrp v4.1.5, DO NOT EDIT! -package main - -import ( - "github.com/httprunner/funplugin/fungo" -) - -func main() { - fungo.Register("SumTwoInt", SumTwoInt) - fungo.Register("SumInts", SumInts) - fungo.Register("Sum", Sum) - fungo.Register("SetupHookExample", SetupHookExample) - fungo.Register("TeardownHookExample", TeardownHookExample) - fungo.Register("GetUserAgent", GetUserAgent) - fungo.Serve() -} diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index ecc3509d..3225b92e 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-go-plugin", - "create_time": "2022-07-26T10:30:29.31361+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-09-28T16:40:14.674398+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-py-plugin/.debugtalk_gen.py b/examples/demo-with-py-plugin/.debugtalk_gen.py deleted file mode 100644 index 50f50e5f..00000000 --- a/examples/demo-with-py-plugin/.debugtalk_gen.py +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: Generated By hrp v4.1.6, DO NOT EDIT! - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from debugtalk import * - - -if __name__ == "__main__": - import funppy - funppy.register("get_user_agent", get_user_agent) - funppy.register("sleep", sleep) - funppy.register("sum", sum) - funppy.register("sum_ints", sum_ints) - funppy.register("sum_two_int", sum_two_int) - funppy.register("sum_two_string", sum_two_string) - funppy.register("sum_strings", sum_strings) - funppy.register("concatenate", concatenate) - funppy.register("setup_hook_example", setup_hook_example) - funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index 8140ae8b..a2b30841 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-py-plugin", - "create_time": "2022-07-26T10:30:30.601095+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-09-28T16:40:15.283869+08:00", + "hrp_version": "v4.3.0" } From e2c499f254087d393b696c1afea46f1a07c7b4e3 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 19:58:39 +0800 Subject: [PATCH 082/169] fix: unittest --- examples/uitest/demo_android_douyin_test.go | 2 ++ examples/uitest/demo_douyin_test.go | 2 ++ examples/uitest/demo_weixin_test.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index a0d946f2..b43f8b9f 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index b9ca8fa9..273be52e 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 112b6ffd..c4cd4380 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( From f1779c4ba3ca27173a71e52eb2368814b5a55bb3 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 20:25:19 +0800 Subject: [PATCH 083/169] fix: failed to generate html report --- hrp/internal/scaffold/templates/report/template.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrp/internal/scaffold/templates/report/template.html b/hrp/internal/scaffold/templates/report/template.html index 4bff6c65..0575c021 100644 --- a/hrp/internal/scaffold/templates/report/template.html +++ b/hrp/internal/scaffold/templates/report/template.html @@ -338,14 +338,14 @@ - {{ if .Attachment }} + {{ if .Attachments }} traceback From 5521e3faffe0f9d708bd9bc617c716012e843e7d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 28 Sep 2022 21:08:07 +0800 Subject: [PATCH 084/169] fix: sleep with int64 seconds --- hrp/internal/uixt/ext.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index b2daad9c..ae4ab142 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -464,6 +464,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } else if param, ok := action.Params.(float64); ok { time.Sleep(time.Duration(param*1000) * time.Millisecond) return nil + } else if param, ok := action.Params.(int64); ok { + time.Sleep(time.Duration(param) * time.Second) + return nil } return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params) case CtlScreenShot: From e2049ca73ee3dc5eb8f96c73b25e9c4968b0e330 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 28 Sep 2022 22:24:28 +0800 Subject: [PATCH 085/169] fix: wda logs --- hrp/internal/uixt/ios_device.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 9a5f6272..3c4a189e 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -245,6 +245,7 @@ func (dExt *DriverExt) GetLogs() (interface{}, error) { data := map[string]interface{}{"action": "stop"} reply, err := dExt.triggerWDALog(data) if err != nil { + log.Error().Err(err).Interface("reply", reply).Msg("failed to get WDA logs") return "", errors.Wrap(err, "failed to get WDA logs") } return reply.Value, nil @@ -279,8 +280,7 @@ func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, reply := new(wdaResponse) if err = json.Unmarshal(rawResp, reply); err != nil { - log.Info().Bytes("rawResp", rawResp).Msg("get unexpected WDA log response") - return nil, err + return reply, err } log.Info().Interface("value", reply.Value).Msg("get WDA log response") From dad3676c3d0c986e42fd656912d4f4c0d4e72076 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 11:04:14 +0800 Subject: [PATCH 086/169] fix: swipe 2 times --- hrp/internal/uixt/swipe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index c329263d..97ac8fc5 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -31,7 +31,7 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier "enable": true, "data": identifier[0], }) - dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, option) + return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, option) } return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY) } From 0a604f6ec5a3520ff0579c6ff72f07b39fda1326 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 11:26:14 +0800 Subject: [PATCH 087/169] change: update version --- hrp/internal/version/VERSION | 2 +- hrp/plugin.go | 5 +++-- hrp/runner.go | 3 +++ httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 1ddc0f60..12e4c43d 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0 \ No newline at end of file +v4.3.0-beta-09291106 \ No newline at end of file diff --git a/hrp/plugin.go b/hrp/plugin.go index c762b6c8..fad9aa7a 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -9,9 +9,10 @@ import ( "github.com/httprunner/funplugin" "github.com/httprunner/funplugin/fungo" + "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" - "github.com/rs/zerolog/log" ) const ( @@ -34,6 +35,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er } pluginPath, err := locatePlugin(path) if err != nil { + log.Warn().Err(err).Str("path", path).Msg("locate plugin failed") return nil, nil } @@ -109,7 +111,6 @@ func locatePlugin(path string) (pluginPath string, err error) { return } - log.Warn().Err(err).Str("path", path).Msg("plugin file not found") return "", fmt.Errorf("plugin file not found") } diff --git a/hrp/runner.go b/hrp/runner.go index bbfd8655..4a672b60 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -21,6 +21,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/internal/version" ) // Run starts to run API test with default configs. @@ -172,6 +173,8 @@ func (r *HRPRunner) GenHTMLReport() *HRPRunner { // Run starts to execute one or multiple testcases. func (r *HRPRunner) Run(testcases ...ITestCase) error { + log.Info().Str("hrp_version", version.VERSION). + Interface("testcases", testcases).Msg("start running") event := sdk.EventTracking{ Category: "RunAPITests", Action: "hrp run", diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 416ff278..d2f494e9 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0" +__version__ = "v4.3.0-beta-09291106" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 33947c0f..6c2b64b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0" +version = "v4.3.0-beta-09291106" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 2cceffa66d35dfd87dcec3be0bc471a80e15cbb0 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 15:46:30 +0800 Subject: [PATCH 088/169] fix: tap x,y positions --- examples/uitest/wda_log_data.json | 112 ++++++++++++++++++++++++++++++ examples/uitest/wda_log_test.go | 48 +++++++++++++ hrp/internal/uixt/ext.go | 45 +++++++++--- hrp/internal/uixt/touch_test.go | 10 --- hrp/internal/version/VERSION | 2 +- hrp/step_ios_ui.go | 10 ++- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 8 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 examples/uitest/wda_log_data.json create mode 100644 examples/uitest/wda_log_test.go diff --git a/examples/uitest/wda_log_data.json b/examples/uitest/wda_log_data.json new file mode 100644 index 00000000..4623ab7d --- /dev/null +++ b/examples/uitest/wda_log_data.json @@ -0,0 +1,112 @@ +{ + "config": { + "name": "验证 WDA 打点数据准确性", + "variables": { + "app_name": "抖音" + }, + "ios": [ + { + "port": 8700, + "mjpeg_port": 8800, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "$app_name", + "identifier": "启动抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "向上滑动 2 次", + "ios": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第 1 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "swipe", + "params": "up", + "identifier": "第 2 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "swipe", + "params": "up", + "identifier": "第 3 次上划" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "tap_xy", + "params": [ + 0.9, + 0.1 + ], + "identifier": "点击进入搜索框" + }, + { + "method": "sleep", + "params": 2 + }, + { + "method": "input", + "params": "httprunner", + "identifier": "输入搜索关键词" + } + ] + } + } + ] +} diff --git a/examples/uitest/wda_log_test.go b/examples/uitest/wda_log_test.go new file mode 100644 index 00000000..eebc6465 --- /dev/null +++ b/examples/uitest/wda_log_test.go @@ -0,0 +1,48 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestWDALog(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("验证 WDA 打点数据准确性"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). + SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("向上滑动 2 次"). + IOS(). + SwipeUp(hrp.WithIdentifier("第 1 次上划")).Sleep(2). + SwipeUp(hrp.WithIdentifier("第 2 次上划")).Sleep(2). + SwipeUp(hrp.WithIdentifier("第 3 次上划")).Sleep(2). + TapXY(0.9, 0.1, hrp.WithIdentifier("点击进入搜索框")).Sleep(2). + Input("httprunner", hrp.WithIdentifier("输入搜索关键词")), + }, + } + + if err := testCase.Dump2JSON("wda_log_data.json"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index ae4ab142..927b08c1 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "testing" "time" "github.com/pkg/errors" @@ -391,21 +392,25 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { case ACTION_Home: return dExt.Driver.Homescreen() case ACTION_TapXY: - if location, ok := action.Params.([]float64); ok { + if location, ok := action.Params.([]interface{}); ok { // relative x,y of window size: [0.5, 0.5] if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return dExt.TapXY(location[0], location[1], action.Identifier) + x, _ := location[0].(float64) + y, _ := location[1].(float64) + return dExt.TapXY(x, y, action.Identifier) } return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) case ACTION_TapAbsXY: - if location, ok := action.Params.([]float64); ok { + if location, ok := action.Params.([]interface{}); ok { // absolute coordinates x,y of window size: [100, 300] if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return dExt.TapAbsXY(location[0], location[1], action.Identifier) + x, _ := location[0].(float64) + y, _ := location[1].(float64) + return dExt.TapAbsXY(x, y, action.Identifier) } return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params) case ACTION_Tap: @@ -424,12 +429,14 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params) case ACTION_DoubleTapXY: - if location, ok := action.Params.([]float64); ok { + if location, ok := action.Params.([]interface{}); ok { // relative x,y of window size: [0.5, 0.5] if len(location) != 2 { return fmt.Errorf("invalid tap location params: %v", location) } - return dExt.DoubleTapXY(location[0], location[1]) + x, _ := location[0].(float64) + y, _ := location[1].(float64) + return dExt.DoubleTapXY(x, y) } return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params) case ACTION_DoubleTap: @@ -438,13 +445,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params) case ACTION_Swipe: - if positions, ok := action.Params.([]float64); ok { + if positions, ok := action.Params.([]interface{}); ok { // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] if len(positions) != 4 { return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions) } - return dExt.SwipeRelative( - positions[0], positions[1], positions[2], positions[3], action.Identifier) + fromX, _ := positions[0].(float64) + fromY, _ := positions[1].(float64) + toX, _ := positions[2].(float64) + toY, _ := positions[3].(float64) + return dExt.SwipeRelative(fromX, fromY, toX, toY, action.Identifier) } if direction, ok := action.Params.(string); ok { return dExt.SwipeTo(direction, action.Identifier) @@ -455,6 +465,13 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { // append \n to send text with enter // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) + if action.Identifier != "" { + option := WithCustomOption("log", map[string]interface{}{ + "enable": true, + "data": action.Identifier, + }) + return dExt.Driver.SendKeys(param, option) + } return dExt.Driver.SendKeys(param) case CtlSleep: if param, ok := action.Params.(json.Number); ok { @@ -529,3 +546,13 @@ func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...s Msg("validate UI success") return true } + +func checkErr(t *testing.T, err error, msg ...string) { + if err != nil { + if len(msg) == 0 { + t.Fatal(err) + } else { + t.Fatal(msg, err) + } + } +} diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index a4b25ade..c89d9879 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -37,13 +37,3 @@ func TestDriverExt_TouchAndHold(t *testing.T) { err = driverExt.TouchAndHoldOffset(pathSearch, 0.8, 0.1) checkErr(t, err) } - -func checkErr(t *testing.T, err error, msg ...string) { - if err != nil { - if len(msg) == 0 { - t.Fatal(err) - } else { - t.Fatal(msg, err) - } - } -} diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 12e4c43d..13572370 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09291106 \ No newline at end of file +v4.3.0-beta-09291549 \ No newline at end of file diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7b2c0c9e..e0a1a985 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -243,11 +243,15 @@ func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *Ste return &StepIOS{step: s.step} } -func (s *StepIOS) Input(text string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepIOS) Input(text string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ Method: uixt.ACTION_Input, Params: text, - }) + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) return &StepIOS{step: s.step} } diff --git a/httprunner/__init__.py b/httprunner/__init__.py index d2f494e9..ad5a4b28 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09291106" +__version__ = "v4.3.0-beta-09291549" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 6c2b64b0..8c82aeee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09291106" +version = "v4.3.0-beta-09291549" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From abf5434e8b763bb1c2fb9829758bc5f358fb78ab Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 16:14:16 +0800 Subject: [PATCH 089/169] change: update tests --- examples/uitest/wda_log_data.json | 23 +++++++++++++++++++++++ examples/uitest/wda_log_test.go | 7 ++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/examples/uitest/wda_log_data.json b/examples/uitest/wda_log_data.json index 4623ab7d..06b012ef 100644 --- a/examples/uitest/wda_log_data.json +++ b/examples/uitest/wda_log_data.json @@ -13,6 +13,29 @@ ] }, "teststeps": [ + { + "name": "查看时间戳", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.apple.mobilesafari" + }, + { + "method": "swipe_to_tap_app", + "params": "timestamp", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 3 + } + ] + } + }, { "name": "启动抖音", "ios": { diff --git a/examples/uitest/wda_log_test.go b/examples/uitest/wda_log_test.go index eebc6465..e34a69d3 100644 --- a/examples/uitest/wda_log_test.go +++ b/examples/uitest/wda_log_test.go @@ -1,4 +1,4 @@ -//go:build localtest +//go:build !localtest package uitest @@ -16,6 +16,11 @@ func TestWDALog(t *testing.T) { }). SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), TestSteps: []hrp.IStep{ + hrp.NewStep("查看时间戳"). + IOS(). + Home(). + AppTerminate("com.apple.mobilesafari"). + SwipeToTapApp("timestamp", hrp.WithMaxRetryTimes(5)).Sleep(3), hrp.NewStep("启动抖音"). IOS(). Home(). From 5bd2edac27e93401800316cfec5024ff67babdd7 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Thu, 29 Sep 2022 16:20:53 +0800 Subject: [PATCH 090/169] update: android actions --- hrp/step_android_ui.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 53168cf2..fb2856b5 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -112,6 +112,32 @@ func (s *StepAndroid) StopRecording() *StepAndroid { return &StepAndroid{step: s.step} } +// TapXY taps the point {X,Y}, X & Y is percentage of coordinates +func (s *StepAndroid) TapXY(x, y float64, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapXY, + Params: []float64{x, y}, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +// TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates +func (s *StepAndroid) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapAbsXY, + Params: []float64{x, y}, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + func (s *StepAndroid) Tap(params interface{}) *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ Method: uixt.ACTION_Tap, From 9c54996d175a0c56aedfaf321d5236a966912129 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Thu, 29 Sep 2022 17:18:26 +0800 Subject: [PATCH 091/169] fix: failed to parse in android ui automation --- hrp/step_android_ui.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index fb2856b5..4db318e7 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) var ( @@ -466,6 +466,13 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err } screenshots := make([]string, 0) + // override step variables + stepVariables, err := s.MergeStepVariables(step.Variables) + if err != nil { + return + } + parser := s.GetParser() + // init uiaClient driver uiaClient, err := s.hrpRunner.initUIClient(&step.Android.AndroidDevice) if err != nil { @@ -511,6 +518,9 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { + if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { + return stepResult, errors.Wrap(err, "parse action params failed") + } if err := uiaClient.DoAction(action); err != nil { return stepResult, err } From 3cfa05f4371cbe276f417aafa55c3e3834825b77 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 17:27:49 +0800 Subject: [PATCH 092/169] update version --- hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 13572370..42e4bb31 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09291549 \ No newline at end of file +v4.3.0-beta-09291727 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index ad5a4b28..80a9948b 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09291549" +__version__ = "v4.3.0-beta-09291727" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 8c82aeee..b8213141 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09291549" +version = "v4.3.0-beta-09291727" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From a248940c816e91facdb736fac0c0a52fe6b93661 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 20:20:48 +0800 Subject: [PATCH 093/169] feat: support override with environment variable WDA_PORT, WDA_MJPEG_PORT --- hrp/internal/uixt/ios_device.go | 23 +++++++++++++++++++++++ hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 3c4a189e..b5f37268 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -11,7 +11,9 @@ import ( "net" "net/http" "net/url" + "os" "regexp" + "strconv" "strings" giDevice "github.com/electricbubble/gidevice" @@ -46,6 +48,27 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { if device.UDID != "" { deviceOptions = append(deviceOptions, WithUDID(device.UDID)) } + + wda_port := os.Getenv("WDA_PORT") + if wda_port != "" { + if port, err := strconv.Atoi(wda_port); err == nil { + log.Info().Str("WDA_PORT", wda_port). + Msg("override with environment variable") + device.Port = port + } else { + log.Error().Err(err).Msg("invalid WDA_PORT, ignored") + } + } + wda_mjpeg_port := os.Getenv("WDA_MJPEG_PORT") + if wda_mjpeg_port != "" { + if mjpeg_port, err := strconv.Atoi(wda_mjpeg_port); err == nil { + log.Info().Str("WDA_MJPEG_PORT", wda_mjpeg_port). + Msg("override with environment variable") + device.MjpegPort = mjpeg_port + } else { + log.Error().Err(err).Msg("invalid WDA_MJPEG_PORT, ignored") + } + } if device.Port != 0 { deviceOptions = append(deviceOptions, WithPort(device.Port)) } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 42e4bb31..710e0281 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09291727 \ No newline at end of file +v4.3.0-beta-09292020 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 80a9948b..415278d7 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09291727" +__version__ = "v4.3.0-beta-09292020" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index b8213141..adbe2bde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09291727" +version = "v4.3.0-beta-09292020" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From eb9e3efe6cf363bad84cfd8f53ac793f95fa29e1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 29 Sep 2022 23:10:46 +0800 Subject: [PATCH 094/169] fix: trigger wda logs with usbmux --- hrp/internal/uixt/android_driver.go | 12 ++++- hrp/internal/uixt/ext.go | 1 - hrp/internal/uixt/interface.go | 4 ++ hrp/internal/uixt/ios_device.go | 70 +---------------------------- hrp/internal/uixt/ios_driver.go | 41 +++++++++++++++++ hrp/internal/version/VERSION | 2 +- hrp/session.go | 16 +++---- hrp/summary.go | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 10 files changed, 69 insertions(+), 83 deletions(-) diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 77501047..858cb2d7 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -138,7 +138,7 @@ func (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err er if rawResp, err = ud.httpGET("/session", ud.sessionId); err != nil { return nil, err } - var reply = new(struct{ Value map[string]interface{} }) + reply := new(struct{ Value map[string]interface{} }) if err = json.Unmarshal(rawResp, reply); err != nil { return nil, err } @@ -944,3 +944,13 @@ func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) func (ud *uiaDriver) Wait(condition Condition) error { return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } + +func (ud *uiaDriver) StartCaptureLog(identifier ...string) (err error) { + // TODO + return +} + +func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) { + // TODO + return +} diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 927b08c1..93155a76 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -131,7 +131,6 @@ type DriverExt struct { frame *bytes.Buffer doneMjpegStream chan bool scale float64 - host string StartTime time.Time // used to associate screenshots name ScreenShots []string // save screenshots path diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index 346233f4..cbc7c668 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -989,6 +989,10 @@ type WebDriver interface { // Close inner connections properly Close() error + + // triggers the log capture and returns the log entries + StartCaptureLog(identifier ...string) (err error) + StopCaptureLog() (result interface{}, err error) } // WebElement defines method supported by web elements. diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index b5f37268..48a4e54e 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -5,7 +5,6 @@ import ( "encoding/base64" builtinJSON "encoding/json" "fmt" - "io/ioutil" "mime" "mime/multipart" "net" @@ -111,9 +110,8 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") - driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", iosDevice.Port) if device.LogOn { - err = driverExt.StartLogRecording("hrp_wda_log") + err = driverExt.Driver.StartCaptureLog("hrp_wda_log") if err != nil { return nil, err } @@ -198,6 +196,7 @@ func (dev *IOSDevice) UUID() string { // NewHTTPDriver creates new remote HTTP client, this will also start a new session. func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { wd := new(wdaDriver) + wd.client = http.DefaultClient urlPrefix := fmt.Sprintf("http://127.0.0.1:%d", dev.Port) if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil { @@ -208,7 +207,6 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver return nil, err } wd.sessionId = sessionInfo.SessionId - wd.client = http.DefaultClient if wd.mjpegHTTPConn, err = net.Dial( "tcp", @@ -246,70 +244,6 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, return wd, err } -type wdaResponse struct { - Value interface{} `json:"value"` - SessionID string `json:"sessionId"` -} - -func (dExt *DriverExt) StartLogRecording(identifier string) error { - log.Info().Msg("start WDA log recording") - data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} - _, err := dExt.triggerWDALog(data) - if err != nil { - return errors.Wrap(err, "failed to start WDA log recording") - } - - return nil -} - -func (dExt *DriverExt) GetLogs() (interface{}, error) { - log.Info().Msg("stop log recording") - if _, ok := dExt.Driver.(*wdaDriver); ok { - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - log.Error().Err(err).Interface("reply", reply).Msg("failed to get WDA logs") - return "", errors.Wrap(err, "failed to get WDA logs") - } - return reply.Value, nil - } else { - // TODO: Android log recording - } - return "", nil -} - -func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { - // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] - postJSON, err := json.Marshal(data) - if err != nil { - return nil, err - } - - url := fmt.Sprintf("%s/gtf/automation/log", dExt.host) - log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log") - resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON)) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode) - } - - rawResp, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - reply := new(wdaResponse) - if err = json.Unmarshal(rawResp, reply); err != nil { - return reply, err - } - log.Info().Interface("value", reply.Value).Msg("get WDA log response") - - return reply, nil -} - func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { if httpClient == nil { return errors.New(`'httpClient' can't be nil`) diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 771d7d36..ac4159f0 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -861,3 +861,44 @@ func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) func (wd *wdaDriver) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } + +func (wd *wdaDriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, err error) { + // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] + return wd.httpPOST(data, "/gtf/automation/log") +} + +func (wd *wdaDriver) StartCaptureLog(identifier ...string) error { + log.Info().Msg("start WDA log recording") + if identifier == nil { + identifier = []string{""} + } + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier[0]} + _, err := wd.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + + return nil +} + +type wdaResponse struct { + Value interface{} `json:"value"` + SessionID string `json:"sessionId"` +} + +func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { + log.Info().Msg("stop log recording") + data := map[string]interface{}{"action": "stop"} + rawResp, err := wd.triggerWDALog(data) + if err != nil { + log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to get WDA logs") + return "", errors.Wrap(err, "failed to get WDA logs") + } + reply := new(wdaResponse) + if err = json.Unmarshal(rawResp, reply); err != nil { + log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to json.Unmarshal WDA logs") + return reply, err + } + log.Info().Interface("value", reply.Value).Msg("get WDA log response") + return reply.Value, nil +} diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 710e0281..0dbff02d 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09292020 \ No newline at end of file +v4.3.0-beta-09292312 \ No newline at end of file diff --git a/hrp/session.go b/hrp/session.go index 83c0429b..d04f5a9e 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -8,8 +8,6 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // SessionRunner is used to run testcase and its steps. @@ -165,17 +163,17 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { caseSummary.InOut.ConfigVars = r.parsedConfig.Variables // add WDA/UIA logs to summary - logs := make(map[string]interface{}) - for udid, client := range r.hrpRunner.uiClients { - log, err := client.GetLogs() + for uuid, client := range r.hrpRunner.uiClients { + log, err := client.Driver.StopCaptureLog() if err != nil { return caseSummary, err } - logs[udid] = log + logs := map[string]interface{}{ + "uuid": uuid, + "content": log, + } + caseSummary.Logs = append(caseSummary.Logs, logs) } - logsBytes, _ := json.Marshal(logs) - caseSummary.Logs = string(logsBytes) - return caseSummary, nil } diff --git a/hrp/summary.go b/hrp/summary.go index 841f7fc5..0883a3d8 100644 --- a/hrp/summary.go +++ b/hrp/summary.go @@ -151,7 +151,7 @@ type TestCaseSummary struct { Stat *TestStepStat `json:"stat" yaml:"stat"` Time *TestCaseTime `json:"time" yaml:"time"` InOut *TestCaseInOut `json:"in_out" yaml:"in_out"` - Logs string `json:"logs,omitempty" yaml:"logs,omitempty"` + Logs []interface{} `json:"logs,omitempty" yaml:"logs,omitempty"` Records []*StepResult `json:"records" yaml:"records"` RootDir string `json:"root_dir" yaml:"root_dir"` } diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 415278d7..fe307b9d 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09292020" +__version__ = "v4.3.0-beta-09292312" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index adbe2bde..b1f1f680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09292020" +version = "v4.3.0-beta-09292312" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From b2cac25bd493cc4b9c5749359de446ccb2a3b82c Mon Sep 17 00:00:00 2001 From: xucong053 Date: Thu, 29 Sep 2022 16:15:56 +0800 Subject: [PATCH 095/169] change: update adb logs --- examples/uitest/demo_android_douyin_test.go | 4 +- hrp/internal/uixt/android_device.go | 168 +++++++++++++++++++- hrp/internal/uixt/android_device_test.go | 18 +++ hrp/internal/uixt/android_driver.go | 1 + hrp/internal/uixt/ext.go | 39 +++++ hrp/step_ios_ui.go | 2 +- 6 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 hrp/internal/uixt/android_device_test.go diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index b43f8b9f..d07558ce 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -11,12 +11,12 @@ import ( func TestAndroidDouYinLive(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). - SetAndroid(hrp.WithAdbLogOn(true)), + SetAndroid(hrp.WithAdbLogOn(true), hrp.WithSerialNumber("2d06bf70")), TestSteps: []hrp.IStep{ hrp.NewStep("打开网页"). Android(). Home(). - AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("搜索").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). Validate(). AssertOCRExists("1664", "网页打开失败"), diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index 568adcdf..a2f170d9 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -2,12 +2,19 @@ package uixt import ( "bytes" + "context" "fmt" "net" + "os/exec" "reflect" + "regexp" + "strconv" + "strings" + "syscall" "github.com/electricbubble/gadb" "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) var ( @@ -19,6 +26,15 @@ var ( const forwardToPrefix = "forward-to-" +const ( + regexFloat = `[0-9\.]*` +) + +var ( + regexCompileSwipe = regexp.MustCompile(fmt.Sprintf(`timesec=(%s)\s*startX=(%s)\s*startY=(%s)\s*endX=(%s)\s*endY=(%s)`, regexFloat, regexFloat, regexFloat, regexFloat, regexFloat)) // parse ${var} or $var + regexCompileTap = regexp.MustCompile(fmt.Sprintf(`timesec=(%s)\s*x=(%s)\s*y=(%s)`, regexFloat, regexFloat, regexFloat)) // parse ${func1($a, $b)} // parse number +) + func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { var deviceOptions []AndroidDeviceOption if device.SerialNumber != "" { @@ -51,10 +67,10 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { } if device.LogOn { - // TODO + err = driverExt.StartLogRecording("hrp_adb_log") } - return driverExt, nil + return driverExt, err } type AndroidDeviceOption func(*AndroidDevice) @@ -106,6 +122,7 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er device.SerialNumber = dev.Serial() device.d = dev + device.logcat = NewAdbLogcat(serialNumber) return device, nil } @@ -114,6 +131,7 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er type AndroidDevice struct { d gadb.Device + logcat *DeviceLogcat SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` IP string `json:"ip,omitempty" yaml:"ip,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` @@ -152,6 +170,7 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDr return nil, err } driver.adbDevice = dev.d + driver.logcat = dev.logcat driver.localPort = localPort return driver, nil @@ -182,6 +201,151 @@ func getFreePort() (int, error) { return l.Addr().(*net.TCPAddr).Port, nil } +type DeviceLogcat struct { + serial string + logBuffer *bytes.Buffer + errs []error + stopping chan struct{} + done chan struct{} + cmd *exec.Cmd +} + +func NewAdbLogcat(serial string) *DeviceLogcat { + return &DeviceLogcat{ + serial: serial, + logBuffer: new(bytes.Buffer), + stopping: make(chan struct{}), + done: make(chan struct{}), + } +} + +// CatchLogcatContext starts logcat with timeout context +func (l *DeviceLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) { + if err = l.CatchLogcat(); err != nil { + return + } + go func() { + select { + case <-timeoutCtx.Done(): + _ = l.Stop() + case <-l.stopping: + } + }() + return +} + +func (l *DeviceLogcat) Stop() error { + select { + case <-l.stopping: + default: + close(l.stopping) + <-l.done + close(l.done) + } + return l.Errors() +} + +func (l *DeviceLogcat) Errors() (err error) { + for _, e := range l.errs { + if err != nil { + err = fmt.Errorf("%v |[DeviceLogcatErr] %v", err, e) + } else { + err = fmt.Errorf("[DeviceLogcatErr] %v", e) + } + } + return +} + +func (l *DeviceLogcat) CatchLogcat() (err error) { + if l.cmd != nil { + err = fmt.Errorf("logcat already start") + } + command := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial) + l.cmd = exec.Command("bash", "-c", command) + l.cmd.Stderr = l.logBuffer + l.cmd.Stdout = l.logBuffer + l.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + if err = l.cmd.Start(); err != nil { + return + } + go func() { + <-l.stopping + if e := syscall.Kill(-l.cmd.Process.Pid, syscall.SIGKILL); e != nil { + l.errs = append(l.errs, fmt.Errorf("kill logcat process err:%v", e)) + } + l.done <- struct{}{} + }() + return +} + +func (l *DeviceLogcat) BufferedLogcat() (err error) { + // -d: dump the current buffered logcat result and exits + command := fmt.Sprintf("adb -s %s logcat -d", l.serial) + cmd := exec.Command("bash", "-c", command) + cmd.Stdout = l.logBuffer + cmd.Stderr = l.logBuffer + if err = cmd.Run(); err != nil { + return + } + return +} + +type ExportPoint struct { + Start int `json:"start" yaml:"start"` + End int `json:"end" yaml:"end"` + From interface{} `json:"from" yaml:"from"` + To interface{} `json:"to" yaml:"to"` + Operation string `json:"operation" yaml:"operation"` + Ext string `json:"ext" yaml:"ext"` + RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"` +} + +func ConvertPoints(data string) (eps []ExportPoint) { + lines := strings.Split(data, "\n") + for _, line := range lines { + if strings.Contains(line, "startX") { + matched := regexCompileSwipe.FindStringSubmatch(line) + if len(matched) != 6 { + log.Error().Msg("failed to parse point data") + continue + } + start, _ := strconv.Atoi(matched[1]) + fromX, _ := strconv.ParseFloat(matched[2], 64) + fromY, _ := strconv.ParseFloat(matched[3], 64) + toX, _ := strconv.ParseFloat(matched[4], 64) + toY, _ := strconv.ParseFloat(matched[5], 64) + p := ExportPoint{ + Start: start, + End: start, + From: []float64{fromX, fromY}, + To: []float64{toX, toY}, + Operation: "Gtf-Drag", + Ext: "", + } + eps = append(eps, p) + } else if strings.Contains(line, "x=") { + matched := regexCompileTap.FindStringSubmatch(line) + if len(matched) != 4 { + log.Error().Msg("failed to parse point data") + continue + } + start, _ := strconv.Atoi(matched[1]) + x, _ := strconv.ParseFloat(matched[2], 64) + y, _ := strconv.ParseFloat(matched[3], 64) + p := ExportPoint{ + Start: start, + End: start, + From: []float64{x, y}, + To: []float64{x, y}, + Operation: "Gtf-Tap", + Ext: "", + } + eps = append(eps, p) + } + } + return +} + type UiSelectorHelper struct { value *bytes.Buffer } diff --git a/hrp/internal/uixt/android_device_test.go b/hrp/internal/uixt/android_device_test.go new file mode 100644 index 00000000..4195a5ef --- /dev/null +++ b/hrp/internal/uixt/android_device_test.go @@ -0,0 +1,18 @@ +package uixt + +import ( + "fmt" + "testing" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" +) + +func TestConvertPoints(t *testing.T) { + data := "09-29 15:02:08.379 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434928378\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:09.433 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434929432\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:10.452 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434930452\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:11.451 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434931450\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:12.491 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434932489\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:16.028 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434936027\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:21.424 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434941423\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:27.923 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434947922\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:33.628 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434953628\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:39.347 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434959347\tx=1259.5y=1868.5" + eps := ConvertPoints(data) + if len(eps) != 10 { + t.Fatal() + } + jsons, _ := json.Marshal(eps) + println(fmt.Sprintf("%v", string(jsons))) +} diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 858cb2d7..c5c0782b 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -22,6 +22,7 @@ type uiaDriver struct { Driver adbDevice gadb.Device + logcat *DeviceLogcat localPort int } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 93155a76..29642168 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -301,6 +301,45 @@ func (dExt *DriverExt) IsImageExist(text string) bool { return err == nil } +func (dExt *DriverExt) StartLogRecording(identifier string) error { + if _, ok := dExt.Driver.(*wdaDriver); ok { + log.Info().Msg("start WDA log recording") + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} + _, err := dExt.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + } else { + log.Info().Msg("start adb log recording") + err := dExt.Driver.(*uiaDriver).logcat.CatchLogcat() + if err != nil { + return errors.Wrap(err, "failed to start adb log recording") + } + } + return nil +} + +func (dExt *DriverExt) GetLogs() (interface{}, error) { + if _, ok := dExt.Driver.(*wdaDriver); ok { + log.Info().Msg("stop WDA log recording") + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + return reply.Value, nil + } else { + log.Info().Msg("stop adb log recording") + err := dExt.Driver.(*uiaDriver).logcat.Stop() + if err != nil { + println("failed to get adb log recording") + //return "", errors.Wrap(err, "failed to get adb log recording") + } + content := dExt.Driver.(*uiaDriver).logcat.logBuffer.String() + return ConvertPoints(content), err + } +} + var errActionNotImplemented = errors.New("UI action not implemented") func (dExt *DriverExt) DoAction(action MobileAction) error { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index e0a1a985..7b16b405 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -477,7 +477,7 @@ func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, er uuid := device.UUID() // avoid duplicate init - if uuid == "" && len(r.uiClients) == 1 { + if uuid == "" && len(r.uiClients) > 0 { for _, v := range r.uiClients { return v, nil } From 6e237429edcf4697a53fca45fafff446f0bf970d Mon Sep 17 00:00:00 2001 From: xucong053 Date: Fri, 30 Sep 2022 13:17:39 +0800 Subject: [PATCH 096/169] fix: failed to exec command line on windows --- hrp/internal/builtin/utils_unix.go | 5 +++++ hrp/internal/builtin/utils_windows.go | 5 +++++ hrp/internal/uixt/android_device.go | 9 +++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/hrp/internal/builtin/utils_unix.go b/hrp/internal/builtin/utils_unix.go index a23da061..4e5ff1ad 100644 --- a/hrp/internal/builtin/utils_unix.go +++ b/hrp/internal/builtin/utils_unix.go @@ -69,6 +69,11 @@ func ensurePython3Venv(venv string, packages ...string) (python3 string, err err return python3, nil } +func Command(name string, arg ...string) *exec.Cmd { + args := strings.Join(arg, " ") + return exec.Command("bash", "-c", name, args) +} + func ExecCommand(cmdName string, args ...string) error { cmd := exec.Command(cmdName, args...) log.Info().Str("cmd", cmd.String()).Msg("exec command") diff --git a/hrp/internal/builtin/utils_windows.go b/hrp/internal/builtin/utils_windows.go index 062b493b..52aea058 100644 --- a/hrp/internal/builtin/utils_windows.go +++ b/hrp/internal/builtin/utils_windows.go @@ -100,6 +100,11 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err return python3, nil } +func Command(name string, arg ...string) *exec.Cmd { + args := strings.Join(arg, " ") + return exec.Command("cmd", "/c", name, args) +} + func ExecCommand(cmdName string, args ...string) error { // "cmd /c" carries out the command specified by string and then stops // refer: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index a2f170d9..6216b882 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/electricbubble/gadb" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -260,8 +261,8 @@ func (l *DeviceLogcat) CatchLogcat() (err error) { if l.cmd != nil { err = fmt.Errorf("logcat already start") } - command := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial) - l.cmd = exec.Command("bash", "-c", command) + cmdLine := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial) + l.cmd = builtin.Command(cmdLine) l.cmd.Stderr = l.logBuffer l.cmd.Stdout = l.logBuffer l.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} @@ -280,8 +281,8 @@ func (l *DeviceLogcat) CatchLogcat() (err error) { func (l *DeviceLogcat) BufferedLogcat() (err error) { // -d: dump the current buffered logcat result and exits - command := fmt.Sprintf("adb -s %s logcat -d", l.serial) - cmd := exec.Command("bash", "-c", command) + cmdLine := fmt.Sprintf("adb -s %s logcat -d", l.serial) + cmd := builtin.Command(cmdLine) cmd.Stdout = l.logBuffer cmd.Stderr = l.logBuffer if err = cmd.Run(); err != nil { From 7deff77507ddcd99c8bca13b1d612b0ef8271b66 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Fri, 30 Sep 2022 13:45:35 +0800 Subject: [PATCH 097/169] fix: update interface of recording adb logs --- hrp/internal/uixt/android_device.go | 5 +++- hrp/internal/uixt/android_driver.go | 13 +++++++--- hrp/internal/uixt/ext.go | 39 ----------------------------- 3 files changed, 14 insertions(+), 43 deletions(-) diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index 6216b882..f58afb10 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -68,7 +68,10 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { } if device.LogOn { - err = driverExt.StartLogRecording("hrp_adb_log") + err = driverExt.Driver.StartCaptureLog("hrp_adb_log") + if err != nil { + return nil, err + } } return driverExt, err diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index c5c0782b..80385504 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -947,11 +947,18 @@ func (ud *uiaDriver) Wait(condition Condition) error { } func (ud *uiaDriver) StartCaptureLog(identifier ...string) (err error) { - // TODO + log.Info().Msg("start adb log recording") + err = ud.logcat.CatchLogcat() return } func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) { - // TODO - return + log.Info().Msg("stop adb log recording") + err = ud.logcat.Stop() + if err != nil { + log.Error().Err(err).Msg("failed to get adb log recording") + return "", err + } + content := ud.logcat.logBuffer.String() + return ConvertPoints(content), nil } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 29642168..93155a76 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -301,45 +301,6 @@ func (dExt *DriverExt) IsImageExist(text string) bool { return err == nil } -func (dExt *DriverExt) StartLogRecording(identifier string) error { - if _, ok := dExt.Driver.(*wdaDriver); ok { - log.Info().Msg("start WDA log recording") - data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} - _, err := dExt.triggerWDALog(data) - if err != nil { - return errors.Wrap(err, "failed to start WDA log recording") - } - } else { - log.Info().Msg("start adb log recording") - err := dExt.Driver.(*uiaDriver).logcat.CatchLogcat() - if err != nil { - return errors.Wrap(err, "failed to start adb log recording") - } - } - return nil -} - -func (dExt *DriverExt) GetLogs() (interface{}, error) { - if _, ok := dExt.Driver.(*wdaDriver); ok { - log.Info().Msg("stop WDA log recording") - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - return "", errors.Wrap(err, "failed to get WDA logs") - } - return reply.Value, nil - } else { - log.Info().Msg("stop adb log recording") - err := dExt.Driver.(*uiaDriver).logcat.Stop() - if err != nil { - println("failed to get adb log recording") - //return "", errors.Wrap(err, "failed to get adb log recording") - } - content := dExt.Driver.(*uiaDriver).logcat.logBuffer.String() - return ConvertPoints(content), err - } -} - var errActionNotImplemented = errors.New("UI action not implemented") func (dExt *DriverExt) DoAction(action MobileAction) error { From 73e974968b92287c0fea40a4369dd3ca3c0e46ca Mon Sep 17 00:00:00 2001 From: xucong053 Date: Thu, 29 Sep 2022 16:15:56 +0800 Subject: [PATCH 098/169] change: update adb logs --- hrp/internal/uixt/ext.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 93155a76..29642168 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -301,6 +301,45 @@ func (dExt *DriverExt) IsImageExist(text string) bool { return err == nil } +func (dExt *DriverExt) StartLogRecording(identifier string) error { + if _, ok := dExt.Driver.(*wdaDriver); ok { + log.Info().Msg("start WDA log recording") + data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} + _, err := dExt.triggerWDALog(data) + if err != nil { + return errors.Wrap(err, "failed to start WDA log recording") + } + } else { + log.Info().Msg("start adb log recording") + err := dExt.Driver.(*uiaDriver).logcat.CatchLogcat() + if err != nil { + return errors.Wrap(err, "failed to start adb log recording") + } + } + return nil +} + +func (dExt *DriverExt) GetLogs() (interface{}, error) { + if _, ok := dExt.Driver.(*wdaDriver); ok { + log.Info().Msg("stop WDA log recording") + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + return reply.Value, nil + } else { + log.Info().Msg("stop adb log recording") + err := dExt.Driver.(*uiaDriver).logcat.Stop() + if err != nil { + println("failed to get adb log recording") + //return "", errors.Wrap(err, "failed to get adb log recording") + } + content := dExt.Driver.(*uiaDriver).logcat.logBuffer.String() + return ConvertPoints(content), err + } +} + var errActionNotImplemented = errors.New("UI action not implemented") func (dExt *DriverExt) DoAction(action MobileAction) error { From bd1ef254fa9af38e4e5eed20016ea7c0186bfc53 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Fri, 30 Sep 2022 12:00:44 +0800 Subject: [PATCH 099/169] fix: failed to input on android device --- hrp/internal/uixt/android_driver.go | 8 ++++++ hrp/internal/uixt/ext.go | 43 ++--------------------------- hrp/internal/uixt/interface.go | 4 +++ hrp/internal/uixt/ios_driver.go | 4 +++ 4 files changed, 18 insertions(+), 41 deletions(-) diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 80385504..85363014 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -676,6 +676,14 @@ func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { return } +func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { + element, err := ud.FindElement(BySelector{ClassName: ElementType{EditText: true}}) + if err != nil { + return err + } + return element.SendKeys(text) +} + func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { // TODO return errDriverNotImplemented diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 29642168..0970edb2 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -301,45 +301,6 @@ func (dExt *DriverExt) IsImageExist(text string) bool { return err == nil } -func (dExt *DriverExt) StartLogRecording(identifier string) error { - if _, ok := dExt.Driver.(*wdaDriver); ok { - log.Info().Msg("start WDA log recording") - data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier} - _, err := dExt.triggerWDALog(data) - if err != nil { - return errors.Wrap(err, "failed to start WDA log recording") - } - } else { - log.Info().Msg("start adb log recording") - err := dExt.Driver.(*uiaDriver).logcat.CatchLogcat() - if err != nil { - return errors.Wrap(err, "failed to start adb log recording") - } - } - return nil -} - -func (dExt *DriverExt) GetLogs() (interface{}, error) { - if _, ok := dExt.Driver.(*wdaDriver); ok { - log.Info().Msg("stop WDA log recording") - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - return "", errors.Wrap(err, "failed to get WDA logs") - } - return reply.Value, nil - } else { - log.Info().Msg("stop adb log recording") - err := dExt.Driver.(*uiaDriver).logcat.Stop() - if err != nil { - println("failed to get adb log recording") - //return "", errors.Wrap(err, "failed to get adb log recording") - } - content := dExt.Driver.(*uiaDriver).logcat.logBuffer.String() - return ConvertPoints(content), err - } -} - var errActionNotImplemented = errors.New("UI action not implemented") func (dExt *DriverExt) DoAction(action MobileAction) error { @@ -508,9 +469,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { "enable": true, "data": action.Identifier, }) - return dExt.Driver.SendKeys(param, option) + return dExt.Driver.Input(param, option) } - return dExt.Driver.SendKeys(param) + return dExt.Driver.Input(param) case CtlSleep: if param, ok := action.Params.(json.Number); ok { seconds, _ := param.Float64() diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index cbc7c668..c341ac97 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -715,6 +715,7 @@ type ElementType struct { Tab bool `json:"XCUIElementTypeTab"` TouchBar bool `json:"XCUIElementTypeTouchBar"` StatusItem bool `json:"XCUIElementTypeStatusItem"` + EditText bool `json:"android.widget.EditText"` } // ProtectedResource A system resource that requires user authorization to access. @@ -927,6 +928,9 @@ type WebDriver interface { // WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60 SendKeys(text string, options ...DataOption) error + // Input works like SendKeys + Input(text string, options ...DataOption) error + // KeyboardDismiss Tries to dismiss the on-screen keyboard KeyboardDismiss(keyNames ...string) error diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index ac4159f0..7dfbcbde 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -527,6 +527,10 @@ func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) { return } +func (wd *wdaDriver) Input(text string, options ...DataOption) (err error) { + return wd.SendKeys(text, options...) +} + func (wd *wdaDriver) KeyboardDismiss(keyNames ...string) (err error) { // [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)] if len(keyNames) == 0 { From 0bdf8d760b381b390e2171ed9792418d08307684 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 30 Sep 2022 14:11:07 +0800 Subject: [PATCH 100/169] change: override WDA proxy port with environment variable WDA_PROXY_PORT, WDA_PROXY_MJPEG_PORT --- hrp/internal/uixt/ios_device.go | 47 +++++++++++++++------------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 48a4e54e..2c698a8e 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -47,27 +47,6 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { if device.UDID != "" { deviceOptions = append(deviceOptions, WithUDID(device.UDID)) } - - wda_port := os.Getenv("WDA_PORT") - if wda_port != "" { - if port, err := strconv.Atoi(wda_port); err == nil { - log.Info().Str("WDA_PORT", wda_port). - Msg("override with environment variable") - device.Port = port - } else { - log.Error().Err(err).Msg("invalid WDA_PORT, ignored") - } - } - wda_mjpeg_port := os.Getenv("WDA_MJPEG_PORT") - if wda_mjpeg_port != "" { - if mjpeg_port, err := strconv.Atoi(wda_mjpeg_port); err == nil { - log.Info().Str("WDA_MJPEG_PORT", wda_mjpeg_port). - Msg("override with environment variable") - device.MjpegPort = mjpeg_port - } else { - log.Error().Err(err).Msg("invalid WDA_MJPEG_PORT, ignored") - } - } if device.Port != 0 { deviceOptions = append(deviceOptions, WithPort(device.Port)) } @@ -198,8 +177,27 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver wd := new(wdaDriver) wd.client = http.DefaultClient - urlPrefix := fmt.Sprintf("http://127.0.0.1:%d", dev.Port) - if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil { + port := dev.Port + mjpeg_port := dev.MjpegPort + if wda_port := os.Getenv("WDA_PROXY_PORT"); wda_port != "" { + if port, err = strconv.Atoi(wda_port); err == nil { + log.Info().Str("WDA_PROXY_PORT", wda_port). + Msg("override with environment variable") + } else { + log.Error().Err(err).Msg("invalid WDA_PROXY_PORT, ignored") + } + } + if wda_mjpeg_port := os.Getenv("WDA_PROXY_MJPEG_PORT"); wda_mjpeg_port != "" { + if mjpeg_port, err = strconv.Atoi(wda_mjpeg_port); err == nil { + log.Info().Str("WDA_PROXY_MJPEG_PORT", wda_mjpeg_port). + Msg("override with environment variable") + } else { + log.Error().Err(err).Msg("invalid WDA_PROXY_MJPEG_PORT, ignored") + } + } + + host := "127.0.0.1" + if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, port)); err != nil { return nil, err } var sessionInfo SessionInfo @@ -210,8 +208,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver if wd.mjpegHTTPConn, err = net.Dial( "tcp", - fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), - dev.MjpegPort), + fmt.Sprintf("%s:%d", host, mjpeg_port), ); err != nil { return nil, err } From fe111c8c4b042f6a585691b388e6b5cbdfa514d8 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Fri, 30 Sep 2022 14:17:02 +0800 Subject: [PATCH 101/169] fix: failed to open camera on android --- hrp/internal/uixt/android_driver.go | 49 +++++++++++++++++------------ hrp/internal/uixt/ext.go | 4 --- hrp/internal/uixt/interface.go | 3 -- hrp/internal/uixt/ios_driver.go | 10 ------ 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 85363014..ad3ce2a9 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -238,10 +238,37 @@ func (ud *uiaDriver) PressBack() (err error) { } func (ud *uiaDriver) StartCamera() (err error) { - if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil { + if _, err = ud.adbDevice.RunShellCommand("rm", "-r", "/sdcard/DCIM/Camera"); err != nil { return err } - return + time.Sleep(5 * time.Second) + var version string + if version, err = ud.adbDevice.RunShellCommand("getprop", "ro.build.version.release"); err != nil { + return err + } + if version == "11" || version == "12" { + if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.STILL_IMAGE_CAMERA"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "swipe", "750", "1000", "250", "1000"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + return + } else { + if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil { + return err + } + time.Sleep(5 * time.Second) + if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + return + } } func (ud *uiaDriver) StopCamera() (err error) { @@ -266,24 +293,6 @@ func (ud *uiaDriver) StopCamera() (err error) { return } -func (ud *uiaDriver) StartRecording() (err error) { - var res string - if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { - return err - } - log.Info().Str("shell", res) - return -} - -func (ud *uiaDriver) StopRecording() (err error) { - var res string - if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { - return err - } - log.Info().Str("shell", res) - return -} - func (ud *uiaDriver) ActiveAppInfo() (info AppInfo, err error) { // TODO return info, errDriverNotImplemented diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 0970edb2..bf7b6422 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -500,10 +500,6 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return dExt.Driver.StartCamera() case CtlStopCamera: return dExt.Driver.StopCamera() - case RecordStart: - return dExt.Driver.StartRecording() - case RecordStop: - return dExt.Driver.StopRecording() } return nil } diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index c341ac97..4922ba37 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -885,9 +885,6 @@ type WebDriver interface { // StopCamera Stops the camera for recording StopCamera() error - StartRecording() error - StopRecording() error - // Tap Sends a tap event at the coordinate. Tap(x, y int, options ...DataOption) error TapFloat(x, y float64, options ...DataOption) error diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 7dfbcbde..4e4e131f 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -579,16 +579,6 @@ func (wd *wdaDriver) StopCamera() (err error) { return nil } -func (wd *wdaDriver) StartRecording() (err error) { - // TODO - return errDriverNotImplemented -} - -func (wd *wdaDriver) StopRecording() (err error) { - // TODO - return errDriverNotImplemented -} - func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) { // [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)] if len(second) == 0 { From 635898f2c4c27ad61a0f0bd9af665e46c69d9068 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Fri, 30 Sep 2022 15:07:31 +0800 Subject: [PATCH 102/169] fix: update action interface of Android automation --- hrp/step_android_ui.go | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 4db318e7..80f5122a 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -138,11 +138,16 @@ func (s *StepAndroid) TapAbsXY(x, y float64, options ...uixt.ActionOption) *Step return &StepAndroid{step: s.step} } -func (s *StepAndroid) Tap(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +// Tap taps on the target element +func (s *StepAndroid) Tap(params string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Tap, Params: params, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } @@ -244,11 +249,36 @@ func (s *StepAndroid) SwipeRight(options ...uixt.ActionOption) *StepAndroid { return &StepAndroid{step: s.step} } -func (s *StepAndroid) Input(text string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) Input(text string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Input, Params: text, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +// Times specify running times for run last action +func (s *StepAndroid) Times(n int) *StepAndroid { + if n <= 0 { + log.Warn().Int("n", n).Msg("times should be positive, set to 1") + n = 1 + } + + actionsTotal := len(s.step.Android.Actions) + if actionsTotal == 0 { + return s + } + + // actionsTotal >=1 && n >= 1 + lastAction := s.step.Android.Actions[actionsTotal-1 : actionsTotal][0] + for i := 0; i < n-1; i++ { + // duplicate last action n-1 times + s.step.Android.Actions = append(s.step.Android.Actions, lastAction) + } return &StepAndroid{step: s.step} } From 30626575dd3099306c0ada4e1010028e8aa61f06 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 30 Sep 2022 16:14:58 +0800 Subject: [PATCH 103/169] refactor: init WDA HTTP driver --- examples/uitest/demo_douyin_test.go | 2 +- examples/uitest/demo_weixin_test.go | 2 +- examples/uitest/wda_log_test.go | 4 +- hrp/internal/uixt/ios_device.go | 134 +++++++++++++++++----------- hrp/internal/uixt/ios_test.go | 5 +- hrp/internal/version/VERSION | 2 +- hrp/step_ios_ui.go | 10 ++- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 9 files changed, 100 insertions(+), 63 deletions(-) diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index 273be52e..ff42eb86 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -14,7 +14,7 @@ func TestIOSDouyinLive(t *testing.T) { WithVariables(map[string]interface{}{ "app_name": "抖音", }). - SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), + SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). IOS(). diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index c4cd4380..835411c5 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -11,7 +11,7 @@ import ( func TestIOSWeixinLive(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"). - SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), + SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("启动微信"). IOS(). diff --git a/examples/uitest/wda_log_test.go b/examples/uitest/wda_log_test.go index e34a69d3..a0e2b231 100644 --- a/examples/uitest/wda_log_test.go +++ b/examples/uitest/wda_log_test.go @@ -1,4 +1,4 @@ -//go:build !localtest +//go:build localtest package uitest @@ -14,7 +14,7 @@ func TestWDALog(t *testing.T) { WithVariables(map[string]interface{}{ "app_name": "抖音", }). - SetIOS(hrp.WithLogOn(true), hrp.WithPort(8700), hrp.WithMjpegPort(8800)), + SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), TestSteps: []hrp.IStep{ hrp.NewStep("查看时间戳"). IOS(). diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 2c698a8e..960a1ef7 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -43,39 +43,32 @@ const ( ) func InitWDAClient(device *IOSDevice) (*DriverExt, error) { - var deviceOptions []IOSDeviceOption - if device.UDID != "" { - deviceOptions = append(deviceOptions, WithUDID(device.UDID)) - } - if device.Port != 0 { - deviceOptions = append(deviceOptions, WithPort(device.Port)) - } - if device.MjpegPort != 0 { - deviceOptions = append(deviceOptions, WithMjpegPort(device.MjpegPort)) - } - // init wda device - iosDevice, err := NewIOSDevice(deviceOptions...) + iosDevice, err := NewIOSDevice(device.opitons()...) if err != nil { return nil, err } - // switch to iOS springboard before init WDA session - // aviod getting stuck when some super app is activate such as douyin or wexin - log.Info().Msg("switch to iOS springboard") - bundleID := "com.apple.springboard" - _, err = iosDevice.d.AppLaunch(bundleID) - if err != nil { - return nil, errors.Wrap(err, "launch springboard failed") - } - // init WDA driver capabilities := NewCapabilities() capabilities.WithDefaultAlertAction(AlertActionAccept) - driver, err := iosDevice.NewUSBDriver(capabilities) + var driver WebDriver + if iosDevice.LocalPort != 0 && iosDevice.LocalMjpegPort != 0 { + driver, err = iosDevice.NewHTTPDriver(capabilities) + } else { + driver, err = iosDevice.NewUSBDriver(capabilities) + } if err != nil { return nil, errors.Wrap(err, "failed to init WDA driver") } + + // switch to iOS springboard before init WDA session + // aviod getting stuck when some super app is activate such as douyin or wexin + log.Info().Msg("go back to home screen") + if err = driver.Homescreen(); err != nil { + return nil, errors.Wrap(err, "failed to go back to home screen") + } + driverExt, err := Extend(driver) if err != nil { return nil, errors.Wrap(err, "failed to extend WebDriver") @@ -108,18 +101,30 @@ func WithUDID(udid string) IOSDeviceOption { } } -func WithPort(port int) IOSDeviceOption { +func WithWDAPort(port int) IOSDeviceOption { return func(device *IOSDevice) { device.Port = port } } -func WithMjpegPort(port int) IOSDeviceOption { +func WithWDAMjpegPort(port int) IOSDeviceOption { return func(device *IOSDevice) { device.MjpegPort = port } } +func WithWDALocalPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.LocalPort = port + } +} + +func WithWDALocalMjpegPort(port int) IOSDeviceOption { + return func(device *IOSDevice) { + device.LocalMjpegPort = port + } +} + func WithLogOn(logOn bool) IOSDeviceOption { return func(device *IOSDevice) { device.LogOn = logOn @@ -161,43 +166,71 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } type IOSDevice struct { - d giDevice.Device - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + d giDevice.Device + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port + LocalPort int `json:"local_port,omitempty" yaml:"local_port,omitempty"` // WDA local port + LocalMjpegPort int `json:"local_mjpeg_port,omitempty" yaml:"local_mjpeg_port,omitempty"` // WDA local MJPEG port + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } func (dev *IOSDevice) UUID() string { return dev.UDID } +func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { + if dev.UDID != "" { + deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) + } + if dev.MjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) + } + + if wda_port := os.Getenv("WDA_LOCAL_PORT"); wda_port != "" { + if port, err := strconv.Atoi(wda_port); err == nil { + log.Info().Int("WDA_LOCAL_PORT", port). + Msg("override with environment variable") + dev.LocalPort = port + } else { + log.Error().Err(err).Str("WDA_LOCAL_PORT", wda_port). + Msg("invalid WDA_LOCAL_PORT, ignored") + } + } + if wda_mjpeg_port := os.Getenv("WDA_LOCAL_MJPEG_PORT"); wda_mjpeg_port != "" { + if mjpeg_port, err := strconv.Atoi(wda_mjpeg_port); err == nil { + log.Info().Int("WDA_LOCAL_MJPEG_PORT", mjpeg_port). + Msg("override with environment variable") + dev.LocalMjpegPort = mjpeg_port + } else { + log.Error().Err(err).Str("WDA_LOCAL_MJPEG_PORT", wda_mjpeg_port). + Msg("invalid WDA_LOCAL_MJPEG_PORT, ignored") + } + } + if dev.LocalPort != 0 { + deviceOptions = append(deviceOptions, WithWDALocalPort(dev.LocalPort)) + } + if dev.LocalMjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDALocalMjpegPort(dev.LocalMjpegPort)) + } + + return +} + // NewHTTPDriver creates new remote HTTP client, this will also start a new session. +// WDA port and mjpeg port must be proxied to local ports: +// iproxy -u UDID WDA_LOCAL_PORT WDA_PORT +// iproxy -u UDID WDA_LOCAL_MJPEG_PORT WDA_MJPEG_PORT func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { + log.Info().Interface("capabilities", capabilities).Msg("init WDA HTTP driver") wd := new(wdaDriver) wd.client = http.DefaultClient - port := dev.Port - mjpeg_port := dev.MjpegPort - if wda_port := os.Getenv("WDA_PROXY_PORT"); wda_port != "" { - if port, err = strconv.Atoi(wda_port); err == nil { - log.Info().Str("WDA_PROXY_PORT", wda_port). - Msg("override with environment variable") - } else { - log.Error().Err(err).Msg("invalid WDA_PROXY_PORT, ignored") - } - } - if wda_mjpeg_port := os.Getenv("WDA_PROXY_MJPEG_PORT"); wda_mjpeg_port != "" { - if mjpeg_port, err = strconv.Atoi(wda_mjpeg_port); err == nil { - log.Info().Str("WDA_PROXY_MJPEG_PORT", wda_mjpeg_port). - Msg("override with environment variable") - } else { - log.Error().Err(err).Msg("invalid WDA_PROXY_MJPEG_PORT, ignored") - } - } - host := "127.0.0.1" - if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, port)); err != nil { + if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, dev.LocalPort)); err != nil { return nil, err } var sessionInfo SessionInfo @@ -208,7 +241,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver if wd.mjpegHTTPConn, err = net.Dial( "tcp", - fmt.Sprintf("%s:%d", host, mjpeg_port), + fmt.Sprintf("%s:%d", host, dev.LocalMjpegPort), ); err != nil { return nil, err } @@ -219,6 +252,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver // NewUSBDriver creates new client via USB connected device, this will also start a new session. func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { + log.Info().Interface("capabilities", capabilities).Msg("init WDA USB driver") wd := new(wdaDriver) if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go index efc61518..136d4876 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -1,4 +1,5 @@ //go:build localtest + package uixt import ( @@ -42,12 +43,12 @@ func TestNewIOSDevice(t *testing.T) { t.Log(device) } - device, _ = NewIOSDevice(WithPort(8700), WithMjpegPort(8800)) + device, _ = NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800)) if device != nil { t.Log(device) } - device, _ = NewIOSDevice(WithUDID("xxxx"), WithPort(8700), WithMjpegPort(8800)) + device, _ = NewIOSDevice(WithUDID("xxxx"), WithWDAPort(8700), WithWDAMjpegPort(8800)) if device != nil { t.Log(device) } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 0dbff02d..4d1be50b 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09292312 \ No newline at end of file +v4.3.0-beta-09301627 \ No newline at end of file diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7b16b405..07d6cc11 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -11,10 +11,12 @@ import ( ) var ( - WithUDID = uixt.WithUDID - WithPort = uixt.WithPort - WithMjpegPort = uixt.WithMjpegPort - WithLogOn = uixt.WithLogOn + WithUDID = uixt.WithUDID + WithWDAPort = uixt.WithWDAPort + WithWDAMjpegPort = uixt.WithWDAMjpegPort + WithWDALocalPort = uixt.WithWDALocalPort + WithWDALocalMjpegPort = uixt.WithWDALocalMjpegPort + WithLogOn = uixt.WithLogOn ) type IOSStep struct { diff --git a/httprunner/__init__.py b/httprunner/__init__.py index fe307b9d..4768b08d 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09292312" +__version__ = "v4.3.0-beta-09301627" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index b1f1f680..850193fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09292312" +version = "v4.3.0-beta-09301627" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 740e4874878f0b6c06350abc549b1b416f84965b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 30 Sep 2022 16:44:15 +0800 Subject: [PATCH 104/169] change: update logs --- hrp/internal/uixt/ios_device.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 960a1ef7..791e10dc 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -225,11 +225,12 @@ func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { // iproxy -u UDID WDA_LOCAL_PORT WDA_PORT // iproxy -u UDID WDA_LOCAL_MJPEG_PORT WDA_MJPEG_PORT func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { - log.Info().Interface("capabilities", capabilities).Msg("init WDA HTTP driver") + host := "127.0.0.1" + log.Info().Interface("capabilities", capabilities). + Str("host", host).Msg("init WDA HTTP driver") wd := new(wdaDriver) wd.client = http.DefaultClient - host := "127.0.0.1" if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, dev.LocalPort)); err != nil { return nil, err } @@ -252,9 +253,10 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver // NewUSBDriver creates new client via USB connected device, this will also start a new session. func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { - log.Info().Interface("capabilities", capabilities).Msg("init WDA USB driver") - wd := new(wdaDriver) + log.Info().Interface("capabilities", capabilities). + Str("udid", dev.UDID).Msg("init WDA USB driver") + wd := new(wdaDriver) if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { return nil, fmt.Errorf("connect port %d failed: %w", dev.Port, err) From 508db771a2e6c06a47034956a68857c62b109728 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 30 Sep 2022 17:44:01 +0800 Subject: [PATCH 105/169] change: update tests --- hrp/step_android_ui_test.go | 12 +++---- hrp/step_ios_ui_test.go | 62 ++++++++++++++----------------------- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go index 7162eef9..ad02cfe4 100644 --- a/hrp/step_android_ui_test.go +++ b/hrp/step_android_ui_test.go @@ -3,7 +3,6 @@ package hrp import ( - "fmt" "testing" ) @@ -20,11 +19,8 @@ func TestAndroidAction(t *testing.T) { Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), }, } - tCase := testCase.ToTCase() - fmt.Println(tCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 4b60b04c..0767b8d6 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -1,8 +1,8 @@ //go:build localtest + package hrp import ( - "fmt" "testing" ) @@ -20,8 +20,6 @@ func TestIOSSettingsAction(t *testing.T) { IOS().SwipeUp().SwipeUp().SwipeDown(), }, } - fmt.Println(testCase) - err := NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) @@ -40,18 +38,16 @@ func TestIOSSearchApp(t *testing.T) { IOS().Input("抖音\n"), }, } - fmt.Println(testCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSAppLaunch(t *testing.T) { testCase := &TestCase{ Config: NewConfig("启动 & 关闭 App"). - SetIOS(WithPort(8100), WithMjpegPort(9100)), + SetIOS(WithWDAPort(8100), WithWDAMjpegPort(9100)), TestSteps: []IStep{ NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), @@ -63,18 +59,16 @@ func TestIOSAppLaunch(t *testing.T) { IOS().AppLaunchUnattached("com.ss.iphone.article.News"), }, } - fmt.Println(testCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSWeixinLive(t *testing.T) { testCase := &TestCase{ Config: NewConfig("ios ui action on 微信直播"). - SetIOS(WithLogOn(true), WithPort(8100), WithMjpegPort(9100)), + SetIOS(WithLogOn(true), WithWDAPort(8100), WithWDAMjpegPort(9100)), TestSteps: []IStep{ NewStep("启动微信"). IOS(). @@ -86,7 +80,7 @@ func TestIOSWeixinLive(t *testing.T) { NewStep("进入直播页"). IOS(). Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 - TapByOCR("直播"). // 通过 OCR 识别「直播」 + TapByOCR("直播"). // 通过 OCR 识别「直播」 Validate(). AssertLabelExists("直播"), NewStep("向上滑动 5 次"). @@ -95,8 +89,6 @@ func TestIOSWeixinLive(t *testing.T) { SwipeUp().Times(2).ScreenShot(), // 再上划 2 次,截图保存 }, } - fmt.Println(testCase) - err := NewRunner(t).Run(testCase) if err != nil { t.Fatal(err) @@ -117,12 +109,10 @@ func TestIOSCameraPhotoCapture(t *testing.T) { IOS().Tap("PhotoCapture"), }, } - fmt.Println(testCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSCameraVideoCapture(t *testing.T) { @@ -147,12 +137,10 @@ func TestIOSCameraVideoCapture(t *testing.T) { Tap("VideoCapture"), // 停止录像 }, } - fmt.Println(testCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } func TestIOSDouyinAction(t *testing.T) { @@ -168,10 +156,8 @@ func TestIOSDouyinAction(t *testing.T) { IOS().SwipeUp().Times(3).SwipeDown(), }, } - fmt.Println(testCase) - - // err := NewRunner(t).Run(testCase) - // if err != nil { - // t.Fatal(err) - // } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } } From a6e568e73b983c07b21fd88325b2d8e9408441ba Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 30 Sep 2022 20:37:34 +0800 Subject: [PATCH 106/169] feat: create free port for WDA HTTP driver --- hrp/internal/uixt/android_device.go | 7 +- hrp/internal/uixt/ios_device.go | 132 ++++++++++++++++------------ hrp/internal/version/VERSION | 2 +- hrp/step_ios_ui.go | 10 +-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 6 files changed, 87 insertions(+), 68 deletions(-) diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index f58afb10..cfaf09a0 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -13,9 +13,10 @@ import ( "syscall" "github.com/electricbubble/gadb" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) var ( @@ -194,12 +195,12 @@ func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver *uiaD func getFreePort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "localhost:0") if err != nil { - return 0, fmt.Errorf("free port: %w", err) + return 0, errors.Wrap(err, "resolve tcp addr failed") } l, err := net.ListenTCP("tcp", addr) if err != nil { - return 0, fmt.Errorf("free port: %w", err) + return 0, errors.Wrap(err, "listen tcp addr failed") } defer func() { _ = l.Close() }() return l.Addr().(*net.TCPAddr).Port, nil diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 791e10dc..80b19a07 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -5,6 +5,7 @@ import ( "encoding/base64" builtinJSON "encoding/json" "fmt" + "io" "mime" "mime/multipart" "net" @@ -12,8 +13,8 @@ import ( "net/url" "os" "regexp" - "strconv" "strings" + "time" giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" @@ -53,7 +54,9 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { capabilities := NewCapabilities() capabilities.WithDefaultAlertAction(AlertActionAccept) var driver WebDriver - if iosDevice.LocalPort != 0 && iosDevice.LocalMjpegPort != 0 { + + if os.Getenv("WDA_USB_DRIVER") == "" { + // default use http driver driver, err = iosDevice.NewHTTPDriver(capabilities) } else { driver, err = iosDevice.NewUSBDriver(capabilities) @@ -113,18 +116,6 @@ func WithWDAMjpegPort(port int) IOSDeviceOption { } } -func WithWDALocalPort(port int) IOSDeviceOption { - return func(device *IOSDevice) { - device.LocalPort = port - } -} - -func WithWDALocalMjpegPort(port int) IOSDeviceOption { - return func(device *IOSDevice) { - device.LocalMjpegPort = port - } -} - func WithLogOn(logOn bool) IOSDeviceOption { return func(device *IOSDevice) { device.LogOn = logOn @@ -166,19 +157,62 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } type IOSDevice struct { - d giDevice.Device - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port - LocalPort int `json:"local_port,omitempty" yaml:"local_port,omitempty"` // WDA local port - LocalMjpegPort int `json:"local_mjpeg_port,omitempty" yaml:"local_mjpeg_port,omitempty"` // WDA local MJPEG port - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + d giDevice.Device + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } func (dev *IOSDevice) UUID() string { return 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") + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + log.Error().Err(err).Msg("listen tcp error") + return err + } + + go func(listener net.Listener, device giDevice.Device) { + for { + accept, err := listener.Accept() + if err != nil { + log.Error().Err(err).Msg("accept error") + continue + } + + rInnerConn, err := device.NewConnect(remotePort) + if err != nil { + log.Error().Err(err).Msg("connect to device failed") + os.Exit(1) + } + + rConn := rInnerConn.RawConn() + _ = rConn.SetDeadline(time.Time{}) + + go func(lConn net.Conn) { + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(lConn, rConn); err != nil { + log.Error().Err(err).Msg("copy local -> remote") + } + }(lConn, rConn) + go func(lConn, rConn net.Conn) { + if _, err := io.Copy(rConn, lConn); err != nil { + log.Error().Err(err).Msg("copy local <- remote") + } + }(lConn, rConn) + }(accept) + } + }(listener, dev.d) + + return nil +} + func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { if dev.UDID != "" { deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) @@ -189,49 +223,35 @@ func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { if dev.MjpegPort != 0 { deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) } - - if wda_port := os.Getenv("WDA_LOCAL_PORT"); wda_port != "" { - if port, err := strconv.Atoi(wda_port); err == nil { - log.Info().Int("WDA_LOCAL_PORT", port). - Msg("override with environment variable") - dev.LocalPort = port - } else { - log.Error().Err(err).Str("WDA_LOCAL_PORT", wda_port). - Msg("invalid WDA_LOCAL_PORT, ignored") - } - } - if wda_mjpeg_port := os.Getenv("WDA_LOCAL_MJPEG_PORT"); wda_mjpeg_port != "" { - if mjpeg_port, err := strconv.Atoi(wda_mjpeg_port); err == nil { - log.Info().Int("WDA_LOCAL_MJPEG_PORT", mjpeg_port). - Msg("override with environment variable") - dev.LocalMjpegPort = mjpeg_port - } else { - log.Error().Err(err).Str("WDA_LOCAL_MJPEG_PORT", wda_mjpeg_port). - Msg("invalid WDA_LOCAL_MJPEG_PORT, ignored") - } - } - if dev.LocalPort != 0 { - deviceOptions = append(deviceOptions, WithWDALocalPort(dev.LocalPort)) - } - if dev.LocalMjpegPort != 0 { - deviceOptions = append(deviceOptions, WithWDALocalMjpegPort(dev.LocalMjpegPort)) - } - return } // NewHTTPDriver creates new remote HTTP client, this will also start a new session. -// WDA port and mjpeg port must be proxied to local ports: -// iproxy -u UDID WDA_LOCAL_PORT WDA_PORT -// iproxy -u UDID WDA_LOCAL_MJPEG_PORT WDA_MJPEG_PORT func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { - host := "127.0.0.1" + localPort, err := getFreePort() + if err != nil { + return nil, errors.Wrap(err, "get free port failed") + } + if err = dev.forward(localPort, dev.Port); err != nil { + return nil, errors.Wrap(err, "forward tcp port failed") + } + localMjpegPort, err := getFreePort() + if err != nil { + return nil, errors.Wrap(err, "get free port failed") + } + if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil { + return nil, errors.Wrap(err, "forward tcp port failed") + } + log.Info().Interface("capabilities", capabilities). - Str("host", host).Msg("init WDA HTTP driver") + Int("localPort", localPort).Int("localMjpegPort", localMjpegPort). + Msg("init WDA HTTP driver") + wd := new(wdaDriver) wd.client = http.DefaultClient - if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, dev.LocalPort)); err != nil { + host := "127.0.0.1" + if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil { return nil, err } var sessionInfo SessionInfo @@ -242,7 +262,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver if wd.mjpegHTTPConn, err = net.Dial( "tcp", - fmt.Sprintf("%s:%d", host, dev.LocalMjpegPort), + fmt.Sprintf("%s:%d", host, localMjpegPort), ); err != nil { return nil, err } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 4d1be50b..2bdbc37a 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09301627 \ No newline at end of file +v4.3.0-beta-09302036 \ No newline at end of file diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 07d6cc11..d482c0e1 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -11,12 +11,10 @@ import ( ) var ( - WithUDID = uixt.WithUDID - WithWDAPort = uixt.WithWDAPort - WithWDAMjpegPort = uixt.WithWDAMjpegPort - WithWDALocalPort = uixt.WithWDALocalPort - WithWDALocalMjpegPort = uixt.WithWDALocalMjpegPort - WithLogOn = uixt.WithLogOn + WithUDID = uixt.WithUDID + WithWDAPort = uixt.WithWDAPort + WithWDAMjpegPort = uixt.WithWDAMjpegPort + WithLogOn = uixt.WithLogOn ) type IOSStep struct { diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 4768b08d..08477dd7 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09301627" +__version__ = "v4.3.0-beta-09302036" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 850193fd..db1b5924 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09301627" +version = "v4.3.0-beta-09302036" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From bfc7a49669692058b2abc76685ebbcb6dbc0be9f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 14:48:29 +0800 Subject: [PATCH 107/169] feat: print ios devices --- hrp/cmd/ios/devices.go | 134 +++++++++++++++++++++++++++++++++++++++++ hrp/cmd/ios/init.go | 13 ++++ hrp/cmd/root.go | 2 + 3 files changed, 149 insertions(+) create mode 100644 hrp/cmd/ios/devices.go create mode 100644 hrp/cmd/ios/init.go diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go new file mode 100644 index 00000000..8286d6ef --- /dev/null +++ b/hrp/cmd/ios/devices.go @@ -0,0 +1,134 @@ +package ios + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + giDevice "github.com/electricbubble/gidevice" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func wrapError(err error, msg string) error { + if strings.HasSuffix(err.Error(), "InvalidService") { + msg += ", check if Developer Disk Image mounted" + } + return errors.Wrap(err, msg) +} + +type DeviceList []Device + +type Device struct { + d giDevice.Device + UDID string `json:"UDID"` + Status string `json:"status"` + ConnectionType string `json:"connectionType"` + ConnectionSpeed int `json:"connectionSpeed"` + DeviceDetail *DeviceDetail `json:"deviceDetail,omitempty"` +} + +type DeviceDetail struct { + DeviceName string `json:"deviceName,omitempty"` + DeviceClass string `json:"deviceClass,omitempty"` + ProductVersion string `json:"productVersion,omitempty"` + ProductType string `json:"productType,omitempty"` + ProductName string `json:"productName,omitempty"` + PasswordProtected bool `json:"passwordProtected,omitempty"` + ModelNumber string `json:"modelNumber,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + SIMStatus string `json:"simStatus,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` + CPUArchitecture string `json:"cpuArchitecture,omitempty"` + ProtocolVersion string `json:"protocolVersion,omitempty"` + RegionInfo string `json:"regionInfo,omitempty"` + TimeZone string `json:"timeZone,omitempty"` + UniqueDeviceID string `json:"uniqueDeviceID,omitempty"` + WiFiAddress string `json:"wifiAddress,omitempty"` + BuildVersion string `json:"buildVersion,omitempty"` +} + +func (device *Device) GetDetail() (*DeviceDetail, error) { + value, err := device.d.GetValue("", "") + if err != nil { + return nil, errors.Wrap(err, "get device detail failed") + } + detailByte, _ := json.Marshal(value) + detail := &DeviceDetail{} + json.Unmarshal(detailByte, detail) + return detail, nil +} + +func (device *Device) GetStatus() string { + if device.ConnectionType != "" { + return "online" + } else { + return "offline" + } +} + +func (device *Device) ToFormat() string { + result, _ := json.MarshalIndent(device, "", "\t") + return string(result) +} + +var listIOSDevicesCmd = &cobra.Command{ + Use: "devices", + Short: "List all iOS devices", + RunE: func(cmd *cobra.Command, args []string) error { + usbmux, err := giDevice.NewUsbmux() + if err != nil { + return wrapError(err, "create usbmux failed") + } + + devices, err := usbmux.Devices() + if err != nil { + return wrapError(err, "list ios devices failed") + } + + var deviceList []giDevice.Device + // filter by udid + for _, d := range devices { + if udid != "" && udid != d.Properties().SerialNumber { + continue + } + deviceList = append(deviceList, d) + } + if udid != "" && len(deviceList) == 0 { + fmt.Printf("no device found for udid: %s\n", udid) + os.Exit(1) + } + + for _, d := range deviceList { + deviceByte, _ := json.Marshal(d.Properties()) + device := &Device{ + d: d, + } + json.Unmarshal(deviceByte, device) + device.Status = device.GetStatus() + + if isDetail { + device.DeviceDetail, err = device.GetDetail() + if err != nil { + return err + } + fmt.Println(device.ToFormat()) + } else { + fmt.Println(device.UDID, device.ConnectionType, device.Status) + } + } + return nil + }, +} + +var ( + udid string + isDetail bool +) + +func init() { + listIOSDevicesCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listIOSDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") + iosRootCmd.AddCommand(listIOSDevicesCmd) +} diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go new file mode 100644 index 00000000..a4110b64 --- /dev/null +++ b/hrp/cmd/ios/init.go @@ -0,0 +1,13 @@ +package ios + +import "github.com/spf13/cobra" + +var iosRootCmd = &cobra.Command{ + Use: "ios", + Short: "simple utils for ios device management", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, +} + +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(iosRootCmd) +} diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index 4ecb63f8..17ca8ba2 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/httprunner/httprunner/v4/hrp/cmd/ios" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -59,6 +60,7 @@ func Execute() { rootCmd.PersistentFlags().BoolVar(&logJSON, "log-json", false, "set log to json format") rootCmd.PersistentFlags().StringVar(&venv, "venv", "", "specify python3 venv path") + ios.Init(rootCmd) if err := rootCmd.Execute(); err != nil { os.Exit(1) } From 8c0d495c525ee0212aeb556834165a862e7a5cfd Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 15:20:41 +0800 Subject: [PATCH 108/169] feat: print android devices --- hrp/cmd/adb/devices.go | 62 ++++++++++++++++++++++++++++++++++++++++++ hrp/cmd/adb/init.go | 13 +++++++++ hrp/cmd/ios/devices.go | 2 +- hrp/cmd/root.go | 3 ++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 hrp/cmd/adb/devices.go create mode 100644 hrp/cmd/adb/init.go diff --git a/hrp/cmd/adb/devices.go b/hrp/cmd/adb/devices.go new file mode 100644 index 00000000..789bc1e4 --- /dev/null +++ b/hrp/cmd/adb/devices.go @@ -0,0 +1,62 @@ +package adb + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +) + +func format(data map[string]string) string { + result, _ := json.MarshalIndent(data, "", "\t") + return string(result) +} + +var listAndroidDevicesCmd = &cobra.Command{ + Use: "devices", + Short: "List all Android devices", + RunE: func(cmd *cobra.Command, args []string) error { + devices, err := uixt.DeviceList() + if err != nil { + return errors.Wrap(err, "list android devices failed") + } + + var deviceList []gadb.Device + // filter by serial + for _, d := range devices { + if serial != "" && serial != d.Serial() { + continue + } + deviceList = append(deviceList, d) + } + if serial != "" && len(deviceList) == 0 { + fmt.Printf("no android device found for serial: %s\n", serial) + os.Exit(1) + } + + for _, d := range deviceList { + if isDetail { + fmt.Println(format(d.DeviceInfo())) + } else { + fmt.Println(d.Serial(), d.Usb()) + } + } + return nil + }, +} + +var ( + serial string + isDetail bool +) + +func init() { + listAndroidDevicesCmd.Flags().StringVarP(&serial, "serial", "s", "", "filter by device's serial") + listAndroidDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") + androidRootCmd.AddCommand(listAndroidDevicesCmd) +} diff --git a/hrp/cmd/adb/init.go b/hrp/cmd/adb/init.go new file mode 100644 index 00000000..9025ef70 --- /dev/null +++ b/hrp/cmd/adb/init.go @@ -0,0 +1,13 @@ +package adb + +import "github.com/spf13/cobra" + +var androidRootCmd = &cobra.Command{ + Use: "adb", + Short: "simple utils for android device management", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, +} + +func Init(rootCmd *cobra.Command) { + rootCmd.AddCommand(androidRootCmd) +} diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index 8286d6ef..87ae3b84 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -96,7 +96,7 @@ var listIOSDevicesCmd = &cobra.Command{ deviceList = append(deviceList, d) } if udid != "" && len(deviceList) == 0 { - fmt.Printf("no device found for udid: %s\n", udid) + fmt.Printf("no ios device found for udid: %s\n", udid) os.Exit(1) } diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index 17ca8ba2..f7572d10 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/httprunner/httprunner/v4/hrp/cmd/adb" "github.com/httprunner/httprunner/v4/hrp/cmd/ios" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -61,6 +62,8 @@ func Execute() { rootCmd.PersistentFlags().StringVar(&venv, "venv", "", "specify python3 venv path") ios.Init(rootCmd) + adb.Init(rootCmd) + if err := rootCmd.Execute(); err != nil { os.Exit(1) } From d96c599749aa7059fffbd247bdeedd81f9295987 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 17:21:36 +0800 Subject: [PATCH 109/169] feat: print ios apps --- hrp/cmd/ios/apps.go | 53 +++++++++++++++++++++++++++++++++ hrp/cmd/ios/devices.go | 41 ++++++++----------------- hrp/internal/uixt/ios_device.go | 38 +++++++++++++++-------- 3 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 hrp/cmd/ios/apps.go diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go new file mode 100644 index 00000000..ed8d6063 --- /dev/null +++ b/hrp/cmd/ios/apps.go @@ -0,0 +1,53 @@ +package ios + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +) + +var listIOSAppsCmd = &cobra.Command{ + Use: "apps", + Short: "List all iOS installed apps", + RunE: func(cmd *cobra.Command, args []string) error { + devices, err := uixt.IOSDevices(udid) + if err != nil { + return err + } + if len(devices) == 0 { + fmt.Println("no ios device found") + os.Exit(1) + } + if len(devices) > 1 { + return fmt.Errorf("multiple devices found, please specify udid") + } + device := devices[0] + + apps, err := device.AppList() + if err != nil { + return errors.Wrap(err, "get ios apps failed") + } + for _, app := range apps { + if appType != "all" && strings.ToLower(app.Type) != appType { + continue + } + + fmt.Printf("%-10.10s %-30.30s %-50.50s %-s\n", + app.Type, app.DisplayName, app.CFBundleIdentifier, app.Version) + } + return nil + }, +} + +var appType string + +func init() { + listIOSAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listIOSAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|pluginkit|all]") + iosRootCmd.AddCommand(listIOSAppsCmd) +} diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index 87ae3b84..f10736e4 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -4,22 +4,14 @@ import ( "encoding/json" "fmt" "os" - "strings" giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -func wrapError(err error, msg string) error { - if strings.HasSuffix(err.Error(), "InvalidService") { - msg += ", check if Developer Disk Image mounted" - } - return errors.Wrap(err, msg) -} - -type DeviceList []Device - type Device struct { d giDevice.Device UDID string `json:"UDID"` @@ -77,30 +69,21 @@ var listIOSDevicesCmd = &cobra.Command{ Use: "devices", Short: "List all iOS devices", RunE: func(cmd *cobra.Command, args []string) error { - usbmux, err := giDevice.NewUsbmux() + devices, err := uixt.IOSDevices(udid) if err != nil { - return wrapError(err, "create usbmux failed") + return err } - - devices, err := usbmux.Devices() - if err != nil { - return wrapError(err, "list ios devices failed") - } - - var deviceList []giDevice.Device - // filter by udid - for _, d := range devices { - if udid != "" && udid != d.Properties().SerialNumber { - continue + if len(devices) == 0 { + if udid != "" { + fmt.Printf("no ios device found for udid: %s\n", udid) + os.Exit(1) + } else { + fmt.Println("no ios device found") + os.Exit(0) } - deviceList = append(deviceList, d) - } - if udid != "" && len(deviceList) == 0 { - fmt.Printf("no ios device found for udid: %s\n", udid) - os.Exit(1) } - for _, d := range deviceList { + for _, d := range devices { deviceByte, _ := json.Marshal(d.Properties()) device := &Device{ d: d, diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 80b19a07..95228062 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -122,17 +122,31 @@ func WithLogOn(logOn bool) IOSDeviceOption { } } -func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { +func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { var usbmux giDevice.Usbmux if usbmux, err = giDevice.NewUsbmux(); err != nil { return nil, fmt.Errorf("init usbmux failed: %v", err) } - var deviceList []giDevice.Device - if deviceList, err = usbmux.Devices(); err != nil { - return nil, fmt.Errorf("get attached devices failed: %v", err) + if devices, err = usbmux.Devices(); err != nil { + return nil, fmt.Errorf("list ios devices failed: %v", err) } + // filter by udid + var deviceList []giDevice.Device + for _, d := range devices { + for _, u := range udid { + if u != "" && u != d.Properties().SerialNumber { + continue + } + deviceList = append(deviceList, d) + } + } + + return deviceList, nil +} + +func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { device = &IOSDevice{ Port: defaultWDAPort, MjpegPort: defaultMjpegPort, @@ -141,15 +155,15 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { option(device) } - serialNumber := device.UDID - for _, dev := range deviceList { - // find device by serial number if specified - if serialNumber != "" && dev.Properties().SerialNumber != serialNumber { - continue - } + deviceList, err := IOSDevices(device.UDID) + if err != nil { + return nil, err + } - device.UDID = dev.Properties().SerialNumber - device.d = dev + if len(deviceList) > 0 { + device.UDID = deviceList[0].Properties().SerialNumber + log.Info().Str("udid", device.UDID).Msg("select device") + device.d = deviceList[0] return device, nil } From fdc9f382d4a2b4507780433487e1e54f8c3a7b83 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 22:40:29 +0800 Subject: [PATCH 110/169] feat: run ios xctest --- hrp/cmd/adb/init.go | 5 ++-- hrp/cmd/ios/init.go | 5 ++-- hrp/cmd/ios/xctest.go | 67 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 hrp/cmd/ios/xctest.go diff --git a/hrp/cmd/adb/init.go b/hrp/cmd/adb/init.go index 9025ef70..2914cc64 100644 --- a/hrp/cmd/adb/init.go +++ b/hrp/cmd/adb/init.go @@ -3,9 +3,8 @@ package adb import "github.com/spf13/cobra" var androidRootCmd = &cobra.Command{ - Use: "adb", - Short: "simple utils for android device management", - PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + Use: "adb", + Short: "simple utils for android device management", } func Init(rootCmd *cobra.Command) { diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index a4110b64..e66898a9 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -3,9 +3,8 @@ package ios import "github.com/spf13/cobra" var iosRootCmd = &cobra.Command{ - Use: "ios", - Short: "simple utils for ios device management", - PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + Use: "ios", + Short: "simple utils for ios device management", } func Init(rootCmd *cobra.Command) { diff --git a/hrp/cmd/ios/xctest.go b/hrp/cmd/ios/xctest.go new file mode 100644 index 00000000..9300c6e1 --- /dev/null +++ b/hrp/cmd/ios/xctest.go @@ -0,0 +1,67 @@ +package ios + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +) + +var xctestCmd = &cobra.Command{ + Use: "xctest", + Short: "run xctest", + RunE: func(cmd *cobra.Command, args []string) error { + if bundleID == "" { + return fmt.Errorf("bundleID is required") + } + + devices, err := uixt.IOSDevices(udid) + if err != nil { + return err + } + if len(devices) == 0 { + fmt.Println("no ios device found") + os.Exit(1) + } + if len(devices) > 1 { + return fmt.Errorf("multiple devices found, please specify udid") + } + device := devices[0] + + log.Info().Str("bundleID", bundleID).Msg("run xctest") + out, cancel, err := device.XCTest(bundleID) + if err != nil { + return errors.Wrap(err, "run xctest failed") + } + + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGTERM, syscall.SIGINT) + + // print xctest running logs + go func() { + for s := range out { + fmt.Print(s) + } + done <- os.Interrupt + }() + + <-done + cancel() + + return nil + }, +} + +var bundleID string + +func init() { + xctestCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + xctestCmd.Flags().StringVarP(&bundleID, "bundleID", "b", "", "specify ios bundleID") + iosRootCmd.AddCommand(xctestCmd) +} From 0a051a1f3435b397f06d31b8d259280e0df9f282 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 23:46:24 +0800 Subject: [PATCH 111/169] feat: get ios running processes by ps --- hrp/cmd/ios/apps.go | 21 ++++----------- hrp/cmd/ios/devices.go | 8 +++--- hrp/cmd/ios/init.go | 25 +++++++++++++++++- hrp/cmd/ios/ps.go | 60 ++++++++++++++++++++++++++++++++++++++++++ hrp/cmd/ios/xctest.go | 13 +-------- 5 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 hrp/cmd/ios/ps.go diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go index ed8d6063..5021a422 100644 --- a/hrp/cmd/ios/apps.go +++ b/hrp/cmd/ios/apps.go @@ -2,31 +2,20 @@ package ios import ( "fmt" - "os" "strings" "github.com/pkg/errors" "github.com/spf13/cobra" - - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) -var listIOSAppsCmd = &cobra.Command{ +var listAppsCmd = &cobra.Command{ Use: "apps", Short: "List all iOS installed apps", RunE: func(cmd *cobra.Command, args []string) error { - devices, err := uixt.IOSDevices(udid) + device, err := getDevice(udid) if err != nil { return err } - if len(devices) == 0 { - fmt.Println("no ios device found") - os.Exit(1) - } - if len(devices) > 1 { - return fmt.Errorf("multiple devices found, please specify udid") - } - device := devices[0] apps, err := device.AppList() if err != nil { @@ -47,7 +36,7 @@ var listIOSAppsCmd = &cobra.Command{ var appType string func init() { - listIOSAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") - listIOSAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|pluginkit|all]") - iosRootCmd.AddCommand(listIOSAppsCmd) + listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|pluginkit|all]") + iosRootCmd.AddCommand(listAppsCmd) } diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index f10736e4..ec884e19 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -65,7 +65,7 @@ func (device *Device) ToFormat() string { return string(result) } -var listIOSDevicesCmd = &cobra.Command{ +var listDevicesCmd = &cobra.Command{ Use: "devices", Short: "List all iOS devices", RunE: func(cmd *cobra.Command, args []string) error { @@ -111,7 +111,7 @@ var ( ) func init() { - listIOSDevicesCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") - listIOSDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") - iosRootCmd.AddCommand(listIOSDevicesCmd) + listDevicesCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listDevicesCmd.Flags().BoolVarP(&isDetail, "detail", "d", false, "print device's detail") + iosRootCmd.AddCommand(listDevicesCmd) } diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index e66898a9..8cfea1f3 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -1,12 +1,35 @@ package ios -import "github.com/spf13/cobra" +import ( + "fmt" + "os" + + giDevice "github.com/electricbubble/gidevice" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +) var iosRootCmd = &cobra.Command{ Use: "ios", Short: "simple utils for ios device management", } +func getDevice(udid string) (giDevice.Device, error) { + devices, err := uixt.IOSDevices(udid) + if err != nil { + return nil, err + } + if len(devices) == 0 { + fmt.Println("no ios device found") + os.Exit(1) + } + if len(devices) > 1 { + return nil, fmt.Errorf("multiple devices found, please specify udid") + } + return devices[0], nil +} + func Init(rootCmd *cobra.Command) { rootCmd.AddCommand(iosRootCmd) } diff --git a/hrp/cmd/ios/ps.go b/hrp/cmd/ios/ps.go new file mode 100644 index 00000000..22f5a94b --- /dev/null +++ b/hrp/cmd/ios/ps.go @@ -0,0 +1,60 @@ +package ios + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var psCmd = &cobra.Command{ + Use: "ps", + Short: "show running processes", + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + apps, err := device.AppList() + if err != nil { + return errors.Wrap(err, "get ios apps failed") + } + + maxNameLen := 0 + mapper := make(map[string]interface{}) + for _, app := range apps { + mapper[app.ExecutableName] = app.CFBundleIdentifier + if len(app.ExecutableName) > maxNameLen { + maxNameLen = len(app.ExecutableName) + } + } + + runningProcesses, err := device.AppRunningProcesses() + if err != nil { + return errors.Wrap(err, "get running processes failed") + } + for _, p := range runningProcesses { + if !isAll && !p.IsApplication { + continue + } + bundleID, ok := mapper[p.Name] + if !ok { + bundleID = "" + } + + fmt.Printf("%4d %-"+fmt.Sprintf("%d", maxNameLen)+"s %20s %s\n", + p.Pid, p.Name, time.Since(p.StartDate).String(), bundleID) + } + return nil + }, +} + +var isAll bool + +func init() { + psCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + psCmd.Flags().BoolVarP(&isAll, "all", "a", false, "print all processes including system processes") + iosRootCmd.AddCommand(psCmd) +} diff --git a/hrp/cmd/ios/xctest.go b/hrp/cmd/ios/xctest.go index 9300c6e1..e2b8d343 100644 --- a/hrp/cmd/ios/xctest.go +++ b/hrp/cmd/ios/xctest.go @@ -9,8 +9,6 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) var xctestCmd = &cobra.Command{ @@ -20,19 +18,10 @@ var xctestCmd = &cobra.Command{ if bundleID == "" { return fmt.Errorf("bundleID is required") } - - devices, err := uixt.IOSDevices(udid) + device, err := getDevice(udid) if err != nil { return err } - if len(devices) == 0 { - fmt.Println("no ios device found") - os.Exit(1) - } - if len(devices) > 1 { - return fmt.Errorf("multiple devices found, please specify udid") - } - device := devices[0] log.Info().Str("bundleID", bundleID).Msg("run xctest") out, cancel, err := device.XCTest(bundleID) From 942c7d2b2b92fecd95084167e5cc65d6ecfa78a7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 1 Oct 2022 23:58:48 +0800 Subject: [PATCH 112/169] feat: reboot or shutdown ios device --- hrp/cmd/adb/init.go | 5 +++-- hrp/cmd/ios/init.go | 5 +++-- hrp/cmd/ios/reboot.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 hrp/cmd/ios/reboot.go diff --git a/hrp/cmd/adb/init.go b/hrp/cmd/adb/init.go index 2914cc64..9025ef70 100644 --- a/hrp/cmd/adb/init.go +++ b/hrp/cmd/adb/init.go @@ -3,8 +3,9 @@ package adb import "github.com/spf13/cobra" var androidRootCmd = &cobra.Command{ - Use: "adb", - Short: "simple utils for android device management", + Use: "adb", + Short: "simple utils for android device management", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, } func Init(rootCmd *cobra.Command) { diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index 8cfea1f3..22642744 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -11,8 +11,9 @@ import ( ) var iosRootCmd = &cobra.Command{ - Use: "ios", - Short: "simple utils for ios device management", + Use: "ios", + Short: "simple utils for ios device management", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, } func getDevice(udid string) (giDevice.Device, error) { diff --git a/hrp/cmd/ios/reboot.go b/hrp/cmd/ios/reboot.go new file mode 100644 index 00000000..3cddb6c0 --- /dev/null +++ b/hrp/cmd/ios/reboot.go @@ -0,0 +1,37 @@ +package ios + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var rebootCmd = &cobra.Command{ + Use: "reboot", + Short: "reboot or shutdown ios device", + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + if isShutdown { + err = device.Shutdown() + } else { + err = device.Reboot() + } + if err != nil { + return err + } + fmt.Printf("reboot %s success\n", device.Properties().UDID) + return nil + }, +} + +var isShutdown bool + +func init() { + rebootCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + rebootCmd.Flags().BoolVarP(&isShutdown, "shutdown", "s", false, "shutdown ios device") + iosRootCmd.AddCommand(rebootCmd) +} From 2dd524ac05212919888b58767a40fe1298ea8272 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 8 Oct 2022 12:36:25 +0800 Subject: [PATCH 113/169] change: bump verison --- converter.py | 176 +++++++++++ demo/.debugtalk_gen.py | 23 ++ demo/.env | 3 + demo/.gitignore | 14 + demo/debugtalk.py | 62 ++++ demo/har/.keep | 0 demo/proj.json | 5 + demo/testcases/demo.json | 176 +++++++++++ demo/testcases/ref_testcase.yml | 33 ++ demo/testcases/requests.json | 136 ++++++++ demo/testcases/requests.yml | 62 ++++ examples/data/postman/__init__.py | 1 + .../data/postman/postman_collection_test.json | 81 +++++ .../postman/postman_collection_test_test.py | 55 ++++ examples/postman_echo/.debugtalk_gen.py | 20 ++ go.mod | 1 + go.sum | 4 +- google_style.py | 297 ++++++++++++++++++ hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- httprunner/step_android.py | 136 ++++++++ httprunner/step_android_test.py | 42 +++ main.go | 59 ++++ pyproject.toml | 2 +- 24 files changed, 1387 insertions(+), 5 deletions(-) create mode 100644 converter.py create mode 100644 demo/.debugtalk_gen.py create mode 100644 demo/.env create mode 100644 demo/.gitignore create mode 100644 demo/debugtalk.py create mode 100644 demo/har/.keep create mode 100644 demo/proj.json create mode 100644 demo/testcases/demo.json create mode 100644 demo/testcases/ref_testcase.yml create mode 100644 demo/testcases/requests.json create mode 100644 demo/testcases/requests.yml create mode 100644 examples/data/postman/__init__.py create mode 100644 examples/data/postman/postman_collection_test.json create mode 100644 examples/data/postman/postman_collection_test_test.py create mode 100644 examples/postman_echo/.debugtalk_gen.py create mode 100644 google_style.py create mode 100644 httprunner/step_android.py create mode 100644 httprunner/step_android_test.py create mode 100644 main.go diff --git a/converter.py b/converter.py new file mode 100644 index 00000000..a54bc377 --- /dev/null +++ b/converter.py @@ -0,0 +1,176 @@ +import io +import json +import pprint +import re +import textwrap +from typing import Any + +from mitmproxy import http +from mitmproxy.utils import strutils + + +def curl_command(flow: http.HTTPFlow) -> str: + data = "curl " + + request = flow.request.copy() + request.decode(strict=False) + + for k, v in request.headers.items(multi=True): + data += "-H '%s:%s' " % (k, v) + + if request.method != "GET": + data += "-X %s " % request.method + + data += "'%s'" % request.url + + if request.content: + data += " --data-binary '%s'" % strutils.bytes_to_escaped_str( + request.content, escape_single_quotes=True + ) + + return data + + +def python_arg(arg: str, val: Any) -> str: + if not val: + return "" + if arg: + arg += "=" + arg_str = "{}{},\n".format(arg, pprint.pformat(val, 79 - len(arg))) + return textwrap.indent(arg_str, " " * 4) + + +def python_code(flow: http.HTTPFlow): + code = io.StringIO() + + def writearg(arg, val): + code.write(python_arg(arg, val)) + + code.write("import requests\n") + code.write("\n") + if flow.request.method.lower() in ("get", "post", "put", "head", "delete", "patch"): + code.write("response = requests.{}(\n".format(flow.request.method.lower())) + else: + code.write("response = requests.request(\n") + writearg("", flow.request.method) + url_without_query = flow.request.url.split("?", 1)[0] + writearg("", url_without_query) + + writearg("params", list(flow.request.query.fields)) + + headers = flow.request.headers.copy() + # requests adds those by default. + for x in (":authority", "host", "content-length"): + headers.pop(x, None) + writearg("headers", dict(headers)) + try: + if "json" not in flow.request.headers.get("content-type", ""): + raise ValueError() + writearg("json", json.loads(flow.request.text)) + except ValueError: + writearg("data", flow.request.content) + + code.seek(code.tell() - 2) # remove last comma + code.write("\n)\n") + code.write("\n") + code.write("print(response.text)") + + return code.getvalue() + + +def locust_code(flow): + code = textwrap.dedent( + """ + from locust import HttpLocust, TaskSet, task + class UserBehavior(TaskSet): + def on_start(self): + ''' on_start is called when a Locust start before any task is scheduled ''' + self.{name}() + @task() + def {name}(self): + url = self.locust.host + '{path}' + {headers}{params}{data} + self.response = self.client.request( + method='{method}', + url=url,{args} + ) + ### Additional tasks can go here ### + class WebsiteUser(HttpLocust): + task_set = UserBehavior + min_wait = 1000 + max_wait = 3000 +""" + ).strip() + + name = re.sub("\W|^(?=\d)", "_", flow.request.path.strip("/").split("?", 1)[0]) + if not name: + new_name = "_".join([str(flow.request.host), str(flow.request.timestamp_start)]) + name = re.sub("\W|^(?=\d)", "_", new_name) + + path_without_query = flow.request.path.split("?")[0] + + args = "" + headers = "" + + def conv(x): + return strutils.bytes_to_escaped_str(x, escape_single_quotes=True) + + if flow.request.headers: + lines = [ + (conv(k), conv(v)) + for k, v in flow.request.headers.fields + if conv(k).lower() not in [":authority", "host", "cookie"] + ] + lines = [" '%s': '%s',\n" % (k, v) for k, v in lines] + headers += "\n headers = {\n%s }\n" % "".join(lines) + args += "\n headers=headers," + + params = "" + if flow.request.query: + lines = [ + " %s: %s,\n" % (repr(k), repr(v)) + for k, v in flow.request.query.collect() + ] + params = "\n params = {\n%s }\n" % "".join(lines) + args += "\n params=params," + + data = "" + if flow.request.content: + data = "\n data = '''%s'''\n" % conv(flow.request.content) + args += "\n data=data," + + code = code.format( + name=name, + path=path_without_query, + headers=headers, + params=params, + data=data, + method=flow.request.method, + args=args, + ) + + return code + + +def locust_task(flow): + code = locust_code(flow) + start_task = len(code.split("@task")[0]) - 4 + end_task = -19 - len(code.split("### Additional")[1]) + task_code = code[start_task:end_task] + + return task_code + + +def url(flow): + return flow.request.url + + +EXPORTERS = [ + ("content", "c", None), + ("headers+content", "h", None), + ("url", "u", url), + ("as curl command", "r", curl_command), + ("as python code", "p", python_code), + ("as locust code", "l", locust_code), + ("as locust task", "t", locust_task), +] diff --git a/demo/.debugtalk_gen.py b/demo/.debugtalk_gen.py new file mode 100644 index 00000000..a57fef72 --- /dev/null +++ b/demo/.debugtalk_gen.py @@ -0,0 +1,23 @@ +# NOTE: Generated By hrp v4.1.4, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_user_agent", get_user_agent) + funppy.register("sleep", sleep) + funppy.register("sum", sum) + funppy.register("sum_ints", sum_ints) + funppy.register("sum_two_int", sum_two_int) + funppy.register("sum_two_string", sum_two_string) + funppy.register("sum_strings", sum_strings) + funppy.register("concatenate", concatenate) + funppy.register("setup_hook_example", setup_hook_example) + funppy.register("teardown_hook_example", teardown_hook_example) + funppy.serve() diff --git a/demo/.env b/demo/.env new file mode 100644 index 00000000..59ecc742 --- /dev/null +++ b/demo/.env @@ -0,0 +1,3 @@ +base_url=https://postman-echo.com +USERNAME=debugtalk +PASSWORD=123456 \ No newline at end of file diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..4c8cb60c --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,14 @@ +reports/ +*.so +.vscode/ +.idea/ +.DS_Store +output/ +__pycache__/ +*.pyc +.python-version +logs/ + +# plugin +debugtalk.bin +debugtalk.so diff --git a/demo/debugtalk.py b/demo/debugtalk.py new file mode 100644 index 00000000..334a46c7 --- /dev/null +++ b/demo/debugtalk.py @@ -0,0 +1,62 @@ +import logging +import time +from typing import List + + +# commented out function will be filtered +# def get_headers(): +# return {"User-Agent": "hrp"} + + +def get_user_agent(): + return "hrp/funppy" + + +def sleep(n_secs): + time.sleep(n_secs) + + +def sum(*args): + result = 0 + for arg in args: + result += arg + return result + + +def sum_ints(*args: List[int]) -> int: + result = 0 + for arg in args: + result += arg + return result + + +def sum_two_int(a: int, b: int) -> int: + return a + b + + +def sum_two_string(a: str, b: str) -> str: + return a + b + + +def sum_strings(*args: List[str]) -> str: + result = "" + for arg in args: + result += arg + return result + + +def concatenate(*args: List[str]) -> str: + result = "" + for arg in args: + result += str(arg) + return result + + +def setup_hook_example(name): + logging.warning("setup_hook_example") + return f"setup_hook_example: {name}" + + +def teardown_hook_example(name): + logging.warning("teardown_hook_example") + return f"teardown_hook_example: {name}" diff --git a/demo/har/.keep b/demo/har/.keep new file mode 100644 index 00000000..e69de29b diff --git a/demo/proj.json b/demo/proj.json new file mode 100644 index 00000000..08277e5f --- /dev/null +++ b/demo/proj.json @@ -0,0 +1,5 @@ +{ + "project_name": "demo", + "create_time": "2022-06-23T11:15:39.635136+08:00", + "hrp_version": "v4.1.4" +} diff --git a/demo/testcases/demo.json b/demo/testcases/demo.json new file mode 100644 index 00000000..8e50e2aa --- /dev/null +++ b/demo/testcases/demo.json @@ -0,0 +1,176 @@ +{ + "config": { + "name": "demo with complex mechanisms", + "base_url": "https://postman-echo.com", + "variables": { + "a": "${sum(10, 2.3)}", + "b": 3.45, + "n": "${sum_ints(1, 2, 2)}", + "varFoo1": "${gen_random_string($n)}", + "varFoo2": "${max($a, $b)}" + } + }, + "teststeps": [ + { + "name": "transaction 1 start", + "transaction": { + "name": "tran1", + "type": "start" + } + }, + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$varFoo1", + "foo2": "$varFoo2" + }, + "headers": { + "User-Agent": "HttpRunnerPlus" + } + }, + "variables": { + "b": 34.5, + "n": 3, + "name": "get with params", + "varFoo2": "${max($a, $b)}" + }, + "setup_hooks": [ + "${setup_hook_example($name)}" + ], + "teardown_hooks": [ + "${teardown_hook_example($name)}" + ], + "extract": { + "varFoo1": "body.args.foo1" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check response status code" + }, + { + "check": "headers.\"Content-Type\"", + "assert": "startswith", + "expect": "application/json" + }, + { + "check": "body.args.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "$varFoo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.args.foo2", + "assert": "equals", + "expect": "34.5", + "msg": "check args foo2" + } + ] + }, + { + "name": "transaction 1 end", + "transaction": { + "name": "tran1", + "type": "end" + } + }, + { + "name": "post json data", + "request": { + "method": "POST", + "url": "/post", + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}" + } + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.json.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.json.foo2", + "assert": "equals", + "expect": 12.3, + "msg": "check args foo2" + } + ] + }, + { + "name": "post form data", + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + "body": { + "foo1": "$varFoo1", + "foo2": "${max($a, $b)}", + "time": "${get_timestamp()}" + } + }, + "extract": { + "varTime": "body.form.time" + }, + "validate": [ + { + "check": "status_code", + "assert": "equals", + "expect": 200, + "msg": "check status code" + }, + { + "check": "body.form.foo1", + "assert": "length_equals", + "expect": 5, + "msg": "check args foo1" + }, + { + "check": "body.form.foo2", + "assert": "equals", + "expect": "12.3", + "msg": "check args foo2" + } + ] + }, + { + "name": "get with timestamp", + "request": { + "method": "GET", + "url": "/get", + "params": { + "time": "$varTime" + } + }, + "validate": [ + { + "check": "body.args.time", + "assert": "length_equals", + "expect": 13, + "msg": "check extracted var timestamp" + } + ] + } + ] +} diff --git a/demo/testcases/ref_testcase.yml b/demo/testcases/ref_testcase.yml new file mode 100644 index 00000000..c0932124 --- /dev/null +++ b/demo/testcases/ref_testcase.yml @@ -0,0 +1,33 @@ +config: + name: "request methods testcase: reference testcase" + variables: + foo1: testsuite_config_bar1 + expect_foo1: testsuite_config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: request with functions + variables: + foo1: testcase_ref_bar1 + expect_foo1: testcase_ref_bar1 + testcase: testcases/requests.yml + export: + - foo3 +- + name: post form data + variables: + foo1: bar1 + request: + method: POST + url: /post + headers: + User-Agent: ${get_user_agent()} + Content-Type: "application/x-www-form-urlencoded" + body: "foo1=$foo1&foo2=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "bar1"] + - eq: ["body.form.foo2", "bar21"] \ No newline at end of file diff --git a/demo/testcases/requests.json b/demo/testcases/requests.json new file mode 100644 index 00000000..4c01ec74 --- /dev/null +++ b/demo/testcases/requests.json @@ -0,0 +1,136 @@ +{ + "config": { + "name": "request methods testcase with functions", + "variables": { + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2" + }, + "headers": { + "User-Agent": "${get_user_agent()}" + }, + "base_url": "https://postman-echo.com", + "verify": false, + "export": [ + "foo3" + ] + }, + "teststeps": [ + { + "name": "get with params", + "variables": { + "foo1": "${ENV(USERNAME)}", + "foo2": "bar21", + "sum_v": "${sum_two_int(10000000, 20000000)}" + }, + "request": { + "method": "GET", + "url": "/get", + "params": { + "foo1": "$foo1", + "foo2": "$foo2", + "sum_v": "$sum_v" + } + }, + "extract": { + "foo3": "body.args.foo2" + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.args.foo1", + "assert": "equal", + "expect": "debugtalk", + "msg": "check body.args.foo1" + }, + { + "check": "body.args.sum_v", + "assert": "equal", + "expect": "30000000", + "msg": "check body.args.sum_v" + }, + { + "check": "body.args.foo2", + "assert": "equal", + "expect": "bar21", + "msg": "check body.args.foo2" + } + ] + }, + { + "name": "post raw text", + "variables": { + "foo1": "bar12", + "foo3": "bar32" + }, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "text/plain" + }, + "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.data", + "assert": "equal", + "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.", + "msg": "check body.data" + } + ] + }, + { + "name": "post form data", + "variables": { + "foo2": "bar23" + }, + "request": { + "method": "POST", + "url": "/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3" + }, + "validate": [ + { + "check": "status_code", + "assert": "equal", + "expect": 200, + "msg": "check status_code" + }, + { + "check": "body.form.foo1", + "assert": "equal", + "expect": "$expect_foo1", + "msg": "check body.form.foo1" + }, + { + "check": "body.form.foo2", + "assert": "equal", + "expect": "bar23", + "msg": "check body.form.foo2" + }, + { + "check": "body.form.foo3", + "assert": "equal", + "expect": "bar21", + "msg": "check body.form.foo3" + } + ] + } + ] +} \ No newline at end of file diff --git a/demo/testcases/requests.yml b/demo/testcases/requests.yml new file mode 100644 index 00000000..5922ab12 --- /dev/null +++ b/demo/testcases/requests.yml @@ -0,0 +1,62 @@ +config: + name: "request methods testcase with functions" + variables: + foo1: config_bar1 + foo2: config_bar2 + expect_foo1: config_bar1 + expect_foo2: config_bar2 + headers: + User-Agent: ${get_user_agent()} + verify: False + export: ["foo3"] + +teststeps: +- + name: get with params + variables: + foo1: ${ENV(USERNAME)} + foo2: bar21 + sum_v: "${sum_two_int(10000000, 20000000)}" + request: + method: GET + url: $base_url/get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + extract: + foo3: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "debugtalk"] + - eq: ["body.args.sum_v", "30000000"] + - eq: ["body.args.foo2", "bar21"] +- + name: post raw text + variables: + foo1: "bar12" + foo3: "bar32" + request: + method: POST + url: $base_url/post + headers: + Content-Type: "text/plain" + body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] +- + name: post form data + variables: + foo2: bar23 + request: + method: POST + url: $base_url/post + headers: + Content-Type: "application/x-www-form-urlencoded" + body: "foo1=$foo1&foo2=$foo2&foo3=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "$expect_foo1"] + - eq: ["body.form.foo2", "bar23"] + - eq: ["body.form.foo3", "bar21"] diff --git a/examples/data/postman/__init__.py b/examples/data/postman/__init__.py new file mode 100644 index 00000000..70cfba53 --- /dev/null +++ b/examples/data/postman/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/data/postman/postman_collection_test.json b/examples/data/postman/postman_collection_test.json new file mode 100644 index 00000000..8592d93b --- /dev/null +++ b/examples/data/postman/postman_collection_test.json @@ -0,0 +1,81 @@ +{ + "config": { + "name": "postman collection demo" + }, + "teststeps": [ + { + "name": "folder1 - folder2 - Get with params", + "request": { + "method": "GET", + "url": "https://postman-echo.com/get", + "params": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post form-data", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "upload": { + "intro_key": "intro.txt", + "k1": "v1", + "k2": "v2", + "logo_key": "logo.jpeg" + } + } + }, + { + "name": "folder3 - Post x-www-form-urlencoded", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post raw json", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "k1": "v1", + "k2": "v2" + } + } + }, + { + "name": "folder3 - Post raw text", + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": { + "Content-Type": "text/plain" + }, + "body": "have a nice day" + } + }, + { + "name": "Get request headers", + "request": { + "method": "GET", + "url": "https://postman-echo.com/headers", + "headers": { + "Connection": "close", + "User-Agent": "HttpRunner" + } + } + } + ] +} diff --git a/examples/data/postman/postman_collection_test_test.py b/examples/data/postman/postman_collection_test_test.py new file mode 100644 index 00000000..4a813aa8 --- /dev/null +++ b/examples/data/postman/postman_collection_test_test.py @@ -0,0 +1,55 @@ +# NOTE: Generated By HttpRunner v4.1.4 +# FROM: postman/postman_collection_test.json +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCasePostmanCollectionTest(HttpRunner): + + config = Config("postman collection demo") + + teststeps = [ + Step( + RunRequest("folder1 - folder2 - Get with params") + .get("https://postman-echo.com/get") + .with_params(**{"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post form-data") + .post("https://postman-echo.com/post") + .upload( + **{ + "intro_key": "intro.txt", + "k1": "v1", + "k2": "v2", + "logo_key": "logo.jpeg", + } + ) + ), + Step( + RunRequest("folder3 - Post x-www-form-urlencoded") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"}) + .with_data({"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post raw json") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "application/json"}) + .with_json({"k1": "v1", "k2": "v2"}) + ), + Step( + RunRequest("folder3 - Post raw text") + .post("https://postman-echo.com/post") + .with_headers(**{"Content-Type": "text/plain"}) + .with_data("have a nice day") + ), + Step( + RunRequest("Get request headers") + .get("https://postman-echo.com/headers") + .with_headers(**{"Connection": "close", "User-Agent": "HttpRunner"}) + ), + ] + + +if __name__ == "__main__": + TestCasePostmanCollectionTest().test_start() diff --git a/examples/postman_echo/.debugtalk_gen.py b/examples/postman_echo/.debugtalk_gen.py new file mode 100644 index 00000000..f3e58877 --- /dev/null +++ b/examples/postman_echo/.debugtalk_gen.py @@ -0,0 +1,20 @@ +# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_httprunner_version", get_httprunner_version) + funppy.register("sum_two", sum_two) + funppy.register("get_testcase_config_variables", get_testcase_config_variables) + funppy.register("get_testsuite_config_variables", get_testsuite_config_variables) + funppy.register("get_app_version", get_app_version) + funppy.register("calculate_two_nums", calculate_two_nums) + funppy.register("fake_rand_count", fake_rand_count) + funppy.serve() diff --git a/go.mod b/go.mod index 3b02532a..8fa0c5ce 100644 --- a/go.mod +++ b/go.mod @@ -88,3 +88,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 diff --git a/go.sum b/go.sum index 1f3a0ba3..8b05a05a 100644 --- a/go.sum +++ b/go.sum @@ -96,12 +96,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 h1:rxaa937c9aj3Yu4M2UZb5CLAgmPu5XXpXQEfKKSWkEw= +github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= github.com/electricbubble/gadb v0.0.7/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8= -github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= -github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= github.com/electricbubble/opencv-helper v0.0.3/go.mod h1:VHB21p5xsIjXUsUleWSaKGJosRsRAO7cuJoZKf7uCcc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/google_style.py b/google_style.py new file mode 100644 index 00000000..9ba4349f --- /dev/null +++ b/google_style.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +"""Example Google style docstrings. + +This module demonstrates documentation as specified by the `Google Python +Style Guide`_. Docstrings may extend over multiple lines. Sections are created +with a section header and a colon followed by a block of indented text. + +Example: + Examples can be given using either the ``Example`` or ``Examples`` + sections. Sections support any reStructuredText formatting, including + literal blocks:: + + $ python example_google.py + +Section breaks are created by resuming unindented text. Section breaks +are also implicitly created anytime a new section starts. + +Attributes: + module_level_variable1 (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +Todo: + * For module TODOs + * You have to also use ``sphinx.ext.todo`` extension + +.. _Google Python Style Guide: + http://google.github.io/styleguide/pyguide.html + +""" + +module_level_variable1 = 12345 + +module_level_variable2 = 98765 +"""int: Module level variable documented inline. + +The docstring may span multiple lines. The type may optionally be specified +on the first line, separated by a colon. +""" + + +def function_with_types_in_docstring(param1, param2): + """Example function with types documented in the docstring. + + `PEP 484`_ type annotations are supported. If attribute, parameter, and + return types are annotated according to `PEP 484`_, they do not need to be + included in the docstring: + + Args: + param1 (int): The first parameter. + param2 (str): The second parameter. + + Returns: + bool: The return value. True for success, False otherwise. + + .. _PEP 484: + https://www.python.org/dev/peps/pep-0484/ + + """ + + +def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: + """Example function with PEP 484 type annotations. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + The return value. True for success, False otherwise. + + """ + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If \*args or \*\*kwargs are accepted, + they should be listed as ``*args`` and ``**kwargs``. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. The "(type)" is optional. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (:obj:`str`, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError("param1 may not be equal to param2") + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Args: + n (int): The upper limit of the range to generate, from 0 to `n` - 1. + + Yields: + int: The next number in the range of 0 to `n` - 1. + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print([i for i in example_generator(4)]) + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + msg (str): Human readable string describing the exception. + code (:obj:`int`, optional): Error code. + + Attributes: + msg (str): Human readable string describing the exception. + code (int): Exception error code. + + """ + + def __init__(self, msg, code): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they may be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. Alternatively, attributes may be documented + inline with the attribute's declaration (see __init__ method below). + + Properties created with the ``@property`` decorator should be documented + in the property's getter method. + + Attributes: + attr1 (str): Description of `attr1`. + attr2 (:obj:`int`, optional): Description of `attr2`. + + """ + + def __init__(self, param1, param2, param3): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1 (str): Description of `param1`. + param2 (:obj:`int`, optional): Description of `param2`. Multiple + lines are supported. + param3 (:obj:`list` of :obj:`str`): Description of `param3`. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 #: Doc comment *inline* with attribute + + #: list of str: Doc comment *before* attribute, with type specified + self.attr4 = ["attr4"] + + self.attr5 = None + """str: Docstring *after* attribute, with type specified.""" + + @property + def readonly_property(self): + """str: Properties should be documented in their getter method.""" + return "readonly_property" + + @property + def readwrite_property(self): + """:obj:`list` of :obj:`str`: Properties with both a getter and setter + should only be documented in their getter method. + + If the setter method contains notable behavior, it should be + mentioned here. + """ + return ["readwrite_property"] + + @readwrite_property.setter + def readwrite_property(self, value): + value + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are not included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output, if + ``napoleon_include_special_with_doc`` is set to True. + + This behavior can be enabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = True + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 2bdbc37a..3d9f5b5f 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-09302036 \ No newline at end of file +v4.3.0-beta-10081235 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 08477dd7..f3da2174 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-09302036" +__version__ = "v4.3.0-beta-10081235" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/httprunner/step_android.py b/httprunner/step_android.py new file mode 100644 index 00000000..6a485cc8 --- /dev/null +++ b/httprunner/step_android.py @@ -0,0 +1,136 @@ +from typing import Text + +from loguru import logger +import uiautomator2 as u2 + +from httprunner.models import IStep, StepResult, TStep, TStepAndroidUI +from httprunner.runner import HttpRunner + + +def run_android_ui(runner: HttpRunner, step: TStep) -> StepResult: + step_result = StepResult( + name=step.name, + step_type="android_ui", + success=False, + ) + logger.info(f"run android ui action: {step.android.method}, param: {step.android.param}") + + return step_result + + +class StepAndroidControl(IStep): + + def __init__(self, step: TStep): + self.__step = step + + def start_app(self, package_name: Text) -> "StepAndroidControl": + return self + + def stop_app(self, package_name: Text) -> "StepAndroidControl": + return self + + def start_watcher(self) -> "StepAndroidControl": + return self + + def stop_watcher(self) -> "StepAndroidControl": + return self + + def start_camera(self) -> "StepAndroidControl": + return self + + def stop_camera(self) -> "StepAndroidControl": + return self + + def start_record(self) -> "StepAndroidControl": + return self + + def stop_record(self) -> "StepAndroidControl": + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return "android-control" + + def run(self, runner: HttpRunner): + return run_android_ui(runner, self.__step) + + +class StepAndroidUI(IStep): + + def __init__(self, step: TStep): + self.__step = step + + def press_back(self) -> "StepAndroidUI": + self.__step.android.method = "press" + self.__step.android.param = "back" + return self + + def press_home(self) -> "StepAndroidUI": + self.__step.android.method = "press" + self.__step.android.param = "home" + return self + + def sleep(self, time: int) -> "StepAndroidUI": + self.__step.android.method = "sleep" + self.__step.android.param = time + return self + + def swipe_up(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.25, 0.5, 0.75, 0.5] + return self + + def swipe_down(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.75, 0.5, 0.25, 0.5] + return self + + def swipe_left(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.5, 0.75, 0.5, 0.25] + return self + + def swipe_right(self) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [0.5, 0.25, 0.5, 0.75] + return self + + def swipe(self, from_x: float, from_y: float, to_x: float, to_y: float) -> "StepAndroidUI": + self.__step.android.method = "swipe" + self.__step.android.param = [from_x, from_y, to_x, to_y] + return self + + def click(self, text: Text) -> "StepAndroidUI": + self.__step.android.method = "click" + self.__step.android.param = text + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return "android-ui" + + def run(self, runner: HttpRunner): + return run_android_ui(runner, self.__step) + + +class RunAndroidUI(object): + + def __init__(self, name: Text): + self.__step = TStep(name=name) + self.__step.android = TStepAndroidUI() + + def control(self) -> StepAndroidControl: + return StepAndroidControl(self.__step) + + def ui(self) -> StepAndroidUI: + return StepAndroidUI(self.__step) diff --git a/httprunner/step_android_test.py b/httprunner/step_android_test.py new file mode 100644 index 00000000..fe484a89 --- /dev/null +++ b/httprunner/step_android_test.py @@ -0,0 +1,42 @@ +from httprunner import HttpRunner, Config, Step, RunAndroidUI + + +class TestCaseAndroidDemo(HttpRunner): + + config = ( + Config("demo for android UI test") + .variables( + **{ + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2", + } + ) + .android() + .serial("xxx") + .package_name("xxx") + .install_apk("xxx") + ) + + teststeps = [ + # Step( + # RunAndroidUI("start app").control().start_app("com.ss.android.ugc.aweme") + # ), + Step( + RunAndroidUI("back home").ui().press_home() + ), + Step( + RunAndroidUI("back home").control().start_app() + ), + Step( + RunAndroidUI("swipe up").ui().swipe_up() + ), + Step( + RunAndroidUI("swipe up").ui().swipe_up() + ), + ] + + +if __name__ == "__main__": + TestCaseAndroidDemo().test_start() diff --git a/main.go b/main.go new file mode 100644 index 00000000..5d81a823 --- /dev/null +++ b/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "log" + "os" + "strings" + + "github.com/electricbubble/gadb" +) + +func main() { + adbClient, err := gadb.NewClient() + checkErr(err, "fail to connect adb server") + + devices, err := adbClient.DeviceList() + checkErr(err) + + if len(devices) == 0 { + log.Fatalln("list of devices is empty") + } + + dev := devices[0] + + userHomeDir, _ := os.UserHomeDir() + apk, err := os.Open(userHomeDir + "/Desktop/xuexi_android_10002068.apk") + checkErr(err) + + log.Println("starting to push apk") + + remotePath := "/data/local/tmp/xuexi_android_10002068.apk" + err = dev.PushFile(apk, remotePath) + checkErr(err, "adb push") + + log.Println("push completed") + + log.Println("starting to install apk") + + shellOutput, err := dev.RunShellCommand("pm install", remotePath) + checkErr(err, "pm install") + if !strings.Contains(shellOutput, "Success") { + log.Fatalln("fail to install: ", shellOutput) + } + + log.Println("install completed") + +} + +func checkErr(err error, msg ...string) { + if err == nil { + return + } + + var output string + if len(msg) != 0 { + output = msg[0] + " " + } + output += err.Error() + log.Fatalln(output) +} diff --git a/pyproject.toml b/pyproject.toml index db1b5924..6e10cef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-09302036" +version = "v4.3.0-beta-10081235" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 4c2f976f1f769c01aef5d44fb103f21a040dbf9c Mon Sep 17 00:00:00 2001 From: xucong053 Date: Sat, 8 Oct 2022 16:31:10 +0800 Subject: [PATCH 114/169] fix: failed to parse in testcase --- hrp/runner.go | 14 ++++++++++++++ hrp/step_android_ui.go | 9 +++++++++ hrp/step_ios_ui.go | 9 +++++++++ 3 files changed, 32 insertions(+) diff --git a/hrp/runner.go b/hrp/runner.go index 4a672b60..13dced72 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -390,12 +390,26 @@ func (r *testCaseRunner) parseConfig() error { // init iOS/Android clients for _, iosDeviceConfig := range r.parsedConfig.IOS { + if iosDeviceConfig.UDID != "" { + udid, err := r.parser.ParseString(iosDeviceConfig.UDID, parsedVariables) + if err != nil { + return errors.Wrap(err, "failed to parse ios device udid") + } + iosDeviceConfig.UDID = udid.(string) + } _, err := r.hrpRunner.initUIClient(iosDeviceConfig) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") } } for _, androidDeviceConfig := range r.parsedConfig.Android { + if androidDeviceConfig.SerialNumber != "" { + sn, err := r.parser.ParseString(androidDeviceConfig.SerialNumber, parsedVariables) + if err != nil { + return errors.Wrap(err, "failed to parse android device serial") + } + androidDeviceConfig.SerialNumber = sn.(string) + } _, err := r.hrpRunner.initUIClient(androidDeviceConfig) if err != nil { return errors.Wrap(err, "init Android UIAutomator client failed") diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 80f5122a..8fffc533 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -503,6 +503,15 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err } parser := s.GetParser() + // parse device serial + if step.Android.AndroidDevice.SerialNumber != "" { + sn, err := parser.ParseString(step.Android.AndroidDevice.SerialNumber, stepVariables) + if err != nil { + return stepResult, err + } + step.Android.AndroidDevice.SerialNumber = sn.(string) + } + // init uiaClient driver uiaClient, err := s.hrpRunner.initUIClient(&step.Android.AndroidDevice) if err != nil { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index d482c0e1..fe56185a 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -524,6 +524,15 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } parser := s.GetParser() + // parse device udid + if step.IOS.IOSDevice.UDID != "" { + udid, err := parser.ParseString(step.IOS.IOSDevice.UDID, stepVariables) + if err != nil { + return stepResult, err + } + step.IOS.IOSDevice.UDID = udid.(string) + } + // init wdaClient driver wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.IOSDevice) if err != nil { From bfe403043182d57740ab873e98bb34ed33757904 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 9 Oct 2022 17:53:37 +0800 Subject: [PATCH 115/169] change: go ios apps --- hrp/cmd/ios/apps.go | 44 ++++++++++++++++++++++++++++++++------------ hrp/cmd/root.go | 2 +- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go index 5021a422..a77f74c7 100644 --- a/hrp/cmd/ios/apps.go +++ b/hrp/cmd/ios/apps.go @@ -2,12 +2,18 @@ package ios import ( "fmt" - "strings" - "github.com/pkg/errors" + giDevice "github.com/electricbubble/gidevice" + "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" ) +type Application struct { + CFBundleVersion string `json:"version"` + CFBundleDisplayName string `json:"name"` + CFBundleIdentifier string `json:"bundleId"` +} + var listAppsCmd = &cobra.Command{ Use: "apps", Short: "List all iOS installed apps", @@ -17,17 +23,31 @@ var listAppsCmd = &cobra.Command{ return err } - apps, err := device.AppList() - if err != nil { - return errors.Wrap(err, "get ios apps failed") + var applicationType giDevice.ApplicationType + switch appType { + case "user": + applicationType = giDevice.ApplicationTypeUser + case "system": + applicationType = giDevice.ApplicationTypeSystem + case "internal": + applicationType = giDevice.ApplicationTypeInternal + case "all": + applicationType = giDevice.ApplicationTypeAny } - for _, app := range apps { - if appType != "all" && strings.ToLower(app.Type) != appType { - continue - } - fmt.Printf("%-10.10s %-30.30s %-50.50s %-s\n", - app.Type, app.DisplayName, app.CFBundleIdentifier, app.Version) + result, errList := device.InstallationProxyBrowse( + giDevice.WithApplicationType(applicationType), + giDevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier")) + if errList != nil { + return fmt.Errorf("get app list failed") + } + + for _, app := range result { + a := Application{} + mapstructure.Decode(app, &a) + + fmt.Printf("%-30.30s %-50.50s %-s\n", + a.CFBundleDisplayName, a.CFBundleIdentifier, a.CFBundleVersion) } return nil }, @@ -37,6 +57,6 @@ var appType string func init() { listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") - listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|pluginkit|all]") + listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|internal|all]") iosRootCmd.AddCommand(listAppsCmd) } diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index f7572d10..616fd0ab 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -41,7 +41,7 @@ Copyright 2017 debugtalk`, } if !logJSON { log.Logger = zerolog.New(zerolog.ConsoleWriter{NoColor: noColor, Out: os.Stderr}).With().Timestamp().Logger() - log.Info().Msg("Set log to color console other than JSON format.") + log.Info().Msg("Set log to color console") } }, Version: version.VERSION, From 5d271406f9802e827e8f995ed41789c3ecb41cb7 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Sun, 9 Oct 2022 21:28:21 +0800 Subject: [PATCH 116/169] fix: accurate adb log logging with identifier --- hrp/internal/uixt/android_device.go | 53 ++++-------------------- hrp/internal/uixt/android_device_test.go | 4 +- hrp/internal/uixt/android_driver.go | 44 ++++++++++---------- hrp/internal/uixt/android_elment.go | 24 +++++++---- hrp/internal/uixt/interface.go | 8 +++- hrp/internal/uixt/ios_element.go | 17 +++++--- 6 files changed, 67 insertions(+), 83 deletions(-) diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index cfaf09a0..227e0784 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -7,8 +7,6 @@ import ( "net" "os/exec" "reflect" - "regexp" - "strconv" "strings" "syscall" @@ -17,6 +15,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) var ( @@ -28,15 +27,6 @@ var ( const forwardToPrefix = "forward-to-" -const ( - regexFloat = `[0-9\.]*` -) - -var ( - regexCompileSwipe = regexp.MustCompile(fmt.Sprintf(`timesec=(%s)\s*startX=(%s)\s*startY=(%s)\s*endX=(%s)\s*endY=(%s)`, regexFloat, regexFloat, regexFloat, regexFloat, regexFloat)) // parse ${var} or $var - regexCompileTap = regexp.MustCompile(fmt.Sprintf(`timesec=(%s)\s*x=(%s)\s*y=(%s)`, regexFloat, regexFloat, regexFloat)) // parse ${func1($a, $b)} // parse number -) - func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { var deviceOptions []AndroidDeviceOption if device.SerialNumber != "" { @@ -75,6 +65,7 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { } } + driverExt.UUID = androidDevice.UUID() return driverExt, err } @@ -308,43 +299,15 @@ type ExportPoint struct { func ConvertPoints(data string) (eps []ExportPoint) { lines := strings.Split(data, "\n") for _, line := range lines { - if strings.Contains(line, "startX") { - matched := regexCompileSwipe.FindStringSubmatch(line) - if len(matched) != 6 { + if strings.Contains(line, "ext") { + idx := strings.Index(line, "{") + line = line[idx:] + p := ExportPoint{} + err := json.Unmarshal([]byte(line), &p) + if err != nil { log.Error().Msg("failed to parse point data") continue } - start, _ := strconv.Atoi(matched[1]) - fromX, _ := strconv.ParseFloat(matched[2], 64) - fromY, _ := strconv.ParseFloat(matched[3], 64) - toX, _ := strconv.ParseFloat(matched[4], 64) - toY, _ := strconv.ParseFloat(matched[5], 64) - p := ExportPoint{ - Start: start, - End: start, - From: []float64{fromX, fromY}, - To: []float64{toX, toY}, - Operation: "Gtf-Drag", - Ext: "", - } - eps = append(eps, p) - } else if strings.Contains(line, "x=") { - matched := regexCompileTap.FindStringSubmatch(line) - if len(matched) != 4 { - log.Error().Msg("failed to parse point data") - continue - } - start, _ := strconv.Atoi(matched[1]) - x, _ := strconv.ParseFloat(matched[2], 64) - y, _ := strconv.ParseFloat(matched[3], 64) - p := ExportPoint{ - Start: start, - End: start, - From: []float64{x, y}, - To: []float64{x, y}, - Operation: "Gtf-Tap", - Ext: "", - } eps = append(eps, p) } } diff --git a/hrp/internal/uixt/android_device_test.go b/hrp/internal/uixt/android_device_test.go index 4195a5ef..2167d2f5 100644 --- a/hrp/internal/uixt/android_device_test.go +++ b/hrp/internal/uixt/android_device_test.go @@ -8,9 +8,9 @@ import ( ) func TestConvertPoints(t *testing.T) { - data := "09-29 15:02:08.379 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434928378\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:09.433 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434929432\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:10.452 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434930452\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:11.451 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434931450\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:12.491 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434932489\tstartX=720.000000\tstartY=1462.000000\tendX=1296.000000\tendY=1462.000000\n09-29 15:02:16.028 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434936027\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:21.424 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434941423\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:27.923 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434947922\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:33.628 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434953628\tstartX=720.000000\tstartY=1462.000000\tendX=144.000000\tendY=1462.000000\n09-29 15:02:39.347 I/iesqaMonitor( 9938): [tap]\ttimesec=1664434959347\tx=1259.5y=1868.5" + data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}" eps := ConvertPoints(data) - if len(eps) != 10 { + if len(eps) != 3 { t.Fatal() } jsons, _ := json.Marshal(eps) diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index ad3ce2a9..2c80a6a1 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -482,7 +482,7 @@ func (ud *uiaDriver) AppAuthReset(resource ProtectedResource) (err error) { } func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error { - return ud.TapFloat(float64(x), float64(y)) + return ud.TapFloat(float64(x), float64(y), options...) } func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { @@ -491,6 +491,11 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { "x": x, "y": y, } + // append options in post data for extra uiautomator configurations + for _, option := range options { + option(data) + } + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap") return } @@ -546,8 +551,7 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp "endY": toY, } - // append options in post data for extra WDA configurations - // e.g. use WithPressDuration to set pressForDuration + // append options in post data for extra uiautomator configurations for _, option := range options { option(data) } @@ -559,17 +563,23 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp return ud._drag(data) } -func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { +func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...DataOption) (err error) { // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) data := map[string]interface{}{ "startX": startX, "startY": startY, "endX": endX, "endY": endY, - "steps": steps, } - if len(elementID) != 0 { - data["elementId"] = elementID[0] + + // append options in post data for extra uiautomator configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) + } + + if _, ok := data["steps"]; !ok { + data["steps"] = 12 // default steps } _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/perform") return @@ -580,23 +590,12 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, e // per step. So for a 100 steps, the swipe will take about 1/2 second to complete. // `steps` is the number of move steps sent to the system func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { - options = append(options, WithPressDuration(0)) + options = append(options, WithSteps(12)) return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { - data := map[string]interface{}{} - // append options in post data for extra WDA configurations - // e.g. use WithPressDuration to set pressForDuration - for _, option := range options { - option(data) - } - - if _, ok := data["steps"]; !ok { - data["steps"] = 12 // default steps - } - - return ud._swipe(fromX, fromY, toX, toY, data["steps"].(int)) + return ud._swipe(fromX, fromY, toX, toY, options...) } func (ud *uiaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error { @@ -671,8 +670,7 @@ func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { data := map[string]interface{}{ "text": text, } - // append options in post data for extra WDA configurations - // e.g. use WithPressDuration to set pressForDuration + // append options in post data for extra uiautomator configurations for _, option := range options { option(data) } @@ -690,7 +688,7 @@ func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { if err != nil { return err } - return element.SendKeys(text) + return element.SendKeys(text, options...) } func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { diff --git a/hrp/internal/uixt/android_elment.go b/hrp/internal/uixt/android_elment.go index f14fac40..27c503df 100644 --- a/hrp/internal/uixt/android_elment.go +++ b/hrp/internal/uixt/android_elment.go @@ -21,16 +21,22 @@ func (ue uiaElement) Click() (err error) { return } -func (ue uiaElement) SendKeys(text string, isReplace ...int) (err error) { - if len(isReplace) == 0 { - isReplace = []int{1} - } +func (ue uiaElement) SendKeys(text string, options ...DataOption) (err error) { // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) // https://github.com/appium/appium-uiutomator2-server/blob/master/app/src/main/java/io/appium/uiutomator2/handler/SendKeysToElement.java#L76-L85 data := map[string]interface{}{ - "text": text, - "replace": isReplace[0] == 1, + "text": text, } + + // append options in post data for extra uiautomator configurations + for _, option := range options { + option(data) + } + + if _, ok := data["isReplace"]; !ok { + data["isReplace"] = true // default true + } + _, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") return } @@ -105,7 +111,11 @@ func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error { } func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { - return ue.parent._swipe(fromX, fromY, toX, toY, 12, ue.id) + options := []DataOption{ + WithSteps(12), + WithCustomOption("elementId", ue.id), + } + return ue.parent._swipe(fromX, fromY, toX, toY, options...) } func (ue uiaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) { diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index 4922ba37..3ca14086 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -788,6 +788,12 @@ func WithPressDuration(duraion float64) DataOption { } } +func WithSteps(steps int) DataOption { + return func(data map[string]interface{}) { + data["steps"] = steps + } +} + func WithFrequency(frequency int) DataOption { return func(data map[string]interface{}) { data["frequency"] = frequency @@ -1003,7 +1009,7 @@ type WebElement interface { // SendKeys Types a text into element. It will try to activate keyboard on element, // if element has no keyboard focus. // frequency: Frequency of typing (letters per sec). The default value is 60 - SendKeys(text string, frequency ...int) error + SendKeys(text string, options ...DataOption) error // Clear Clears text on element. It will try to activate keyboard on element, // if element has no keyboard focus. Clear() error diff --git a/hrp/internal/uixt/ios_element.go b/hrp/internal/uixt/ios_element.go index 16479efe..74006479 100644 --- a/hrp/internal/uixt/ios_element.go +++ b/hrp/internal/uixt/ios_element.go @@ -25,13 +25,20 @@ func (we wdaElement) Click() (err error) { return } -func (we wdaElement) SendKeys(text string, frequency ...int) (err error) { +func (we wdaElement) SendKeys(text string, options ...DataOption) (err error) { // [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)] - data := map[string]interface{}{"value": strings.Split(text, "")} - if len(frequency) == 0 || frequency[0] <= 0 { - frequency = []int{60} + data := map[string]interface{}{ + "value": strings.Split(text, ""), } - data["frequency"] = frequency[0] + // append options in post data for extra uiautomator configurations + for _, option := range options { + option(data) + } + + if _, ok := data["frequency"]; !ok { + data["frequency"] = 60 + } + _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/value") return } From cd33f01840548240cc4f2da13bfe9ae1a0678dc1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 10 Oct 2022 14:06:31 +0800 Subject: [PATCH 117/169] feat: integrage ios performance monitor --- examples/uitest/demo_douyin_live.json | 4 ++ examples/uitest/demo_douyin_live.yaml | 5 +- examples/uitest/demo_douyin_test.go | 10 ++- go.mod | 2 +- go.sum | 4 +- hrp/internal/uixt/ext.go | 14 +++- hrp/internal/uixt/ios_device.go | 93 +++++++++++++++++++++++++-- hrp/internal/uixt/ocr_on.go | 2 +- hrp/internal/version/VERSION | 2 +- hrp/session.go | 7 +- hrp/step.go | 21 +++++- hrp/step_ios_ui.go | 1 + httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 14 files changed, 150 insertions(+), 19 deletions(-) diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_douyin_live.json index e9dd2f84..7bc7efab 100644 --- a/examples/uitest/demo_douyin_live.json +++ b/examples/uitest/demo_douyin_live.json @@ -6,6 +6,10 @@ }, "ios": [ { + "perf_options": { + "sys_cpu": true, + "sys_mem": true + }, "port": 8700, "mjpeg_port": 8800, "log_on": true diff --git a/examples/uitest/demo_douyin_live.yaml b/examples/uitest/demo_douyin_live.yaml index b1eccd04..e20f426c 100644 --- a/examples/uitest/demo_douyin_live.yaml +++ b/examples/uitest/demo_douyin_live.yaml @@ -3,7 +3,10 @@ config: variables: app_name: 抖音 ios: - - port: 8700 + - perf_options: + sys_cpu: true + sys_mem: true + port: 8700 mjpeg_port: 8800 log_on: true teststeps: diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index ff42eb86..4d3215bb 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -14,7 +14,15 @@ func TestIOSDouyinLive(t *testing.T) { WithVariables(map[string]interface{}{ "app_name": "抖音", }). - SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), + SetIOS( + hrp.WithLogOn(true), + hrp.WithWDAPort(8700), + hrp.WithWDAMjpegPort(8800), + hrp.WithPerfOptions( + hrp.WithPerfSystemCPU(true), + hrp.WithPerfSystemMem(true), + ), + ), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). IOS(). diff --git a/go.mod b/go.mod index 8fa0c5ce..5a7a6797 100644 --- a/go.mod +++ b/go.mod @@ -88,4 +88,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739 diff --git a/go.sum b/go.sum index 8b05a05a..52334bbd 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7 h1:rxaa937c9aj3Yu4M2UZb5CLAgmPu5XXpXQEfKKSWkEw= -github.com/debugtalk/gidevice v0.6.3-0.20221008035433-d79086a752a7/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= +github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739 h1:rMcmtiJJh28z0ILZxdq0C6A2kbobdr4zu9CtxiS6GVY= +github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index bf7b6422..66666754 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -131,8 +131,10 @@ type DriverExt struct { frame *bytes.Buffer doneMjpegStream chan bool scale float64 - StartTime time.Time // used to associate screenshots name - ScreenShots []string // save screenshots path + StartTime time.Time // used to associate screenshots name + ScreenShots []string // save screenshots path + perfStop chan struct{} // stop performance monitor + perfData []string // save perf data CVArgs } @@ -154,6 +156,14 @@ func extend(driver WebDriver) (dExt *DriverExt, err error) { return dExt, nil } +func (dExt *DriverExt) GetPerfData() []string { + if dExt.perfStop == nil { + return nil + } + close(dExt.perfStop) + return dExt.perfData +} + func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { // wait for action done time.Sleep(500 * time.Millisecond) diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index 95228062..bfe4d3ce 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -92,6 +92,28 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } } + if device.PerfOptions != nil { + data, err := iosDevice.d.PerfStart(device.perfOpitons()...) + if err != nil { + return nil, err + } + + driverExt.perfStop = make(chan struct{}) + // start performance monitor + go func() { + for { + select { + case <-driverExt.perfStop: + iosDevice.d.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + driverExt.perfData = append(driverExt.perfData, string(d)) + } + } + }() + } + driverExt.UUID = iosDevice.UUID() return driverExt, nil } @@ -122,6 +144,15 @@ func WithLogOn(logOn bool) IOSDeviceOption { } } +func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption { + return func(device *IOSDevice) { + device.PerfOptions = &giDevice.PerfOptions{} + for _, option := range options { + option(device.PerfOptions) + } + } +} + func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { var usbmux giDevice.Usbmux if usbmux, err = giDevice.NewUsbmux(); err != nil { @@ -171,11 +202,12 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } type IOSDevice struct { - d giDevice.Device - UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` - Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port - MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port - LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + d giDevice.Device + PerfOptions *giDevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"` + UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port + MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port + LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } func (dev *IOSDevice) UUID() string { @@ -240,6 +272,57 @@ func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { return } +func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) { + if dev.PerfOptions == nil { + return + } + + // system + if dev.PerfOptions.SysCPU { + perfOptions = append(perfOptions, giDevice.WithPerfSystemCPU(true)) + } + if dev.PerfOptions.SysMem { + perfOptions = append(perfOptions, giDevice.WithPerfSystemMem(true)) + } + if dev.PerfOptions.SysDisk { + perfOptions = append(perfOptions, giDevice.WithPerfSystemDisk(true)) + } + if dev.PerfOptions.SysNetwork { + perfOptions = append(perfOptions, giDevice.WithPerfSystemNetwork(true)) + } + if dev.PerfOptions.FPS { + perfOptions = append(perfOptions, giDevice.WithPerfFPS(true)) + } + if dev.PerfOptions.Network { + perfOptions = append(perfOptions, giDevice.WithPerfNetwork(true)) + } + + // process + if dev.PerfOptions.BundleID != "" { + perfOptions = append(perfOptions, + giDevice.WithPerfBundleID(dev.PerfOptions.BundleID)) + } + if dev.PerfOptions.Pid != 0 { + perfOptions = append(perfOptions, + giDevice.WithPerfPID(dev.PerfOptions.Pid)) + } + + // config + if dev.PerfOptions.OutputInterval != 0 { + perfOptions = append(perfOptions, + giDevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval)) + } + if dev.PerfOptions.SystemAttributes != nil { + perfOptions = append(perfOptions, + giDevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...)) + } + if dev.PerfOptions.ProcessAttributes != nil { + perfOptions = append(perfOptions, + giDevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...)) + } + return +} + // NewHTTPDriver creates new remote HTTP client, this will also start a new session. func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { localPort, err := getFreePort() diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index fe0620c9..28ff5826 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -55,7 +55,7 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return nil, fmt.Errorf("close body writer error: %v", err) } - url, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9odWJibGUuYnl0ZWRhbmNlLm5ldC92aWRlby9hcGkvdjEvYWxnb3JpdGhtL29jcg==") + url, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9ndGZ0YXNrLmJ5dGVkYW5jZS5jb20vYXBpL3YxL2FsZ29yaXRobS9vY3I=") req, err := http.NewRequest("POST", string(url), bodyBuf) if err != nil { return nil, fmt.Errorf("construct request error: %v", err) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 3d9f5b5f..17e3a2ee 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10081235 \ No newline at end of file +v4.3.0-beta-10101409 \ No newline at end of file diff --git a/hrp/session.go b/hrp/session.go index d04f5a9e..0a3b7fb5 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -5,7 +5,6 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -162,8 +161,8 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { caseSummary.InOut.ExportVars = exportVars caseSummary.InOut.ConfigVars = r.parsedConfig.Variables - // add WDA/UIA logs to summary for uuid, client := range r.hrpRunner.uiClients { + // add WDA/UIA logs to summary log, err := client.Driver.StopCaptureLog() if err != nil { return caseSummary, err @@ -172,6 +171,10 @@ func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { "uuid": uuid, "content": log, } + + // stop performance monitor + logs["performance"] = client.GetPerfData() + caseSummary.Logs = append(caseSummary.Logs, logs) } diff --git a/hrp/step.go b/hrp/step.go index 13b6da61..86f773fd 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -1,6 +1,10 @@ package hrp -import "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +import ( + giDevice "github.com/electricbubble/gidevice" + + "github.com/httprunner/httprunner/v4/hrp/internal/uixt" +) type StepType string @@ -24,6 +28,21 @@ var ( WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError ) +var ( + WithPerfSystemCPU = giDevice.WithPerfSystemCPU + WithPerfSystemMem = giDevice.WithPerfSystemMem + WithPerfSystemDisk = giDevice.WithPerfSystemDisk + WithPerfSystemNetwork = giDevice.WithPerfSystemNetwork + WithPerfGPU = giDevice.WithPerfGPU + WithPerfFPS = giDevice.WithPerfFPS + WithPerfNetwork = giDevice.WithPerfNetwork + WithPerfBundleID = giDevice.WithPerfBundleID + WithPerfPID = giDevice.WithPerfPID + WithPerfOutputInterval = giDevice.WithPerfOutputInterval + WithPerfProcessAttributes = giDevice.WithPerfProcessAttributes + WithPerfSystemAttributes = giDevice.WithPerfSystemAttributes +) + type StepResult struct { Name string `json:"name" yaml:"name"` // step name StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index d482c0e1..8e09c74f 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -15,6 +15,7 @@ var ( WithWDAPort = uixt.WithWDAPort WithWDAMjpegPort = uixt.WithWDAMjpegPort WithLogOn = uixt.WithLogOn + WithPerfOptions = uixt.WithPerfOptions ) type IOSStep struct { diff --git a/httprunner/__init__.py b/httprunner/__init__.py index f3da2174..f5f861d1 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10081235" +__version__ = "v4.3.0-beta-10101409" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 6e10cef3..bb226013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10081235" +version = "v4.3.0-beta-10101409" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 1edef81da89e7b115315a2d8a239ae0238f8b3b5 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 10 Oct 2022 15:43:34 +0800 Subject: [PATCH 118/169] change: bump version --- hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 17e3a2ee..8d5575f6 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10101409 \ No newline at end of file +v4.3.0-beta-10101543 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index f5f861d1..4e71d36a 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10101409" +__version__ = "v4.3.0-beta-10101543" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index bb226013..0fe8271b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10101409" +version = "v4.3.0-beta-10101543" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 619b8a3b686f7ccac1c52d7390936171e98be30a Mon Sep 17 00:00:00 2001 From: lingxiaozu <1430173805@qq.com> Date: Mon, 10 Oct 2022 17:16:56 +0800 Subject: [PATCH 119/169] fix: failed to send keys in android ui automation --- hrp/internal/uixt/android_driver.go | 20 ++++++++++++++- hrp/internal/uixt/ext.go | 40 ++++++++++++++++++++++++++--- hrp/step.go | 3 +++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 2c80a6a1..cb39fd78 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -684,13 +684,31 @@ func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { } func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { - element, err := ud.FindElement(BySelector{ClassName: ElementType{EditText: true}}) + data := map[string]interface{}{ + "view": text, + } + // append options in post data for extra uiautomator configurations + for _, option := range options { + option(data) + } + + var element WebElement + if valuetext, ok := data["text"]; ok { + element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().TextContains(fmt.Sprintf("%v", valuetext)).String()}) + } else if valueid, ok := data["id"]; ok { + element, err = ud.FindElement(BySelector{ResourceIdID: fmt.Sprintf("%v", valueid)}) + } else if valuedesc, ok := data["description"]; ok { + element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().Description(fmt.Sprintf("%v", valuedesc)).String()}) + } else { + element, err = ud.FindElement(BySelector{ClassName: ElementType{EditText: true}}) + } if err != nil { return err } return element.SendKeys(text, options...) } + func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { // TODO return errDriverNotImplemented diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index bf7b6422..43f3328b 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -68,6 +68,9 @@ type MobileAction struct { Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found + Text string `json:"text,omitempty" yaml:"text,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` } type ActionOption func(o *MobileAction) @@ -84,6 +87,26 @@ func WithIndex(index int) ActionOption { } } +func WithText(text string) ActionOption { + return func(o *MobileAction) { + o.Text = text + } +} + +func WithID(id string) ActionOption { + return func(o *MobileAction) { + o.ID = id + } +} + +func WithDescription(description string) ActionOption { + return func(o *MobileAction) { + o.Description = description + } +} + + + func WithMaxRetryTimes(maxRetryTimes int) ActionOption { return func(o *MobileAction) { o.MaxRetryTimes = maxRetryTimes @@ -464,14 +487,23 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { // append \n to send text with enter // send \b\b\b to delete 3 chars param := fmt.Sprintf("%v", action.Params) + options := []DataOption{} + if action.Text != "" { + options = append(options, WithCustomOption("text", action.Text)) + } + if action.ID != "" { + options = append(options, WithCustomOption("id", action.ID)) + } + if action.Description != "" { + options = append(options, WithCustomOption("description", action.Description)) + } if action.Identifier != "" { - option := WithCustomOption("log", map[string]interface{}{ + options = append(options,WithCustomOption("log", map[string]interface{}{ "enable": true, "data": action.Identifier, - }) - return dExt.Driver.Input(param, option) + })) } - return dExt.Driver.Input(param) + return dExt.Driver.Input(param, options...) case CtlSleep: if param, ok := action.Params.(json.Number); ok { seconds, _ := param.Float64() diff --git a/hrp/step.go b/hrp/step.go index 13b6da61..370f7ab4 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -22,6 +22,9 @@ var ( WithIndex = uixt.WithIndex WithTimeout = uixt.WithTimeout WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError + WithText = uixt.WithText + WithID = uixt.WithID + WithDescription = uixt.WithDescription ) type StepResult struct { From f1ada82452eb55e1d0754f433abf4d36fa7ddc9f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 10 Oct 2022 17:33:34 +0800 Subject: [PATCH 120/169] feat: mount ios developer image --- hrp/cmd/ios/apps.go | 7 +-- hrp/cmd/ios/devices.go | 5 +- hrp/cmd/ios/init.go | 5 +- hrp/cmd/ios/mount.go | 90 +++++++++++++++++++++++++++++++ hrp/cmd/ios/ps.go | 7 +-- hrp/cmd/ios/reboot.go | 7 +-- hrp/internal/uixt/android_test.go | 2 +- 7 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 hrp/cmd/ios/mount.go diff --git a/hrp/cmd/ios/apps.go b/hrp/cmd/ios/apps.go index a77f74c7..d8a62f41 100644 --- a/hrp/cmd/ios/apps.go +++ b/hrp/cmd/ios/apps.go @@ -15,8 +15,9 @@ type Application struct { } var listAppsCmd = &cobra.Command{ - Use: "apps", - Short: "List all iOS installed apps", + Use: "apps", + Short: "List all iOS installed apps", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, RunE: func(cmd *cobra.Command, args []string) error { device, err := getDevice(udid) if err != nil { @@ -56,7 +57,7 @@ var listAppsCmd = &cobra.Command{ var appType string func init() { - listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + listAppsCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") listAppsCmd.Flags().StringVarP(&appType, "type", "t", "user", "filter application type [user|system|internal|all]") iosRootCmd.AddCommand(listAppsCmd) } diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index ec884e19..58484e1f 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -66,8 +66,9 @@ func (device *Device) ToFormat() string { } var listDevicesCmd = &cobra.Command{ - Use: "devices", - Short: "List all iOS devices", + Use: "devices", + Short: "List all iOS devices", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, RunE: func(cmd *cobra.Command, args []string) error { devices, err := uixt.IOSDevices(udid) if err != nil { diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index 22642744..8cfea1f3 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -11,9 +11,8 @@ import ( ) var iosRootCmd = &cobra.Command{ - Use: "ios", - Short: "simple utils for ios device management", - PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + Use: "ios", + Short: "simple utils for ios device management", } func getDevice(udid string) (giDevice.Device, error) { diff --git a/hrp/cmd/ios/mount.go b/hrp/cmd/ios/mount.go new file mode 100644 index 00000000..53972220 --- /dev/null +++ b/hrp/cmd/ios/mount.go @@ -0,0 +1,90 @@ +package ios + +import ( + "encoding/base64" + "fmt" + "path/filepath" + "strings" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +// mountCmd represents the mount command +var mountCmd = &cobra.Command{ + Use: "mount", + Short: "A brief description of your command", + RunE: func(cmd *cobra.Command, args []string) error { + device, err := getDevice(udid) + if err != nil { + return err + } + + value, err := device.GetValue("", "ProductVersion") + if err != nil { + return fmt.Errorf("get device ProductVersion failed: %v", err) + } + log.Info().Str("version", value.(string)).Msg("get device version") + + imageSignatures, errImage := device.Images() + + if listDeveloperDiskImage { + for i, imgSign := range imageSignatures { + fmt.Printf("[%d] %s\n", i+1, base64.StdEncoding.EncodeToString(imgSign)) + } + return nil + } + + if errImage == nil && len(imageSignatures) > 0 { + log.Info().Msg("ios developer image is already mounted") + return nil + } + + log.Info().Str("dir", developerDiskImageDir).Msg("start to mount ios developer image") + + if !builtin.IsFolderPathExists(developerDiskImageDir) { + return fmt.Errorf("developer disk image directory not exist: %s", developerDiskImageDir) + } + + ver := strings.Split(value.(string), ".") + if len(ver) < 2 { + return fmt.Errorf("got invalid device ProductVersion: %v", value) + } + version := ver[0] + "." + ver[1] + + var dmgPath, signaturePath string + if builtin.IsFilePathExists(filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg")) { + dmgPath = filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg") + signaturePath = filepath.Join(developerDiskImageDir, "DeveloperDiskImage.dmg.signature") + } else if builtin.IsFilePathExists(filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg.")) { + dmgPath = filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg") + signaturePath = filepath.Join(developerDiskImageDir, version, "DeveloperDiskImage.dmg.signature") + } else { + log.Error().Str("dir", developerDiskImageDir).Msg("developer disk image not found in directory") + return fmt.Errorf("developer disk image not found") + } + + if err = device.MountDeveloperDiskImage(dmgPath, signaturePath); err != nil { + return fmt.Errorf("mount developer disk image failed: %s", err) + } + + log.Info().Msg("mount developer disk image successfully") + return nil + }, +} + +const defaultDeveloperDiskImageDir = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/" + +var ( + developerDiskImageDir string + listDeveloperDiskImage bool +) + +func init() { + mountCmd.Flags().BoolVar(&listDeveloperDiskImage, "list", false, "list developer disk images") + mountCmd.Flags().StringVarP(&developerDiskImageDir, "dir", "d", defaultDeveloperDiskImageDir, "specify DeveloperDiskImage directory") + mountCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + iosRootCmd.AddCommand(mountCmd) +} diff --git a/hrp/cmd/ios/ps.go b/hrp/cmd/ios/ps.go index 22f5a94b..d40dcc4d 100644 --- a/hrp/cmd/ios/ps.go +++ b/hrp/cmd/ios/ps.go @@ -9,8 +9,9 @@ import ( ) var psCmd = &cobra.Command{ - Use: "ps", - Short: "show running processes", + Use: "ps", + Short: "show running processes", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, RunE: func(cmd *cobra.Command, args []string) error { device, err := getDevice(udid) if err != nil { @@ -54,7 +55,7 @@ var psCmd = &cobra.Command{ var isAll bool func init() { - psCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + psCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") psCmd.Flags().BoolVarP(&isAll, "all", "a", false, "print all processes including system processes") iosRootCmd.AddCommand(psCmd) } diff --git a/hrp/cmd/ios/reboot.go b/hrp/cmd/ios/reboot.go index 3cddb6c0..d38d9db8 100644 --- a/hrp/cmd/ios/reboot.go +++ b/hrp/cmd/ios/reboot.go @@ -7,8 +7,9 @@ import ( ) var rebootCmd = &cobra.Command{ - Use: "reboot", - Short: "reboot or shutdown ios device", + Use: "reboot", + Short: "reboot or shutdown ios device", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, RunE: func(cmd *cobra.Command, args []string) error { device, err := getDevice(udid) if err != nil { @@ -31,7 +32,7 @@ var rebootCmd = &cobra.Command{ var isShutdown bool func init() { - rebootCmd.Flags().StringVarP(&udid, "udid", "u", "", "filter by device's udid") + rebootCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") rebootCmd.Flags().BoolVarP(&isShutdown, "shutdown", "s", false, "shutdown ios device") iosRootCmd.AddCommand(rebootCmd) } diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go index 71c3790c..eecf7b91 100644 --- a/hrp/internal/uixt/android_test.go +++ b/hrp/internal/uixt/android_test.go @@ -1134,7 +1134,7 @@ func TestElement_SendKeys(t *testing.T) { // return // err = elem.SendKeys("abc") - err = elem.SendKeys("456", 0) + err = elem.SendKeys("456") if err != nil { t.Fatal(err) } From 59b06fcf65ecdb71e8922821f635256c12025ba3 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 10 Oct 2022 21:50:07 +0800 Subject: [PATCH 121/169] refactor: move uixt from hrp internal to pkg --- examples/uixt/demo_test.go | 34 +++++++++++++++++++ hrp/cmd/adb/devices.go | 2 +- hrp/cmd/ios/devices.go | 2 +- hrp/cmd/ios/init.go | 2 +- hrp/config.go | 2 +- hrp/internal/version/VERSION | 2 +- hrp/{internal => pkg}/uixt/README.md | 0 hrp/{internal => pkg}/uixt/android_action.go | 0 hrp/{internal => pkg}/uixt/android_device.go | 0 .../uixt/android_device_test.go | 0 hrp/{internal => pkg}/uixt/android_driver.go | 1 - hrp/{internal => pkg}/uixt/android_elment.go | 3 +- hrp/{internal => pkg}/uixt/android_key.go | 0 hrp/{internal => pkg}/uixt/android_test.go | 0 hrp/{internal => pkg}/uixt/client.go | 0 hrp/{internal => pkg}/uixt/drag.go | 0 hrp/{internal => pkg}/uixt/drag_test.go | 0 hrp/{internal => pkg}/uixt/ext.go | 4 +-- hrp/{internal => pkg}/uixt/gesture.go | 0 hrp/{internal => pkg}/uixt/gesture_test.go | 0 hrp/{internal => pkg}/uixt/interface.go | 0 hrp/{internal => pkg}/uixt/ios_action.go | 0 hrp/{internal => pkg}/uixt/ios_device.go | 0 hrp/{internal => pkg}/uixt/ios_driver.go | 0 hrp/{internal => pkg}/uixt/ios_element.go | 0 hrp/{internal => pkg}/uixt/ios_test.go | 0 hrp/{internal => pkg}/uixt/ocr_off.go | 0 hrp/{internal => pkg}/uixt/ocr_on.go | 0 hrp/{internal => pkg}/uixt/ocr_test.go | 0 hrp/{internal => pkg}/uixt/opencv_off.go | 0 hrp/{internal => pkg}/uixt/opencv_on.go | 0 hrp/{internal => pkg}/uixt/swipe.go | 0 hrp/{internal => pkg}/uixt/swipe_test.go | 0 hrp/{internal => pkg}/uixt/tap.go | 0 hrp/{internal => pkg}/uixt/tap_test.go | 0 hrp/{internal => pkg}/uixt/touch.go | 0 hrp/{internal => pkg}/uixt/touch_test.go | 0 hrp/response.go | 2 +- hrp/runner.go | 2 +- hrp/step.go | 2 +- hrp/step_android_ui.go | 7 ++-- hrp/step_ios_ui.go | 6 ++-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 44 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 examples/uixt/demo_test.go rename hrp/{internal => pkg}/uixt/README.md (100%) rename hrp/{internal => pkg}/uixt/android_action.go (100%) rename hrp/{internal => pkg}/uixt/android_device.go (100%) rename hrp/{internal => pkg}/uixt/android_device_test.go (100%) rename hrp/{internal => pkg}/uixt/android_driver.go (99%) rename hrp/{internal => pkg}/uixt/android_elment.go (99%) rename hrp/{internal => pkg}/uixt/android_key.go (100%) rename hrp/{internal => pkg}/uixt/android_test.go (100%) rename hrp/{internal => pkg}/uixt/client.go (100%) rename hrp/{internal => pkg}/uixt/drag.go (100%) rename hrp/{internal => pkg}/uixt/drag_test.go (100%) rename hrp/{internal => pkg}/uixt/ext.go (99%) rename hrp/{internal => pkg}/uixt/gesture.go (100%) rename hrp/{internal => pkg}/uixt/gesture_test.go (100%) rename hrp/{internal => pkg}/uixt/interface.go (100%) rename hrp/{internal => pkg}/uixt/ios_action.go (100%) rename hrp/{internal => pkg}/uixt/ios_device.go (100%) rename hrp/{internal => pkg}/uixt/ios_driver.go (100%) rename hrp/{internal => pkg}/uixt/ios_element.go (100%) rename hrp/{internal => pkg}/uixt/ios_test.go (100%) rename hrp/{internal => pkg}/uixt/ocr_off.go (100%) rename hrp/{internal => pkg}/uixt/ocr_on.go (100%) rename hrp/{internal => pkg}/uixt/ocr_test.go (100%) rename hrp/{internal => pkg}/uixt/opencv_off.go (100%) rename hrp/{internal => pkg}/uixt/opencv_on.go (100%) rename hrp/{internal => pkg}/uixt/swipe.go (100%) rename hrp/{internal => pkg}/uixt/swipe_test.go (100%) rename hrp/{internal => pkg}/uixt/tap.go (100%) rename hrp/{internal => pkg}/uixt/tap_test.go (100%) rename hrp/{internal => pkg}/uixt/touch.go (100%) rename hrp/{internal => pkg}/uixt/touch_test.go (100%) diff --git a/examples/uixt/demo_test.go b/examples/uixt/demo_test.go new file mode 100644 index 00000000..44e29248 --- /dev/null +++ b/examples/uixt/demo_test.go @@ -0,0 +1,34 @@ +package uixt + +import ( + "testing" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func TestDemo(t *testing.T) { + device, err := uixt.NewIOSDevice(uixt.WithWDAPort(8700), uixt.WithWDAMjpegPort(8800)) + if err != nil { + t.Fatal(err) + } + driverExt, err := uixt.InitWDAClient(device) + if err != nil { + t.Fatal(err) + } + + // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 + for { + _, err1 := driverExt.GetTextXY("青少年模式") + point, err2 := driverExt.GetTextXY("我知道了") + if err1 != nil || err2 != nil { + time.Sleep(1 * time.Second) + continue + } + + err := driverExt.TapAbsXY(point.X, point.Y, "") + if err != nil { + t.Fatal(err) + } + } +} diff --git a/hrp/cmd/adb/devices.go b/hrp/cmd/adb/devices.go index 789bc1e4..7cd53b69 100644 --- a/hrp/cmd/adb/devices.go +++ b/hrp/cmd/adb/devices.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) func format(data map[string]string) string { diff --git a/hrp/cmd/ios/devices.go b/hrp/cmd/ios/devices.go index 58484e1f..0553d82a 100644 --- a/hrp/cmd/ios/devices.go +++ b/hrp/cmd/ios/devices.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) type Device struct { diff --git a/hrp/cmd/ios/init.go b/hrp/cmd/ios/init.go index 8cfea1f3..db1f63a9 100644 --- a/hrp/cmd/ios/init.go +++ b/hrp/cmd/ios/init.go @@ -7,7 +7,7 @@ import ( giDevice "github.com/electricbubble/gidevice" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) var iosRootCmd = &cobra.Command{ diff --git a/hrp/config.go b/hrp/config.go index 3d1e1aa1..9c03278f 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -5,7 +5,7 @@ import ( "time" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) // NewConfig returns a new constructed testcase config with specified testcase name. diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 8d5575f6..5c7cda41 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10101543 \ No newline at end of file +v4.3.0-beta-10102150 \ No newline at end of file diff --git a/hrp/internal/uixt/README.md b/hrp/pkg/uixt/README.md similarity index 100% rename from hrp/internal/uixt/README.md rename to hrp/pkg/uixt/README.md diff --git a/hrp/internal/uixt/android_action.go b/hrp/pkg/uixt/android_action.go similarity index 100% rename from hrp/internal/uixt/android_action.go rename to hrp/pkg/uixt/android_action.go diff --git a/hrp/internal/uixt/android_device.go b/hrp/pkg/uixt/android_device.go similarity index 100% rename from hrp/internal/uixt/android_device.go rename to hrp/pkg/uixt/android_device.go diff --git a/hrp/internal/uixt/android_device_test.go b/hrp/pkg/uixt/android_device_test.go similarity index 100% rename from hrp/internal/uixt/android_device_test.go rename to hrp/pkg/uixt/android_device_test.go diff --git a/hrp/internal/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go similarity index 99% rename from hrp/internal/uixt/android_driver.go rename to hrp/pkg/uixt/android_driver.go index cb39fd78..58073906 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -708,7 +708,6 @@ func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { return element.SendKeys(text, options...) } - func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) { // TODO return errDriverNotImplemented diff --git a/hrp/internal/uixt/android_elment.go b/hrp/pkg/uixt/android_elment.go similarity index 99% rename from hrp/internal/uixt/android_elment.go rename to hrp/pkg/uixt/android_elment.go index 27c503df..e03c792d 100644 --- a/hrp/internal/uixt/android_elment.go +++ b/hrp/pkg/uixt/android_elment.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/base64" "encoding/json" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -72,7 +73,7 @@ func (ue uiaElement) TwoFingerTap() (err error) { } func (ue uiaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) { - //Todo: implement + // Todo: implement log.Fatal().Msg("not support") return } diff --git a/hrp/internal/uixt/android_key.go b/hrp/pkg/uixt/android_key.go similarity index 100% rename from hrp/internal/uixt/android_key.go rename to hrp/pkg/uixt/android_key.go diff --git a/hrp/internal/uixt/android_test.go b/hrp/pkg/uixt/android_test.go similarity index 100% rename from hrp/internal/uixt/android_test.go rename to hrp/pkg/uixt/android_test.go diff --git a/hrp/internal/uixt/client.go b/hrp/pkg/uixt/client.go similarity index 100% rename from hrp/internal/uixt/client.go rename to hrp/pkg/uixt/client.go diff --git a/hrp/internal/uixt/drag.go b/hrp/pkg/uixt/drag.go similarity index 100% rename from hrp/internal/uixt/drag.go rename to hrp/pkg/uixt/drag.go diff --git a/hrp/internal/uixt/drag_test.go b/hrp/pkg/uixt/drag_test.go similarity index 100% rename from hrp/internal/uixt/drag_test.go rename to hrp/pkg/uixt/drag_test.go diff --git a/hrp/internal/uixt/ext.go b/hrp/pkg/uixt/ext.go similarity index 99% rename from hrp/internal/uixt/ext.go rename to hrp/pkg/uixt/ext.go index 2c24a815..babc74fc 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -105,8 +105,6 @@ func WithDescription(description string) ActionOption { } } - - func WithMaxRetryTimes(maxRetryTimes int) ActionOption { return func(o *MobileAction) { o.MaxRetryTimes = maxRetryTimes @@ -508,7 +506,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { options = append(options, WithCustomOption("description", action.Description)) } if action.Identifier != "" { - options = append(options,WithCustomOption("log", map[string]interface{}{ + options = append(options, WithCustomOption("log", map[string]interface{}{ "enable": true, "data": action.Identifier, })) diff --git a/hrp/internal/uixt/gesture.go b/hrp/pkg/uixt/gesture.go similarity index 100% rename from hrp/internal/uixt/gesture.go rename to hrp/pkg/uixt/gesture.go diff --git a/hrp/internal/uixt/gesture_test.go b/hrp/pkg/uixt/gesture_test.go similarity index 100% rename from hrp/internal/uixt/gesture_test.go rename to hrp/pkg/uixt/gesture_test.go diff --git a/hrp/internal/uixt/interface.go b/hrp/pkg/uixt/interface.go similarity index 100% rename from hrp/internal/uixt/interface.go rename to hrp/pkg/uixt/interface.go diff --git a/hrp/internal/uixt/ios_action.go b/hrp/pkg/uixt/ios_action.go similarity index 100% rename from hrp/internal/uixt/ios_action.go rename to hrp/pkg/uixt/ios_action.go diff --git a/hrp/internal/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go similarity index 100% rename from hrp/internal/uixt/ios_device.go rename to hrp/pkg/uixt/ios_device.go diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go similarity index 100% rename from hrp/internal/uixt/ios_driver.go rename to hrp/pkg/uixt/ios_driver.go diff --git a/hrp/internal/uixt/ios_element.go b/hrp/pkg/uixt/ios_element.go similarity index 100% rename from hrp/internal/uixt/ios_element.go rename to hrp/pkg/uixt/ios_element.go diff --git a/hrp/internal/uixt/ios_test.go b/hrp/pkg/uixt/ios_test.go similarity index 100% rename from hrp/internal/uixt/ios_test.go rename to hrp/pkg/uixt/ios_test.go diff --git a/hrp/internal/uixt/ocr_off.go b/hrp/pkg/uixt/ocr_off.go similarity index 100% rename from hrp/internal/uixt/ocr_off.go rename to hrp/pkg/uixt/ocr_off.go diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/pkg/uixt/ocr_on.go similarity index 100% rename from hrp/internal/uixt/ocr_on.go rename to hrp/pkg/uixt/ocr_on.go diff --git a/hrp/internal/uixt/ocr_test.go b/hrp/pkg/uixt/ocr_test.go similarity index 100% rename from hrp/internal/uixt/ocr_test.go rename to hrp/pkg/uixt/ocr_test.go diff --git a/hrp/internal/uixt/opencv_off.go b/hrp/pkg/uixt/opencv_off.go similarity index 100% rename from hrp/internal/uixt/opencv_off.go rename to hrp/pkg/uixt/opencv_off.go diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/pkg/uixt/opencv_on.go similarity index 100% rename from hrp/internal/uixt/opencv_on.go rename to hrp/pkg/uixt/opencv_on.go diff --git a/hrp/internal/uixt/swipe.go b/hrp/pkg/uixt/swipe.go similarity index 100% rename from hrp/internal/uixt/swipe.go rename to hrp/pkg/uixt/swipe.go diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go similarity index 100% rename from hrp/internal/uixt/swipe_test.go rename to hrp/pkg/uixt/swipe_test.go diff --git a/hrp/internal/uixt/tap.go b/hrp/pkg/uixt/tap.go similarity index 100% rename from hrp/internal/uixt/tap.go rename to hrp/pkg/uixt/tap.go diff --git a/hrp/internal/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go similarity index 100% rename from hrp/internal/uixt/tap_test.go rename to hrp/pkg/uixt/tap_test.go diff --git a/hrp/internal/uixt/touch.go b/hrp/pkg/uixt/touch.go similarity index 100% rename from hrp/internal/uixt/touch.go rename to hrp/pkg/uixt/touch.go diff --git a/hrp/internal/uixt/touch_test.go b/hrp/pkg/uixt/touch_test.go similarity index 100% rename from hrp/internal/uixt/touch_test.go rename to hrp/pkg/uixt/touch_test.go diff --git a/hrp/response.go b/hrp/response.go index 33fe48e7..5f28b269 100644 --- a/hrp/response.go +++ b/hrp/response.go @@ -16,7 +16,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) var fieldTags = []string{"proto", "status_code", "headers", "cookies", "body", textExtractorSubRegexp} diff --git a/hrp/runner.go b/hrp/runner.go index 13dced72..a587adc1 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -20,8 +20,8 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" "github.com/httprunner/httprunner/v4/hrp/internal/version" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) // Run starts to run API test with default configs. diff --git a/hrp/step.go b/hrp/step.go index 3cdeaeb5..9f6989eb 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -3,7 +3,7 @@ package hrp import ( giDevice "github.com/electricbubble/gidevice" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) type StepType string diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 8fffc533..8c175c21 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -4,9 +4,10 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) var ( @@ -189,10 +190,10 @@ func (s *StepAndroid) DoubleTap(params string, options ...uixt.ActionOption) *St return &StepAndroid{step: s.step} } -func (s *StepAndroid) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepAndroid { +func (s *StepAndroid) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepAndroid { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, - Params: []int{sx, sy, ex, ey}, + Params: []float64{sx, sy, ex, ey}, } for _, option := range options { option(&action) diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 828dbbbf..7ba60103 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/uixt" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) var ( @@ -160,10 +160,10 @@ func (s *StepIOS) DoubleTap(params string, options ...uixt.ActionOption) *StepIO return &StepIOS{step: s.step} } -func (s *StepIOS) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepIOS { +func (s *StepIOS) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepIOS { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, - Params: []int{sx, sy, ex, ey}, + Params: []float64{sx, sy, ex, ey}, } for _, option := range options { option(&action) diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 4e71d36a..563312e8 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10101543" +__version__ = "v4.3.0-beta-10102150" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 0fe8271b..8b34acba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10101543" +version = "v4.3.0-beta-10102150" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From a8aab0e9052c7720bf9bac119aa76b8de1f94648 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 10:40:44 +0800 Subject: [PATCH 122/169] refactor: move httpstat from hrp internal to pkg --- hrp/{internal => pkg}/httpstat/main.go | 0 hrp/pkg/uixt/interface.go | 1 - hrp/step_request.go | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) rename hrp/{internal => pkg}/httpstat/main.go (100%) diff --git a/hrp/internal/httpstat/main.go b/hrp/pkg/httpstat/main.go similarity index 100% rename from hrp/internal/httpstat/main.go rename to hrp/pkg/httpstat/main.go diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 3ca14086..7e8f0400 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -468,7 +468,6 @@ type BySelector struct { Name string `json:"name"` Id string `json:"id"` AccessibilityId string `json:"accessibility id"` - // isSearchByIdentifier // partialSearch LinkText ElementAttribute `json:"link text"` diff --git a/hrp/step_request.go b/hrp/step_request.go index 47a9a242..cc656d34 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -21,8 +21,8 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/httpstat" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/httpstat" ) type HTTPMethod string From c81c5841ee62e7dad4f1c3cab277d065d0dc4281 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 10:52:05 +0800 Subject: [PATCH 123/169] change: group environments --- examples/uitest/demo_android_douyin_test.go | 7 ------- hrp/internal/builtin/utils.go | 5 +++-- hrp/internal/builtin/utils_unix.go | 4 +++- hrp/internal/env/env.go | 12 ++++++++++++ hrp/internal/sdk/init.go | 6 +++--- hrp/pkg/uixt/ios_device.go | 3 ++- hrp/pkg/uixt/ocr_on.go | 11 ++++++++--- httprunner/database/engine.py | 4 ++-- 8 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 hrp/internal/env/env.go diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index d07558ce..01fa3f94 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -13,13 +13,6 @@ func TestAndroidDouYinLive(t *testing.T) { Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). SetAndroid(hrp.WithAdbLogOn(true), hrp.WithSerialNumber("2d06bf70")), TestSteps: []hrp.IStep{ - hrp.NewStep("打开网页"). - Android(). - Home(). - AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). - SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("搜索").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). - Validate(). - AssertOCRExists("1664", "网页打开失败"), hrp.NewStep("启动抖音"). Android(). Home(). diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 07157cdb..4d5fefdc 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -20,6 +20,7 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -170,7 +171,7 @@ func InstallPythonPackage(python3 string, pkg string) (err error) { log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package") // install package - pypiIndexURL := os.Getenv("PYPI_INDEX_URL") + pypiIndexURL := env.PYPI_INDEX_URL if pypiIndexURL == "" { pypiIndexURL = "https://pypi.org/simple" // default } @@ -494,7 +495,7 @@ func GetFileNameWithoutExtension(path string) string { } func Bytes2File(data []byte, filename string) error { - file, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0755) + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o755) defer file.Close() if err != nil { log.Error().Err(err).Msg("failed to generate file") diff --git a/hrp/internal/builtin/utils_unix.go b/hrp/internal/builtin/utils_unix.go index 4e5ff1ad..89b6a4f5 100644 --- a/hrp/internal/builtin/utils_unix.go +++ b/hrp/internal/builtin/utils_unix.go @@ -11,6 +11,8 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/env" ) func isPython3(python string) bool { @@ -80,7 +82,7 @@ func ExecCommand(cmdName string, args ...string) error { // add cmd dir path to $PATH if cmdDir := filepath.Dir(cmdName); cmdDir != "" { - PATH := fmt.Sprintf("%s:%s", cmdDir, os.Getenv("PATH")) + PATH := fmt.Sprintf("%s:%s", cmdDir, env.PATH) if err := os.Setenv("PATH", PATH); err != nil { log.Error().Err(err).Msg("set env $PATH failed") return err diff --git a/hrp/internal/env/env.go b/hrp/internal/env/env.go new file mode 100644 index 00000000..c69ca633 --- /dev/null +++ b/hrp/internal/env/env.go @@ -0,0 +1,12 @@ +package env + +import "os" + +var ( + WDA_USB_DRIVER = os.Getenv("WDA_USB_DRIVER") + VEDEM_OCR_URL = os.Getenv("VEDEM_OCR_URL") + DISABLE_GA = os.Getenv("DISABLE_GA") + DISABLE_SENTRY = os.Getenv("DISABLE_SENTRY") + PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL") + PATH = os.Getenv("PATH") +) diff --git a/hrp/internal/sdk/init.go b/hrp/internal/sdk/init.go index 8e69d5e2..ddb12087 100644 --- a/hrp/internal/sdk/init.go +++ b/hrp/internal/sdk/init.go @@ -2,13 +2,13 @@ package sdk import ( "fmt" - "os" "github.com/denisbrodbeck/machineid" "github.com/getsentry/sentry-go" "github.com/google/uuid" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -29,7 +29,7 @@ func init() { gaClient = NewGAClient(trackingID, clientID) // init sentry sdk - if os.Getenv("DISABLE_SENTRY") == "true" { + if env.DISABLE_SENTRY == "true" { return } err = sentry.Init(sentry.ClientOptions{ @@ -50,7 +50,7 @@ func init() { } func SendEvent(e IEvent) error { - if os.Getenv("DISABLE_GA") == "true" { + if env.DISABLE_GA == "true" { // do not send GA events in CI environment return nil } diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index bfe4d3ce..3a2248c2 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -55,7 +56,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { capabilities.WithDefaultAlertAction(AlertActionAccept) var driver WebDriver - if os.Getenv("WDA_USB_DRIVER") == "" { + if env.WDA_USB_DRIVER == "" { // default use http driver driver, err = iosDevice.NewHTTPDriver(capabilities) } else { diff --git a/hrp/pkg/uixt/ocr_on.go b/hrp/pkg/uixt/ocr_on.go index 28ff5826..550da8e7 100644 --- a/hrp/pkg/uixt/ocr_on.go +++ b/hrp/pkg/uixt/ocr_on.go @@ -4,17 +4,18 @@ package uixt import ( "bytes" - "encoding/base64" "fmt" "image" "io/ioutil" "mime/multipart" "net/http" + "os" "strings" "time" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -55,8 +56,12 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return nil, fmt.Errorf("close body writer error: %v", err) } - url, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9ndGZ0YXNrLmJ5dGVkYW5jZS5jb20vYXBpL3YxL2FsZ29yaXRobS9vY3I=") - req, err := http.NewRequest("POST", string(url), bodyBuf) + if env.VEDEM_OCR_URL == "" { + log.Error().Msg("VEDEM_OCR_URL env missed for OCR service") + os.Exit(1) + } + + req, err := http.NewRequest("POST", env.VEDEM_OCR_URL, bodyBuf) if err != nil { return nil, fmt.Errorf("construct request error: %v", err) } diff --git a/httprunner/database/engine.py b/httprunner/database/engine.py index aa0c0cec..8a99deda 100644 --- a/httprunner/database/engine.py +++ b/httprunner/database/engine.py @@ -79,8 +79,8 @@ class DBEngine(object): if __name__ == "__main__": - # db = DBEngine(f"mysql+pymysql://xxxxx:xxxxx@10.0.0.1:3306/dbname?charset=utf8mb4") - db = DBEngine(f"sqlite:////Users/bytedance/HttpRunner/examples/data/sqlite.db") + # db = DBEngine("mysql+pymysql://xxxxx:xxxxx@10.0.0.1:3306/dbname?charset=utf8mb4") + db = DBEngine("sqlite:////Users/xxx/HttpRunner/examples/data/sqlite.db") print(db.fetchmany(""" select* from student""", 5)) print(db.fetchmany("select* from student", 5)) From 0a0700dda8fa5ba1cb5bbf96331ec9df00e16321 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 11:17:58 +0800 Subject: [PATCH 124/169] refactor: move boomer from hrp internal to pkg --- hrp/boomer.go | 2 +- hrp/cmd/boom.go | 2 +- hrp/cmd/curl.go | 2 +- hrp/{internal => pkg}/boomer/README.md | 0 hrp/{internal => pkg}/boomer/boomer.go | 6 ++-- hrp/{internal => pkg}/boomer/boomer_test.go | 0 hrp/{internal => pkg}/boomer/client_grpc.go | 12 ++++---- .../boomer/client_grpc_test.go | 0 hrp/{internal => pkg}/boomer/data/data.go | 1 - .../boomer/data/x509/README.md | 0 .../boomer/data/x509/ca_cert.pem | 0 .../boomer/data/x509/ca_key.pem | 0 .../boomer/data/x509/client_ca_cert.pem | 0 .../boomer/data/x509/client_ca_key.pem | 0 .../boomer/data/x509/client_cert.pem | 0 .../boomer/data/x509/client_key.pem | 0 .../boomer/data/x509/create.sh | 0 .../boomer/data/x509/openssl.cnf | 0 .../boomer/data/x509/server_cert.pem | 0 .../boomer/data/x509/server_key.pem | 0 .../boomer/grpc/messager/messager.pb.go | 30 +++++++++++-------- .../boomer/grpc/messager/messager_grpc.pb.go | 6 ++-- .../boomer/grpc/proto/messager.proto | 0 hrp/{internal => pkg}/boomer/message.go | 0 hrp/{internal => pkg}/boomer/message_test.go | 0 hrp/{internal => pkg}/boomer/output.go | 0 hrp/{internal => pkg}/boomer/output_test.go | 0 hrp/{internal => pkg}/boomer/ratelimiter.go | 0 .../boomer/ratelimiter_test.go | 0 hrp/{internal => pkg}/boomer/runner.go | 2 +- hrp/{internal => pkg}/boomer/runner_test.go | 5 ++-- hrp/{internal => pkg}/boomer/server_grpc.go | 6 ++-- .../boomer/server_grpc_test.go | 0 hrp/{internal => pkg}/boomer/stats.go | 0 hrp/{internal => pkg}/boomer/stats_test.go | 1 - hrp/{internal => pkg}/boomer/task.go | 0 hrp/{internal => pkg}/boomer/ulimit.go | 0 .../boomer/ulimit_windows.go | 0 hrp/{internal => pkg}/boomer/utils.go | 1 - hrp/{internal => pkg}/boomer/utils_test.go | 1 - hrp/server.go | 2 +- 41 files changed, 41 insertions(+), 38 deletions(-) rename hrp/{internal => pkg}/boomer/README.md (100%) rename hrp/{internal => pkg}/boomer/boomer.go (100%) rename hrp/{internal => pkg}/boomer/boomer_test.go (100%) rename hrp/{internal => pkg}/boomer/client_grpc.go (96%) rename hrp/{internal => pkg}/boomer/client_grpc_test.go (100%) rename hrp/{internal => pkg}/boomer/data/data.go (99%) rename hrp/{internal => pkg}/boomer/data/x509/README.md (100%) rename hrp/{internal => pkg}/boomer/data/x509/ca_cert.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/ca_key.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/client_ca_cert.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/client_ca_key.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/client_cert.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/client_key.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/create.sh (100%) rename hrp/{internal => pkg}/boomer/data/x509/openssl.cnf (100%) rename hrp/{internal => pkg}/boomer/data/x509/server_cert.pem (100%) rename hrp/{internal => pkg}/boomer/data/x509/server_key.pem (100%) rename hrp/{internal => pkg}/boomer/grpc/messager/messager.pb.go (96%) rename hrp/{internal => pkg}/boomer/grpc/messager/messager_grpc.pb.go (99%) rename hrp/{internal => pkg}/boomer/grpc/proto/messager.proto (100%) rename hrp/{internal => pkg}/boomer/message.go (100%) rename hrp/{internal => pkg}/boomer/message_test.go (100%) rename hrp/{internal => pkg}/boomer/output.go (100%) rename hrp/{internal => pkg}/boomer/output_test.go (100%) rename hrp/{internal => pkg}/boomer/ratelimiter.go (100%) rename hrp/{internal => pkg}/boomer/ratelimiter_test.go (100%) rename hrp/{internal => pkg}/boomer/runner.go (99%) rename hrp/{internal => pkg}/boomer/runner_test.go (99%) rename hrp/{internal => pkg}/boomer/server_grpc.go (99%) rename hrp/{internal => pkg}/boomer/server_grpc_test.go (100%) rename hrp/{internal => pkg}/boomer/stats.go (100%) rename hrp/{internal => pkg}/boomer/stats_test.go (99%) rename hrp/{internal => pkg}/boomer/task.go (100%) rename hrp/{internal => pkg}/boomer/ulimit.go (100%) rename hrp/{internal => pkg}/boomer/ulimit_windows.go (100%) rename hrp/{internal => pkg}/boomer/utils.go (99%) rename hrp/{internal => pkg}/boomer/utils_test.go (99%) diff --git a/hrp/boomer.go b/hrp/boomer.go index c7d56bb6..a1d36aac 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -13,10 +13,10 @@ import ( "github.com/rs/zerolog/log" "golang.org/x/net/context" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *HRPBoomer { diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index afe10253..d5f853f4 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -10,8 +10,8 @@ import ( "golang.org/x/net/context" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) // boomCmd represents the boom command diff --git a/hrp/cmd/curl.go b/hrp/cmd/curl.go index 214c880d..bd03aeec 100644 --- a/hrp/cmd/curl.go +++ b/hrp/cmd/curl.go @@ -9,8 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/convert" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) var runCurlCmd = &cobra.Command{ diff --git a/hrp/internal/boomer/README.md b/hrp/pkg/boomer/README.md similarity index 100% rename from hrp/internal/boomer/README.md rename to hrp/pkg/boomer/README.md diff --git a/hrp/internal/boomer/boomer.go b/hrp/pkg/boomer/boomer.go similarity index 100% rename from hrp/internal/boomer/boomer.go rename to hrp/pkg/boomer/boomer.go index cdf8e708..baac35e3 100644 --- a/hrp/internal/boomer/boomer.go +++ b/hrp/pkg/boomer/boomer.go @@ -1,8 +1,6 @@ package boomer import ( - "github.com/httprunner/httprunner/v4/hrp/internal/json" - "golang.org/x/net/context" "math" "os" "os/signal" @@ -11,6 +9,9 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "golang.org/x/net/context" + + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) // Mode is the running mode of boomer, both standalone and distributed are supported. @@ -156,7 +157,6 @@ func NewWorkerBoomer(masterHost string, masterPort int) *Boomer { // SetAutoStart auto start to load testing func (b *Boomer) SetAutoStart() { b.masterRunner.autoStart = true - } // RunMaster start to run master runner diff --git a/hrp/internal/boomer/boomer_test.go b/hrp/pkg/boomer/boomer_test.go similarity index 100% rename from hrp/internal/boomer/boomer_test.go rename to hrp/pkg/boomer/boomer_test.go diff --git a/hrp/internal/boomer/client_grpc.go b/hrp/pkg/boomer/client_grpc.go similarity index 96% rename from hrp/internal/boomer/client_grpc.go rename to hrp/pkg/boomer/client_grpc.go index 82d4241b..67484003 100644 --- a/hrp/internal/boomer/client_grpc.go +++ b/hrp/pkg/boomer/client_grpc.go @@ -8,6 +8,8 @@ import ( "sync/atomic" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" "golang.org/x/oauth2" "google.golang.org/grpc" "google.golang.org/grpc/backoff" @@ -15,10 +17,8 @@ import ( "google.golang.org/grpc/credentials/oauth" "google.golang.org/grpc/metadata" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/data" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type grpcClient struct { @@ -247,7 +247,7 @@ func (c *grpcClient) recv() { msg, err := c.config.getBiStreamClient().Recv() if err != nil { time.Sleep(1 * time.Second) - //log.Error().Err(err).Msg("failed to get message") + // log.Error().Err(err).Msg("failed to get message") continue } if msg == nil { @@ -317,7 +317,7 @@ func (c *grpcClient) sendMessage(msg *genericMessage) { atomic.StoreInt32(&c.failCount, 0) return } - //log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message") + // log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message") if msg.Type == "heartbeat" { atomic.AddInt32(&c.failCount, 1) } diff --git a/hrp/internal/boomer/client_grpc_test.go b/hrp/pkg/boomer/client_grpc_test.go similarity index 100% rename from hrp/internal/boomer/client_grpc_test.go rename to hrp/pkg/boomer/client_grpc_test.go diff --git a/hrp/internal/boomer/data/data.go b/hrp/pkg/boomer/data/data.go similarity index 99% rename from hrp/internal/boomer/data/data.go rename to hrp/pkg/boomer/data/data.go index 9e0a21ea..bdb1f48c 100644 --- a/hrp/internal/boomer/data/data.go +++ b/hrp/pkg/boomer/data/data.go @@ -38,7 +38,6 @@ func init() { } hrpPath = filepath.Join(home, ".hrp") _ = builtin.EnsureFolderExists(filepath.Join(hrpPath, "x509")) - } // Path returns the absolute path the given relative file or directory path diff --git a/hrp/internal/boomer/data/x509/README.md b/hrp/pkg/boomer/data/x509/README.md similarity index 100% rename from hrp/internal/boomer/data/x509/README.md rename to hrp/pkg/boomer/data/x509/README.md diff --git a/hrp/internal/boomer/data/x509/ca_cert.pem b/hrp/pkg/boomer/data/x509/ca_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/ca_cert.pem rename to hrp/pkg/boomer/data/x509/ca_cert.pem diff --git a/hrp/internal/boomer/data/x509/ca_key.pem b/hrp/pkg/boomer/data/x509/ca_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/ca_key.pem rename to hrp/pkg/boomer/data/x509/ca_key.pem diff --git a/hrp/internal/boomer/data/x509/client_ca_cert.pem b/hrp/pkg/boomer/data/x509/client_ca_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_ca_cert.pem rename to hrp/pkg/boomer/data/x509/client_ca_cert.pem diff --git a/hrp/internal/boomer/data/x509/client_ca_key.pem b/hrp/pkg/boomer/data/x509/client_ca_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_ca_key.pem rename to hrp/pkg/boomer/data/x509/client_ca_key.pem diff --git a/hrp/internal/boomer/data/x509/client_cert.pem b/hrp/pkg/boomer/data/x509/client_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_cert.pem rename to hrp/pkg/boomer/data/x509/client_cert.pem diff --git a/hrp/internal/boomer/data/x509/client_key.pem b/hrp/pkg/boomer/data/x509/client_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/client_key.pem rename to hrp/pkg/boomer/data/x509/client_key.pem diff --git a/hrp/internal/boomer/data/x509/create.sh b/hrp/pkg/boomer/data/x509/create.sh similarity index 100% rename from hrp/internal/boomer/data/x509/create.sh rename to hrp/pkg/boomer/data/x509/create.sh diff --git a/hrp/internal/boomer/data/x509/openssl.cnf b/hrp/pkg/boomer/data/x509/openssl.cnf similarity index 100% rename from hrp/internal/boomer/data/x509/openssl.cnf rename to hrp/pkg/boomer/data/x509/openssl.cnf diff --git a/hrp/internal/boomer/data/x509/server_cert.pem b/hrp/pkg/boomer/data/x509/server_cert.pem similarity index 100% rename from hrp/internal/boomer/data/x509/server_cert.pem rename to hrp/pkg/boomer/data/x509/server_cert.pem diff --git a/hrp/internal/boomer/data/x509/server_key.pem b/hrp/pkg/boomer/data/x509/server_key.pem similarity index 100% rename from hrp/internal/boomer/data/x509/server_key.pem rename to hrp/pkg/boomer/data/x509/server_key.pem diff --git a/hrp/internal/boomer/grpc/messager/messager.pb.go b/hrp/pkg/boomer/grpc/messager/messager.pb.go similarity index 96% rename from hrp/internal/boomer/grpc/messager/messager.pb.go rename to hrp/pkg/boomer/grpc/messager/messager.pb.go index 66a20108..3ad177c5 100644 --- a/hrp/internal/boomer/grpc/messager/messager.pb.go +++ b/hrp/pkg/boomer/grpc/messager/messager.pb.go @@ -7,10 +7,11 @@ package messager import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -458,17 +459,20 @@ func file_grpc_proto_messager_proto_rawDescGZIP() []byte { return file_grpc_proto_messager_proto_rawDescData } -var file_grpc_proto_messager_proto_msgTypes = make([]protoimpl.MessageInfo, 8) -var file_grpc_proto_messager_proto_goTypes = []interface{}{ - (*StreamRequest)(nil), // 0: message.StreamRequest - (*StreamResponse)(nil), // 1: message.StreamResponse - (*RegisterRequest)(nil), // 2: message.RegisterRequest - (*RegisterResponse)(nil), // 3: message.RegisterResponse - (*SignOutRequest)(nil), // 4: message.SignOutRequest - (*SignOutResponse)(nil), // 5: message.SignOutResponse - nil, // 6: message.StreamRequest.DataEntry - nil, // 7: message.StreamResponse.DataEntry -} +var ( + file_grpc_proto_messager_proto_msgTypes = make([]protoimpl.MessageInfo, 8) + file_grpc_proto_messager_proto_goTypes = []interface{}{ + (*StreamRequest)(nil), // 0: message.StreamRequest + (*StreamResponse)(nil), // 1: message.StreamResponse + (*RegisterRequest)(nil), // 2: message.RegisterRequest + (*RegisterResponse)(nil), // 3: message.RegisterResponse + (*SignOutRequest)(nil), // 4: message.SignOutRequest + (*SignOutResponse)(nil), // 5: message.SignOutResponse + nil, // 6: message.StreamRequest.DataEntry + nil, // 7: message.StreamResponse.DataEntry + } +) + var file_grpc_proto_messager_proto_depIdxs = []int32{ 6, // 0: message.StreamRequest.data:type_name -> message.StreamRequest.DataEntry 7, // 1: message.StreamResponse.data:type_name -> message.StreamResponse.DataEntry diff --git a/hrp/internal/boomer/grpc/messager/messager_grpc.pb.go b/hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go similarity index 99% rename from hrp/internal/boomer/grpc/messager/messager_grpc.pb.go rename to hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go index b4bbad60..80e1a6b5 100644 --- a/hrp/internal/boomer/grpc/messager/messager_grpc.pb.go +++ b/hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go @@ -8,6 +8,7 @@ package messager import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -95,15 +96,16 @@ type MessageServer interface { } // UnimplementedMessageServer must be embedded to have forward compatible implementations. -type UnimplementedMessageServer struct { -} +type UnimplementedMessageServer struct{} func (UnimplementedMessageServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") } + func (UnimplementedMessageServer) SignOut(context.Context, *SignOutRequest) (*SignOutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SignOut not implemented") } + func (UnimplementedMessageServer) BidirectionalStreamingMessage(Message_BidirectionalStreamingMessageServer) error { return status.Errorf(codes.Unimplemented, "method BidirectionalStreamingMessage not implemented") } diff --git a/hrp/internal/boomer/grpc/proto/messager.proto b/hrp/pkg/boomer/grpc/proto/messager.proto similarity index 100% rename from hrp/internal/boomer/grpc/proto/messager.proto rename to hrp/pkg/boomer/grpc/proto/messager.proto diff --git a/hrp/internal/boomer/message.go b/hrp/pkg/boomer/message.go similarity index 100% rename from hrp/internal/boomer/message.go rename to hrp/pkg/boomer/message.go diff --git a/hrp/internal/boomer/message_test.go b/hrp/pkg/boomer/message_test.go similarity index 100% rename from hrp/internal/boomer/message_test.go rename to hrp/pkg/boomer/message_test.go diff --git a/hrp/internal/boomer/output.go b/hrp/pkg/boomer/output.go similarity index 100% rename from hrp/internal/boomer/output.go rename to hrp/pkg/boomer/output.go diff --git a/hrp/internal/boomer/output_test.go b/hrp/pkg/boomer/output_test.go similarity index 100% rename from hrp/internal/boomer/output_test.go rename to hrp/pkg/boomer/output_test.go diff --git a/hrp/internal/boomer/ratelimiter.go b/hrp/pkg/boomer/ratelimiter.go similarity index 100% rename from hrp/internal/boomer/ratelimiter.go rename to hrp/pkg/boomer/ratelimiter.go diff --git a/hrp/internal/boomer/ratelimiter_test.go b/hrp/pkg/boomer/ratelimiter_test.go similarity index 100% rename from hrp/internal/boomer/ratelimiter_test.go rename to hrp/pkg/boomer/ratelimiter_test.go diff --git a/hrp/internal/boomer/runner.go b/hrp/pkg/boomer/runner.go similarity index 99% rename from hrp/internal/boomer/runner.go rename to hrp/pkg/boomer/runner.go index 4bde3a03..d9e5d5f4 100644 --- a/hrp/internal/boomer/runner.go +++ b/hrp/pkg/boomer/runner.go @@ -15,8 +15,8 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) const ( diff --git a/hrp/internal/boomer/runner_test.go b/hrp/pkg/boomer/runner_test.go similarity index 99% rename from hrp/internal/boomer/runner_test.go rename to hrp/pkg/boomer/runner_test.go index 62d772d5..76b44cfa 100644 --- a/hrp/internal/boomer/runner_test.go +++ b/hrp/pkg/boomer/runner_test.go @@ -6,9 +6,10 @@ import ( "testing" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/stretchr/testify/assert" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type HitOutput struct { diff --git a/hrp/internal/boomer/server_grpc.go b/hrp/pkg/boomer/server_grpc.go similarity index 99% rename from hrp/internal/boomer/server_grpc.go rename to hrp/pkg/boomer/server_grpc.go index 2a9c9d00..f795cac5 100644 --- a/hrp/internal/boomer/server_grpc.go +++ b/hrp/pkg/boomer/server_grpc.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -17,9 +18,8 @@ import ( "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/data" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type WorkerNode struct { diff --git a/hrp/internal/boomer/server_grpc_test.go b/hrp/pkg/boomer/server_grpc_test.go similarity index 100% rename from hrp/internal/boomer/server_grpc_test.go rename to hrp/pkg/boomer/server_grpc_test.go diff --git a/hrp/internal/boomer/stats.go b/hrp/pkg/boomer/stats.go similarity index 100% rename from hrp/internal/boomer/stats.go rename to hrp/pkg/boomer/stats.go diff --git a/hrp/internal/boomer/stats_test.go b/hrp/pkg/boomer/stats_test.go similarity index 99% rename from hrp/internal/boomer/stats_test.go rename to hrp/pkg/boomer/stats_test.go index 1d4806a2..4a8491ca 100644 --- a/hrp/internal/boomer/stats_test.go +++ b/hrp/pkg/boomer/stats_test.go @@ -110,7 +110,6 @@ func TestLogError(t *testing.T) { if err400.occurrences != 2 { t.Error("Error occurrences is wrong, expected: 2, got:", err400.occurrences) } - } func BenchmarkLogError(b *testing.B) { diff --git a/hrp/internal/boomer/task.go b/hrp/pkg/boomer/task.go similarity index 100% rename from hrp/internal/boomer/task.go rename to hrp/pkg/boomer/task.go diff --git a/hrp/internal/boomer/ulimit.go b/hrp/pkg/boomer/ulimit.go similarity index 100% rename from hrp/internal/boomer/ulimit.go rename to hrp/pkg/boomer/ulimit.go diff --git a/hrp/internal/boomer/ulimit_windows.go b/hrp/pkg/boomer/ulimit_windows.go similarity index 100% rename from hrp/internal/boomer/ulimit_windows.go rename to hrp/pkg/boomer/ulimit_windows.go diff --git a/hrp/internal/boomer/utils.go b/hrp/pkg/boomer/utils.go similarity index 99% rename from hrp/internal/boomer/utils.go rename to hrp/pkg/boomer/utils.go index 94fad13b..f10a90d0 100644 --- a/hrp/internal/boomer/utils.go +++ b/hrp/pkg/boomer/utils.go @@ -11,7 +11,6 @@ import ( "time" "github.com/google/uuid" - "github.com/rs/zerolog/log" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/mem" diff --git a/hrp/internal/boomer/utils_test.go b/hrp/pkg/boomer/utils_test.go similarity index 99% rename from hrp/internal/boomer/utils_test.go rename to hrp/pkg/boomer/utils_test.go index c56d1457..a8448a97 100644 --- a/hrp/internal/boomer/utils_test.go +++ b/hrp/pkg/boomer/utils_test.go @@ -32,7 +32,6 @@ func TestRound(t *testing.T) { if roundOne != roundTwo { t.Error("round(58360) should be equal to round(58460)") } - } func TestGenMD5(t *testing.T) { diff --git a/hrp/server.go b/hrp/server.go index 50d9d590..ab2417a6 100644 --- a/hrp/server.go +++ b/hrp/server.go @@ -11,8 +11,8 @@ import ( "github.com/mitchellh/mapstructure" - "github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" ) const jsonContentType = "application/json; encoding=utf-8" From 0ff5fb762ffad4282d627e56bb006d9727d3f3dd Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 11:22:02 +0800 Subject: [PATCH 125/169] refactor: move converter from hrp internal to pkg --- hrp/cmd/convert.go | 2 +- hrp/cmd/curl.go | 2 +- hrp/{internal => pkg}/convert/README.md | 0 .../convert/asset/flowgram.png | Bin hrp/{internal => pkg}/convert/converter.go | 0 .../convert/converter_test.go | 0 hrp/{internal => pkg}/convert/from_curl.go | 0 .../convert/from_curl_test.go | 0 hrp/{internal => pkg}/convert/from_gotest.go | 0 hrp/{internal => pkg}/convert/from_har.go | 0 .../convert/from_har_test.go | 0 hrp/{internal => pkg}/convert/from_json.go | 0 hrp/{internal => pkg}/convert/from_postman.go | 0 .../convert/from_postman_test.go | 0 hrp/{internal => pkg}/convert/from_pytest.go | 0 hrp/{internal => pkg}/convert/from_swagger.go | 0 hrp/{internal => pkg}/convert/from_yaml.go | 0 hrp/{internal => pkg}/convert/testcase.tmpl | 0 hrp/pkg/httpstat/demo/main_test.go | 38 ++++++++++++++++++ hrp/pkg/uixt/ios_device.go | 2 +- 20 files changed, 41 insertions(+), 3 deletions(-) rename hrp/{internal => pkg}/convert/README.md (100%) rename hrp/{internal => pkg}/convert/asset/flowgram.png (100%) rename hrp/{internal => pkg}/convert/converter.go (100%) rename hrp/{internal => pkg}/convert/converter_test.go (100%) rename hrp/{internal => pkg}/convert/from_curl.go (100%) rename hrp/{internal => pkg}/convert/from_curl_test.go (100%) rename hrp/{internal => pkg}/convert/from_gotest.go (100%) rename hrp/{internal => pkg}/convert/from_har.go (100%) rename hrp/{internal => pkg}/convert/from_har_test.go (100%) rename hrp/{internal => pkg}/convert/from_json.go (100%) rename hrp/{internal => pkg}/convert/from_postman.go (100%) rename hrp/{internal => pkg}/convert/from_postman_test.go (100%) rename hrp/{internal => pkg}/convert/from_pytest.go (100%) rename hrp/{internal => pkg}/convert/from_swagger.go (100%) rename hrp/{internal => pkg}/convert/from_yaml.go (100%) rename hrp/{internal => pkg}/convert/testcase.tmpl (100%) create mode 100644 hrp/pkg/httpstat/demo/main_test.go diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 27163f60..96d3e985 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" - "github.com/httprunner/httprunner/v4/hrp/internal/convert" "github.com/httprunner/httprunner/v4/hrp/internal/version" + "github.com/httprunner/httprunner/v4/hrp/pkg/convert" ) var convertCmd = &cobra.Command{ diff --git a/hrp/cmd/curl.go b/hrp/cmd/curl.go index bd03aeec..02c18a7f 100644 --- a/hrp/cmd/curl.go +++ b/hrp/cmd/curl.go @@ -9,8 +9,8 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/convert" "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" + "github.com/httprunner/httprunner/v4/hrp/pkg/convert" ) var runCurlCmd = &cobra.Command{ diff --git a/hrp/internal/convert/README.md b/hrp/pkg/convert/README.md similarity index 100% rename from hrp/internal/convert/README.md rename to hrp/pkg/convert/README.md diff --git a/hrp/internal/convert/asset/flowgram.png b/hrp/pkg/convert/asset/flowgram.png similarity index 100% rename from hrp/internal/convert/asset/flowgram.png rename to hrp/pkg/convert/asset/flowgram.png diff --git a/hrp/internal/convert/converter.go b/hrp/pkg/convert/converter.go similarity index 100% rename from hrp/internal/convert/converter.go rename to hrp/pkg/convert/converter.go diff --git a/hrp/internal/convert/converter_test.go b/hrp/pkg/convert/converter_test.go similarity index 100% rename from hrp/internal/convert/converter_test.go rename to hrp/pkg/convert/converter_test.go diff --git a/hrp/internal/convert/from_curl.go b/hrp/pkg/convert/from_curl.go similarity index 100% rename from hrp/internal/convert/from_curl.go rename to hrp/pkg/convert/from_curl.go diff --git a/hrp/internal/convert/from_curl_test.go b/hrp/pkg/convert/from_curl_test.go similarity index 100% rename from hrp/internal/convert/from_curl_test.go rename to hrp/pkg/convert/from_curl_test.go diff --git a/hrp/internal/convert/from_gotest.go b/hrp/pkg/convert/from_gotest.go similarity index 100% rename from hrp/internal/convert/from_gotest.go rename to hrp/pkg/convert/from_gotest.go diff --git a/hrp/internal/convert/from_har.go b/hrp/pkg/convert/from_har.go similarity index 100% rename from hrp/internal/convert/from_har.go rename to hrp/pkg/convert/from_har.go diff --git a/hrp/internal/convert/from_har_test.go b/hrp/pkg/convert/from_har_test.go similarity index 100% rename from hrp/internal/convert/from_har_test.go rename to hrp/pkg/convert/from_har_test.go diff --git a/hrp/internal/convert/from_json.go b/hrp/pkg/convert/from_json.go similarity index 100% rename from hrp/internal/convert/from_json.go rename to hrp/pkg/convert/from_json.go diff --git a/hrp/internal/convert/from_postman.go b/hrp/pkg/convert/from_postman.go similarity index 100% rename from hrp/internal/convert/from_postman.go rename to hrp/pkg/convert/from_postman.go diff --git a/hrp/internal/convert/from_postman_test.go b/hrp/pkg/convert/from_postman_test.go similarity index 100% rename from hrp/internal/convert/from_postman_test.go rename to hrp/pkg/convert/from_postman_test.go diff --git a/hrp/internal/convert/from_pytest.go b/hrp/pkg/convert/from_pytest.go similarity index 100% rename from hrp/internal/convert/from_pytest.go rename to hrp/pkg/convert/from_pytest.go diff --git a/hrp/internal/convert/from_swagger.go b/hrp/pkg/convert/from_swagger.go similarity index 100% rename from hrp/internal/convert/from_swagger.go rename to hrp/pkg/convert/from_swagger.go diff --git a/hrp/internal/convert/from_yaml.go b/hrp/pkg/convert/from_yaml.go similarity index 100% rename from hrp/internal/convert/from_yaml.go rename to hrp/pkg/convert/from_yaml.go diff --git a/hrp/internal/convert/testcase.tmpl b/hrp/pkg/convert/testcase.tmpl similarity index 100% rename from hrp/internal/convert/testcase.tmpl rename to hrp/pkg/convert/testcase.tmpl diff --git a/hrp/pkg/httpstat/demo/main_test.go b/hrp/pkg/httpstat/demo/main_test.go new file mode 100644 index 00000000..e14bfed7 --- /dev/null +++ b/hrp/pkg/httpstat/demo/main_test.go @@ -0,0 +1,38 @@ +package demo + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/httprunner/httprunner/v4/hrp/pkg/httpstat" +) + +func TestMain(t *testing.T) { + var httpStat httpstat.Stat + + req, _ := http.NewRequest("GET", "https://httprunner.com", nil) + ctx := httpstat.WithHTTPStat(req, &httpStat) + + client := &http.Client{ + Timeout: time.Second * 10, + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + if resp != nil { + defer resp.Body.Close() + } + + // get stat + httpStat.Finish() + result := httpStat.Durations() + fmt.Println(result) + + // print stat + httpStat.Print() +} diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 3a2248c2..eacd8891 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -67,7 +67,7 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { } // switch to iOS springboard before init WDA session - // aviod getting stuck when some super app is activate such as douyin or wexin + // avoid getting stuck when some super app is activate such as douyin or wexin log.Info().Msg("go back to home screen") if err = driver.Homescreen(); err != nil { return nil, errors.Wrap(err, "failed to go back to home screen") From cd0ab6dfdbdc05dc8fb36a48babbf71a17fea150 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Tue, 11 Oct 2022 14:53:21 +0800 Subject: [PATCH 126/169] feat: tap the first one matches text from given texts by ocr --- examples/uitest/demo_douyin_follow_live.json | 152 ++++++++++++++++++ examples/uitest/demo_douyin_follow_live.yaml | 83 ++++++++++ .../uitest/demo_douyin_follow_live_test.go | 58 +++++++ hrp/pkg/uixt/android_driver.go | 2 +- hrp/pkg/uixt/ext.go | 81 +++++++--- hrp/pkg/uixt/ocr_off.go | 9 +- hrp/pkg/uixt/ocr_on.go | 71 ++++++++ hrp/pkg/uixt/swipe.go | 12 +- hrp/pkg/uixt/tap.go | 21 +++ hrp/step.go | 1 + hrp/step_android_ui.go | 12 ++ hrp/step_ios_ui.go | 12 ++ 12 files changed, 485 insertions(+), 29 deletions(-) create mode 100644 examples/uitest/demo_douyin_follow_live.json create mode 100644 examples/uitest/demo_douyin_follow_live.yaml create mode 100644 examples/uitest/demo_douyin_follow_live_test.go diff --git a/examples/uitest/demo_douyin_follow_live.json b/examples/uitest/demo_douyin_follow_live.json new file mode 100644 index 00000000..3be24779 --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live.json @@ -0,0 +1,152 @@ +{ + "config": { + "name": "通过 关注天窗 进入指定主播抖音直播间", + "variables": { + "app_name": "抖音" + }, + "ios": [ + { + "port": 8100, + "mjpeg_port": 9100, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "ios": { + "actions": [ + { + "method": "home" + }, + { + "method": "app_terminate", + "params": "com.ss.iphone.ugc.Aweme" + }, + { + "method": "swipe_to_tap_app", + "params": "$app_name", + "identifier": "启动抖音", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 5 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "抖音启动失败,「推荐」不存在" + } + ] + }, + { + "name": "处理青少年弹窗", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "点击首页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "首页", + "index": -1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击关注页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "关注", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "向上滑动 2 次", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_text", + "params": [ + "理肤泉", + "婉宝" + ], + "identifier": "click_live", + "direction": [ + 0.6, + 0.2, + 0.2, + 0.2 + ] + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + } + ] + } + } + ] +} diff --git a/examples/uitest/demo_douyin_follow_live.yaml b/examples/uitest/demo_douyin_follow_live.yaml new file mode 100644 index 00000000..4e1b616c --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live.yaml @@ -0,0 +1,83 @@ +config: + name: 通过 关注天窗 进入指定主播抖音直播间 + variables: + app_name: 抖音 + ios: + - port: 8100 + mjpeg_port: 9100 + log_on: true +teststeps: + - name: 启动抖音 + ios: + actions: + - method: home + - method: app_terminate + params: com.ss.iphone.ugc.Aweme + - method: swipe_to_tap_app + params: $app_name + identifier: 启动抖音 + max_retry_times: 5 + - method: sleep + params: 5 + validate: + - check: ui_ocr + assert: exists + expect: 推荐 + msg: 抖音启动失败,「推荐」不存在 + - name: 处理青少年弹窗 + ios: + actions: + - method: tap_ocr + params: 我知道了 + ignore_NotFoundError: true + - name: 点击首页 + ios: + actions: + - method: tap_ocr + params: 首页 + index: -1 + - method: sleep + params: 10 + - name: 点击关注页 + ios: + actions: + - method: tap_ocr + params: 关注 + index: 1 + - method: sleep + params: 10 + - name: 向上滑动 2 次 + ios: + actions: + - method: swipe_to_tap_text + params: + - 理肤泉 + - 婉宝 + identifier: click_live + direction: + - 0.6 + - 0.2 + - 0.2 + - 0.2 + - method: sleep + params: 10 + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 10 + - method: screenshot + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 10 + - method: screenshot diff --git a/examples/uitest/demo_douyin_follow_live_test.go b/examples/uitest/demo_douyin_follow_live_test.go new file mode 100644 index 00000000..a20dd363 --- /dev/null +++ b/examples/uitest/demo_douyin_follow_live_test.go @@ -0,0 +1,58 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouyinFollowLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 关注天窗 进入指定主播抖音直播间"). + WithVariables(map[string]interface{}{ + "app_name": "抖音", + }). + SetIOS( + hrp.WithLogOn(true), + hrp.WithWDAPort(8100), + hrp.WithWDAMjpegPort(9100), + ), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + IOS(). + Home(). + AppTerminate("com.ss.iphone.ugc.Aweme"). // 关闭已运行的抖音 + SwipeToTapApp("$app_name", hrp.WithMaxRetryTimes(5), hrp.WithIdentifier("启动抖音")).Sleep(5). + Validate(). + AssertOCRExists("推荐", "抖音启动失败,「推荐」不存在"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("点击首页"). + IOS(). + TapByOCR("首页", hrp.WithIndex(-1)).Sleep(10), + hrp.NewStep("点击关注页"). + IOS(). + TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("向上滑动 2 次"). + IOS().SwipeToTapFromTexts([]string{"理肤泉", "婉宝"}, hrp.WithDirection([]float64{0.6, 0.2, 0.2, 0.2}), hrp.WithIdentifier("click_live")).Sleep(10). + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + if err := testCase.Dump2JSON("demo_douyin_follow_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("demo_douyin_follow_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index 58073906..b4a2b713 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -693,7 +693,7 @@ func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { } var element WebElement - if valuetext, ok := data["text"]; ok { + if valuetext, ok := data["textview"]; ok { element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().TextContains(fmt.Sprintf("%v", valuetext)).String()}) } else if valueid, ok := data["id"]; ok { element, err = ud.FindElement(BySelector{ResourceIdID: fmt.Sprintf("%v", valueid)}) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index babc74fc..14cbd0e3 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -63,14 +63,15 @@ type MobileAction struct { Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` - Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log - MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times - Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 - Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action - IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found - Text string `json:"text,omitempty" yaml:"text,omitempty"` - ID string `json:"id,omitempty" yaml:"id,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log + MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app + Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action + IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found + Text string `json:"text,omitempty" yaml:"text,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` } type ActionOption func(o *MobileAction) @@ -87,6 +88,13 @@ func WithIndex(index int) ActionOption { } } +// WithDirection inputs direction (up, down, left, right, []float64{sx, sy, ex, ey}) +func WithDirection(direction interface{}) ActionOption { + return func(o *MobileAction) { + o.Direction = direction + } +} + func WithText(text string) ActionOption { return func(o *MobileAction) { o.Text = text @@ -363,7 +371,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } foundAppAction := func(d *DriverExt) error { // click app to launch - return d.TapAbsXY(point.X, point.Y-20, action.Identifier) + return d.TapAbsXY(point.X, point.Y-25, action.Identifier) } // go to home screen @@ -386,27 +394,52 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return fmt.Errorf("invalid %s params, should be app name(string), got %v", ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: + var point PointF + var findText func(d *DriverExt) error + if text, ok := action.Params.(string); ok { - var point PointF - findText := func(d *DriverExt) error { + findText = func(d *DriverExt) error { var err error point, err = d.GetTextXY(text, action.Index) return err } - foundTextAction := func(d *DriverExt) error { - // tap text - return d.TapAbsXY(point.X, point.Y, action.Identifier) + } else if texts, ok := action.Params.([]interface{}); ok { + findText = func(d *DriverExt) error { + var err error + var ts []string + for _, t := range texts { + ts = append(ts, t.(string)) + } + points, err := d.GetTextXYs(ts) + if err != nil { + return err + } + for _, point = range points { + if point != (PointF{}) { + return nil + } + } + return errors.New("failed to find text position") } - - // default to retry 10 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 10 - } - // swipe until live room found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + } else { + return fmt.Errorf("invalid %s params, should be app text(string or []string), got %v", + ACTION_SwipeToTapText, action.Params) } - return fmt.Errorf("invalid %s params, should be app text(string), got %v", - ACTION_SwipeToTapText, action.Params) + + foundTextAction := func(d *DriverExt) error { + // tap text + return d.TapAbsXY(point.X, point.Y, action.Identifier) + } + + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + if action.Direction != nil { + return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) + } + // swipe until live room found + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) case AppTerminate: if bundleId, ok := action.Params.(string); ok { success, err := dExt.Driver.AppTerminate(bundleId) @@ -497,7 +530,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { param := fmt.Sprintf("%v", action.Params) options := []DataOption{} if action.Text != "" { - options = append(options, WithCustomOption("text", action.Text)) + options = append(options, WithCustomOption("textview", action.Text)) } if action.ID != "" { options = append(options, WithCustomOption("id", action.ID)) diff --git a/hrp/pkg/uixt/ocr_off.go b/hrp/pkg/uixt/ocr_off.go index 669dbe46..03b2e505 100644 --- a/hrp/pkg/uixt/ocr_off.go +++ b/hrp/pkg/uixt/ocr_off.go @@ -2,9 +2,16 @@ package uixt -import "github.com/rs/zerolog/log" +import ( + "github.com/rs/zerolog/log" +) func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) { log.Fatal().Msg("OCR is not supported") return } + +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (ps map[string][]float64, err error) { + log.Fatal().Msg("OCR is not supported") + return +} diff --git a/hrp/pkg/uixt/ocr_on.go b/hrp/pkg/uixt/ocr_on.go index 550da8e7..7a5faf91 100644 --- a/hrp/pkg/uixt/ocr_on.go +++ b/hrp/pkg/uixt/ocr_on.go @@ -159,6 +159,48 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( return rects[idx], nil } +func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects map[string]image.Rectangle, err error) { + ocrResults, err := s.getOCRResult(imageBuf) + if err != nil { + log.Error().Err(err).Msg("getOCRResult failed") + return + } + + var ocrTexts []string + rects = map[string]image.Rectangle{} + + for _, text := range texts { + for _, ocrResult := range ocrResults { + ocrTexts = append(ocrTexts, ocrResult.Text) + + // not contains text + if !strings.Contains(ocrResult.Text, text) { + continue + } + + rect := image.Rectangle{ + // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 + Min: image.Point{ + X: int(ocrResult.Points[0].X), + Y: int(ocrResult.Points[0].Y), + }, + Max: image.Point{ + X: int(ocrResult.Points[2].X), + Y: int(ocrResult.Points[2].Y), + }, + } + rects[text] = rect + break + } + + if _, ok := rects[text]; !ok { + rects[text] = image.Rectangle{} + } + } + + return rects, nil +} + type OCRService interface { FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) } @@ -182,3 +224,32 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, x, y, width, height = dExt.MappingToRectInUIKit(rect) return } + +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (ps map[string][]float64, err error) { + var bufSource *bytes.Buffer + if bufSource, err = dExt.takeScreenShot(); err != nil { + err = fmt.Errorf("takeScreenShot error: %v", err) + return + } + + service := &veDEMOCRService{} + rects, err := service.FindTexts(ocrTexts, bufSource.Bytes()) + if err != nil { + log.Warn().Msgf("FindTexts failed: %s", err.Error()) + err = fmt.Errorf("FindTexts failed: %v", err) + return + } + + ps = map[string][]float64{} + log.Info().Interface("ocrTexts", ocrTexts).Msgf("FindTexts success") + for text, rect := range rects { + if rect == (image.Rectangle{}) { + ps[text] = []float64{} + continue + } + x, y, width, height := dExt.MappingToRectInUIKit(rect) + ps[text] = []float64{x, y, width, height} + } + + return +} diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index 97ac8fc5..5cb8bffa 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -72,14 +72,20 @@ type FindCondition func(driver *DriverExt) error // FoundAction indicates the action to do after a UI element is found type FoundAction func(driver *DriverExt) error -func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, action FoundAction, maxTimes int) error { +func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition, action FoundAction, maxTimes int) error { for i := 0; i < maxTimes; i++ { if err := condition(dExt); err == nil { // do action after found return action(dExt) } - if err := dExt.SwipeTo(direction); err != nil { - log.Error().Err(err).Msgf("swipe %s failed", direction) + if d, ok := direction.(string); ok { + if err := dExt.SwipeTo(d); err != nil { + log.Error().Err(err).Msgf("swipe %s failed", d) + } + } else if d, ok := direction.([]float64); ok { + if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3]); err != nil { + log.Error().Err(err).Msgf("swipe %s failed", d) + } } } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index ada1ae73..10e1f79c 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -41,6 +41,27 @@ func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, er return point, nil } +func (dExt *DriverExt) GetTextXYs(ocrText []string) (points map[string]PointF, err error) { + ps, err := dExt.FindTextsByOCR(ocrText) + if err != nil { + return map[string]PointF{}, err + } + + points = map[string]PointF{} + for text, point := range ps { + if len(point) == 0 { + points[text] = PointF{} + continue + } + points[text] = PointF{ + X: point[0] + point[2]*0.5, + Y: point[1] + point[3]*0.5, + } + } + + return points, nil +} + func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, err error) { x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, index...) if err != nil { diff --git a/hrp/step.go b/hrp/step.go index 9f6989eb..244903bd 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -29,6 +29,7 @@ var ( WithText = uixt.WithText WithID = uixt.WithID WithDescription = uixt.WithDescription + WithDirection = uixt.WithDirection ) var ( diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 8c175c21..6bb8b33d 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -324,6 +324,18 @@ func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) return &StepAndroid{step: s.step} } +func (s *StepAndroid) SwipeToTapFromTexts(texts []string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapText, + Params: texts, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + // Validate switches to step validation. func (s *StepAndroid) Validate() *StepAndroidValidation { return &StepAndroidValidation{ diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7ba60103..5f54c28a 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -244,6 +244,18 @@ func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *Ste return &StepIOS{step: s.step} } +func (s *StepIOS) SwipeToTapFromTexts(texts []string, options ...uixt.ActionOption) *StepIOS { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapText, + Params: texts, + } + for _, option := range options { + option(&action) + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + func (s *StepIOS) Input(text string, options ...uixt.ActionOption) *StepIOS { action := uixt.MobileAction{ Method: uixt.ACTION_Input, From ba00e9590bde62be9d1b910793cbf8c352d987d0 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Tue, 11 Oct 2022 15:55:20 +0800 Subject: [PATCH 127/169] fix: unittext --- examples/uitest/demo_douyin_follow_live_test.go | 2 +- hrp/pkg/uixt/ext.go | 13 ++++++++++--- hrp/step.go | 1 + hrp/step_android_ui.go | 2 +- hrp/step_ios_ui.go | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/uitest/demo_douyin_follow_live_test.go b/examples/uitest/demo_douyin_follow_live_test.go index a20dd363..ad8fc608 100644 --- a/examples/uitest/demo_douyin_follow_live_test.go +++ b/examples/uitest/demo_douyin_follow_live_test.go @@ -37,7 +37,7 @@ func TestIOSDouyinFollowLive(t *testing.T) { IOS(). TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), hrp.NewStep("向上滑动 2 次"). - IOS().SwipeToTapFromTexts([]string{"理肤泉", "婉宝"}, hrp.WithDirection([]float64{0.6, 0.2, 0.2, 0.2}), hrp.WithIdentifier("click_live")).Sleep(10). + IOS().SwipeToTapTexts([]string{"理肤泉", "婉宝"}, hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(10). Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 }, diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 14cbd0e3..5bd6306e 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -88,13 +88,20 @@ func WithIndex(index int) ActionOption { } } -// WithDirection inputs direction (up, down, left, right, []float64{sx, sy, ex, ey}) -func WithDirection(direction interface{}) ActionOption { +// WithDirection inputs direction (up, down, left, right) +func WithDirection(direction string) ActionOption { return func(o *MobileAction) { o.Direction = direction } } +// WithCustomDirection inputs sx, sy, ex, ey +func WithCustomDirection(sx, sy, ex, ey float64) ActionOption { + return func(o *MobileAction) { + o.Direction = []float64{sx, sy, ex, ey} + } +} + func WithText(text string) ActionOption { return func(o *MobileAction) { o.Text = text @@ -438,7 +445,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { if action.Direction != nil { return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) } - // swipe until live room found + // swipe until found return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) case AppTerminate: if bundleId, ok := action.Params.(string); ok { diff --git a/hrp/step.go b/hrp/step.go index 244903bd..98bccab7 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -30,6 +30,7 @@ var ( WithID = uixt.WithID WithDescription = uixt.WithDescription WithDirection = uixt.WithDirection + WithCustomDirection = uixt.WithCustomDirection ) var ( diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 6bb8b33d..e16840e1 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -324,7 +324,7 @@ func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeToTapFromTexts(texts []string, options ...uixt.ActionOption) *StepAndroid { +func (s *StepAndroid) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepAndroid { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapText, Params: texts, diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5f54c28a..7d6d8df9 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -244,7 +244,7 @@ func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *Ste return &StepIOS{step: s.step} } -func (s *StepIOS) SwipeToTapFromTexts(texts []string, options ...uixt.ActionOption) *StepIOS { +func (s *StepIOS) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepIOS { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapText, Params: texts, From 2d529071abadbbdafafcb92b34383f57a6016ec9 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Tue, 11 Oct 2022 16:33:10 +0800 Subject: [PATCH 128/169] fix: add swipe_to_tap_texts action --- examples/uitest/demo_douyin_follow_live.json | 2 +- examples/uitest/demo_douyin_follow_live.yaml | 2 +- hrp/pkg/uixt/ext.go | 66 +++++++++++++------- hrp/step_android_ui.go | 2 +- hrp/step_ios_ui.go | 2 +- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/examples/uitest/demo_douyin_follow_live.json b/examples/uitest/demo_douyin_follow_live.json index 3be24779..4f9b411b 100644 --- a/examples/uitest/demo_douyin_follow_live.json +++ b/examples/uitest/demo_douyin_follow_live.json @@ -94,7 +94,7 @@ "ios": { "actions": [ { - "method": "swipe_to_tap_text", + "method": "swipe_to_tap_texts", "params": [ "理肤泉", "婉宝" diff --git a/examples/uitest/demo_douyin_follow_live.yaml b/examples/uitest/demo_douyin_follow_live.yaml index 4e1b616c..454df7c8 100644 --- a/examples/uitest/demo_douyin_follow_live.yaml +++ b/examples/uitest/demo_douyin_follow_live.yaml @@ -49,7 +49,7 @@ teststeps: - name: 向上滑动 2 次 ios: actions: - - method: swipe_to_tap_text + - method: swipe_to_tap_texts params: - 理肤泉 - 婉宝 diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 5bd6306e..cfd5d356 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -55,8 +55,9 @@ const ( ACTION_Input MobileMethod = "input" // custom actions - ACTION_SwipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap - ACTION_SwipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap + ACTION_SwipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap + ACTION_SwipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap + ACTION_SwipeToTapTexts MobileMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap ) type MobileAction struct { @@ -401,17 +402,35 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return fmt.Errorf("invalid %s params, should be app name(string), got %v", ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: - var point PointF - var findText func(d *DriverExt) error - if text, ok := action.Params.(string); ok { - findText = func(d *DriverExt) error { + var point PointF + findText := func(d *DriverExt) error { var err error point, err = d.GetTextXY(text, action.Index) return err } - } else if texts, ok := action.Params.([]interface{}); ok { - findText = func(d *DriverExt) error { + foundTextAction := func(d *DriverExt) error { + // tap text + return d.TapAbsXY(point.X, point.Y, action.Identifier) + } + + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + + if action.Direction != nil { + return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) + } + // swipe until found + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + } + return fmt.Errorf("invalid %s params, should be app text(string), got %v", + ACTION_SwipeToTapText, action.Params) + case ACTION_SwipeToTapTexts: + if texts, ok := action.Params.([]interface{}); ok { + var point PointF + findText := func(d *DriverExt) error { var err error var ts []string for _, t := range texts { @@ -428,25 +447,24 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } return errors.New("failed to find text position") } - } else { - return fmt.Errorf("invalid %s params, should be app text(string or []string), got %v", - ACTION_SwipeToTapText, action.Params) - } + foundTextAction := func(d *DriverExt) error { + // tap text + return d.TapAbsXY(point.X, point.Y, action.Identifier) + } - foundTextAction := func(d *DriverExt) error { - // tap text - return d.TapAbsXY(point.X, point.Y, action.Identifier) - } + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } - // default to retry 10 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 10 + if action.Direction != nil { + return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) + } + // swipe until found + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) } - if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) - } - // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + return fmt.Errorf("invalid %s params, should be app text([]string), got %v", + ACTION_SwipeToTapText, action.Params) case AppTerminate: if bundleId, ok := action.Params.(string); ok { success, err := dExt.Driver.AppTerminate(bundleId) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index e16840e1..a479b5c6 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -326,7 +326,7 @@ func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) func (s *StepAndroid) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepAndroid { action := uixt.MobileAction{ - Method: uixt.ACTION_SwipeToTapText, + Method: uixt.ACTION_SwipeToTapTexts, Params: texts, } for _, option := range options { diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7d6d8df9..5a2fb01f 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -246,7 +246,7 @@ func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *Ste func (s *StepIOS) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepIOS { action := uixt.MobileAction{ - Method: uixt.ACTION_SwipeToTapText, + Method: uixt.ACTION_SwipeToTapTexts, Params: texts, } for _, option := range options { From 3683589b37a6c4fa946958d5a52e4b4a8b32ef60 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 18:01:49 +0800 Subject: [PATCH 129/169] refactor: init driver for device --- hrp/pkg/uixt/android_device.go | 101 +++++----- .../pkg/uixt/demo/main_test.go | 6 +- hrp/pkg/uixt/drag_test.go | 4 +- hrp/pkg/uixt/ext.go | 4 +- hrp/pkg/uixt/gesture_test.go | 4 +- hrp/pkg/uixt/interface.go | 1 + hrp/pkg/uixt/ios_device.go | 178 +++++++++--------- hrp/pkg/uixt/ocr_test.go | 4 +- hrp/pkg/uixt/swipe_test.go | 4 +- hrp/pkg/uixt/tap_test.go | 16 +- hrp/pkg/uixt/touch_test.go | 8 +- hrp/step_ios_ui.go | 6 +- 12 files changed, 178 insertions(+), 158 deletions(-) rename examples/uixt/demo_test.go => hrp/pkg/uixt/demo/main_test.go (87%) diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index 227e0784..c43eaa4f 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -27,48 +27,6 @@ var ( const forwardToPrefix = "forward-to-" -func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { - var deviceOptions []AndroidDeviceOption - if device.SerialNumber != "" { - deviceOptions = append(deviceOptions, WithSerialNumber(device.SerialNumber)) - } - if device.IP != "" { - deviceOptions = append(deviceOptions, WithAdbIP(device.IP)) - } - if device.Port != 0 { - deviceOptions = append(deviceOptions, WithAdbPort(device.Port)) - } - - // init uia device - androidDevice, err := NewAndroidDevice(deviceOptions...) - if err != nil { - return nil, err - } - - driver, err := androidDevice.NewUSBDriver(nil) - if err != nil { - return nil, errors.Wrap(err, "failed to init UIA driver") - } - fmt.Println(driver) - - var driverExt *DriverExt - - driverExt, err = Extend(driver) - if err != nil { - return nil, errors.Wrap(err, "failed to extend UIA Driver") - } - - if device.LogOn { - err = driverExt.Driver.StartCaptureLog("hrp_adb_log") - if err != nil { - return nil, err - } - } - - driverExt.UUID = androidDevice.UUID() - return driverExt, err -} - type AndroidDeviceOption func(*AndroidDevice) func WithSerialNumber(serial string) AndroidDeviceOption { @@ -125,6 +83,15 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er return nil, fmt.Errorf("device %s not found", device.SerialNumber) } +func DeviceList() (devices []gadb.Device, err error) { + var adbClient gadb.Client + if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { + return nil, err + } + + return adbClient.DeviceList() +} + type AndroidDevice struct { d gadb.Device logcat *DeviceLogcat @@ -135,17 +102,52 @@ type AndroidDevice struct { LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } -func (o AndroidDevice) UUID() string { - return o.SerialNumber +func (dev *AndroidDevice) UUID() string { + return dev.SerialNumber } -func DeviceList() (devices []gadb.Device, err error) { - var adbClient gadb.Client - if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { - return nil, err +func (dev *AndroidDevice) NewDriver() (driverExt *DriverExt, err error) { + var deviceOptions []AndroidDeviceOption + if dev.SerialNumber != "" { + deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) + } + if dev.IP != "" { + deviceOptions = append(deviceOptions, WithAdbIP(dev.IP)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithAdbPort(dev.Port)) } - return adbClient.DeviceList() + androidDevice, err := NewAndroidDevice(deviceOptions...) + if err != nil { + return nil, err + } + return androidDevice.InitUIAClient() +} + +func (dev *AndroidDevice) InitUIAClient() (*DriverExt, error) { + driver, err := dev.NewUSBDriver(nil) + if err != nil { + return nil, errors.Wrap(err, "failed to init UIA driver") + } + fmt.Println(driver) + + var driverExt *DriverExt + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend UIA Driver") + } + + if dev.LogOn { + err = driverExt.Driver.StartCaptureLog("hrp_adb_log") + if err != nil { + return nil, err + } + } + + driverExt.UUID = dev.UUID() + return driverExt, err } // NewUSBDriver creates new client via USB connected device, this will also start a new session. @@ -255,6 +257,7 @@ func (l *DeviceLogcat) Errors() (err error) { func (l *DeviceLogcat) CatchLogcat() (err error) { if l.cmd != nil { err = fmt.Errorf("logcat already start") + return } cmdLine := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial) l.cmd = builtin.Command(cmdLine) diff --git a/examples/uixt/demo_test.go b/hrp/pkg/uixt/demo/main_test.go similarity index 87% rename from examples/uixt/demo_test.go rename to hrp/pkg/uixt/demo/main_test.go index 44e29248..e6747e46 100644 --- a/examples/uixt/demo_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -1,4 +1,4 @@ -package uixt +package demo import ( "testing" @@ -7,12 +7,12 @@ import ( "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) -func TestDemo(t *testing.T) { +func TestIOSDemo(t *testing.T) { device, err := uixt.NewIOSDevice(uixt.WithWDAPort(8700), uixt.WithWDAMjpegPort(8800)) if err != nil { t.Fatal(err) } - driverExt, err := uixt.InitWDAClient(device) + driverExt, err := device.InitWDAClient() if err != nil { t.Fatal(err) } diff --git a/hrp/pkg/uixt/drag_test.go b/hrp/pkg/uixt/drag_test.go index f2a628c5..653c716e 100644 --- a/hrp/pkg/uixt/drag_test.go +++ b/hrp/pkg/uixt/drag_test.go @@ -7,7 +7,9 @@ import ( ) func TestDriverExt_Drag(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index cfd5d356..25a3bb44 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -205,7 +205,7 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { // wait for action done time.Sleep(500 * time.Millisecond) - // 优先使用 MJPEG 流进行截图,性能最优 + // iOS 优先使用 MJPEG 流进行截图,性能最优 // 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口 if dExt.frame != nil { return dExt.frame, nil @@ -259,7 +259,7 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin func (dExt *DriverExt) ScreenShot(fileName string) (string, error) { raw, err := dExt.takeScreenShot() if err != nil { - return "", errors.Wrap(err, "screenshot by WDA failed") + return "", errors.Wrap(err, "screenshot failed") } path, err := dExt.saveScreenShot(raw, fileName) diff --git a/hrp/pkg/uixt/gesture_test.go b/hrp/pkg/uixt/gesture_test.go index 31664954..0e880ecc 100644 --- a/hrp/pkg/uixt/gesture_test.go +++ b/hrp/pkg/uixt/gesture_test.go @@ -15,7 +15,9 @@ func TestDriverExt_GesturePassword(t *testing.T) { password[i], _ = strconv.Atoi(split[i]) } - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 7e8f0400..8c6e3d99 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -802,6 +802,7 @@ func WithFrequency(frequency int) DataOption { // current implemeted device: IOSDevice, AndroidDevice type Device interface { UUID() string + NewDriver() (driverExt *DriverExt, err error) } // WebDriver defines methods supported by WebDriver drivers. diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index eacd8891..cd358bae 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -29,7 +29,7 @@ const ( // It may help to prevent out of memory or timeout errors while getting the elements source tree, // but it might restrict the depth of source tree. // A part of elements source tree might be lost if the value was too small. Defaults to 50 - snapshotMaxDepth = 10 + snapshotMaxDepth = 16 // Allows to customize accept/dismiss alert button selector. // It helps you to handle an arbitrary element as accept button in accept alert command. // The selector should be a valid class chain expression, where the search root is the alert element itself. @@ -44,81 +44,6 @@ const ( defaultMjpegPort = 9100 ) -func InitWDAClient(device *IOSDevice) (*DriverExt, error) { - // init wda device - iosDevice, err := NewIOSDevice(device.opitons()...) - if err != nil { - return nil, err - } - - // init WDA driver - capabilities := NewCapabilities() - capabilities.WithDefaultAlertAction(AlertActionAccept) - var driver WebDriver - - if env.WDA_USB_DRIVER == "" { - // default use http driver - driver, err = iosDevice.NewHTTPDriver(capabilities) - } else { - driver, err = iosDevice.NewUSBDriver(capabilities) - } - if err != nil { - return nil, errors.Wrap(err, "failed to init WDA driver") - } - - // switch to iOS springboard before init WDA session - // avoid getting stuck when some super app is activate such as douyin or wexin - log.Info().Msg("go back to home screen") - if err = driver.Homescreen(); err != nil { - return nil, errors.Wrap(err, "failed to go back to home screen") - } - - driverExt, err := Extend(driver) - if err != nil { - return nil, errors.Wrap(err, "failed to extend WebDriver") - } - settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ - "snapshotMaxDepth": snapshotMaxDepth, - "acceptAlertButtonSelector": acceptAlertButtonSelector, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to set appium WDA settings") - } - log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") - - if device.LogOn { - err = driverExt.Driver.StartCaptureLog("hrp_wda_log") - if err != nil { - return nil, err - } - } - - if device.PerfOptions != nil { - data, err := iosDevice.d.PerfStart(device.perfOpitons()...) - if err != nil { - return nil, err - } - - driverExt.perfStop = make(chan struct{}) - // start performance monitor - go func() { - for { - select { - case <-driverExt.perfStop: - iosDevice.d.PerfStop() - return - case d := <-data: - fmt.Println(string(d)) - driverExt.perfData = append(driverExt.perfData, string(d)) - } - } - }() - } - - driverExt.UUID = iosDevice.UUID() - return driverExt, nil -} - type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -215,6 +140,94 @@ func (dev *IOSDevice) UUID() string { return dev.UDID } +func (dev *IOSDevice) NewDriver() (driverExt *DriverExt, err error) { + var deviceOptions []IOSDeviceOption + if dev.UDID != "" { + deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) + } + if dev.MjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) + } + + iosDevice, err := NewIOSDevice(deviceOptions...) + if err != nil { + return nil, err + } + return iosDevice.InitWDAClient() +} + +func (dev *IOSDevice) InitWDAClient() (driverExt *DriverExt, err error) { + // init WDA driver + capabilities := NewCapabilities() + capabilities.WithDefaultAlertAction(AlertActionAccept) + var driver WebDriver + + if env.WDA_USB_DRIVER == "" { + // default use http driver + driver, err = dev.NewHTTPDriver(capabilities) + } else { + driver, err = dev.NewUSBDriver(capabilities) + } + if err != nil { + return nil, errors.Wrap(err, "failed to init WDA driver") + } + + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is activate such as douyin or wexin + log.Info().Msg("go back to home screen") + if err = driver.Homescreen(); err != nil { + return nil, errors.Wrap(err, "failed to go back to home screen") + } + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend WebDriver") + } + settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ + "snapshotMaxDepth": snapshotMaxDepth, + "acceptAlertButtonSelector": acceptAlertButtonSelector, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set appium WDA settings") + } + log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings") + + if dev.LogOn { + err = driverExt.Driver.StartCaptureLog("hrp_wda_log") + if err != nil { + return nil, err + } + } + + if dev.PerfOptions != nil { + data, err := dev.d.PerfStart(dev.perfOpitons()...) + if err != nil { + return nil, err + } + + driverExt.perfStop = make(chan struct{}) + // start performance monitor + go func() { + for { + select { + case <-driverExt.perfStop: + dev.d.PerfStop() + return + case d := <-data: + fmt.Println(string(d)) + driverExt.perfData = append(driverExt.perfData, string(d)) + } + } + }() + } + + driverExt.UUID = dev.UUID() + return driverExt, nil +} + func (dev *IOSDevice) forward(localPort, remotePort int) error { log.Info().Int("localPort", localPort).Int("remotePort", remotePort). Str("udid", dev.UDID).Msg("forward tcp port") @@ -260,19 +273,6 @@ func (dev *IOSDevice) forward(localPort, remotePort int) error { return nil } -func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) { - if dev.UDID != "" { - deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) - } - if dev.Port != 0 { - deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) - } - if dev.MjpegPort != 0 { - deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) - } - return -} - func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) { if dev.PerfOptions == nil { return diff --git a/hrp/pkg/uixt/ocr_test.go b/hrp/pkg/uixt/ocr_test.go index 59b19160..ce58950e 100644 --- a/hrp/pkg/uixt/ocr_test.go +++ b/hrp/pkg/uixt/ocr_test.go @@ -7,7 +7,9 @@ import ( ) func TestDriverExtOCR(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) x, y, width, height, err := driverExt.FindTextByOCR("抖音") diff --git a/hrp/pkg/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go index 61ded0af..d9e31947 100644 --- a/hrp/pkg/uixt/swipe_test.go +++ b/hrp/pkg/uixt/swipe_test.go @@ -7,7 +7,9 @@ import ( ) func TestSwipeUntil(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) var point PointF diff --git a/hrp/pkg/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go index c5dcafa7..d824785b 100644 --- a/hrp/pkg/uixt/tap_test.go +++ b/hrp/pkg/uixt/tap_test.go @@ -7,7 +7,9 @@ import ( ) func TestDriverExt_TapWithNumber(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -20,7 +22,9 @@ func TestDriverExt_TapWithNumber(t *testing.T) { } func TestDriverExt_TapXY(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) err = driverExt.TapXY(0.4, 0.5, "") @@ -28,7 +32,9 @@ func TestDriverExt_TapXY(t *testing.T) { } func TestDriverExt_TapAbsXY(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) err = driverExt.TapAbsXY(100, 300, "") @@ -36,7 +42,9 @@ func TestDriverExt_TapAbsXY(t *testing.T) { } func TestDriverExt_TapWithOCR(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) // 需要点击文字上方的图标 diff --git a/hrp/pkg/uixt/touch_test.go b/hrp/pkg/uixt/touch_test.go index c89d9879..92d73d18 100644 --- a/hrp/pkg/uixt/touch_test.go +++ b/hrp/pkg/uixt/touch_test.go @@ -7,7 +7,9 @@ import ( ) func TestDriverExt_ForceTouch(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" @@ -23,7 +25,9 @@ func TestDriverExt_ForceTouch(t *testing.T) { } func TestDriverExt_TouchAndHold(t *testing.T) { - driverExt, err := InitWDAClient(nil) + device, err := NewIOSDevice() + checkErr(t, err) + driverExt, err := device.InitWDAClient() checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5a2fb01f..94107d49 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -503,11 +503,7 @@ func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, er } } - if iosDevice, ok := device.(*uixt.IOSDevice); ok { - client, err = uixt.InitWDAClient(iosDevice) - } else if androidDevice, ok := device.(*uixt.AndroidDevice); ok { - client, err = uixt.InitUIAClient(androidDevice) - } + client, err = device.NewDriver() if err != nil { return nil, err } From acb9c80a938ed993b190257d76453809e93979e8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 18:16:48 +0800 Subject: [PATCH 130/169] change: add capabilities for driver --- hrp/pkg/uixt/android_device.go | 8 ++++---- hrp/pkg/uixt/demo/main_test.go | 5 ++++- hrp/pkg/uixt/drag_test.go | 4 +--- hrp/pkg/uixt/gesture_test.go | 4 +--- hrp/pkg/uixt/interface.go | 2 +- hrp/pkg/uixt/ios_device.go | 14 ++++++++------ hrp/pkg/uixt/ocr_test.go | 4 +--- hrp/pkg/uixt/swipe_test.go | 4 +--- hrp/pkg/uixt/tap_test.go | 22 ++++++++++------------ hrp/pkg/uixt/touch_test.go | 8 ++------ hrp/step_ios_ui.go | 2 +- 11 files changed, 34 insertions(+), 43 deletions(-) diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index c43eaa4f..9e0b4c1b 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -106,7 +106,7 @@ func (dev *AndroidDevice) UUID() string { return dev.SerialNumber } -func (dev *AndroidDevice) NewDriver() (driverExt *DriverExt, err error) { +func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { var deviceOptions []AndroidDeviceOption if dev.SerialNumber != "" { deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) @@ -122,11 +122,11 @@ func (dev *AndroidDevice) NewDriver() (driverExt *DriverExt, err error) { if err != nil { return nil, err } - return androidDevice.InitUIAClient() + return androidDevice.initUIAClient(capabilities) } -func (dev *AndroidDevice) InitUIAClient() (*DriverExt, error) { - driver, err := dev.NewUSBDriver(nil) +func (dev *AndroidDevice) initUIAClient(capabilities Capabilities) (*DriverExt, error) { + driver, err := dev.NewUSBDriver(capabilities) if err != nil { return nil, errors.Wrap(err, "failed to init UIA driver") } diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index e6747e46..26bfb46b 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -12,7 +12,10 @@ func TestIOSDemo(t *testing.T) { if err != nil { t.Fatal(err) } - driverExt, err := device.InitWDAClient() + + capabilities := uixt.NewCapabilities() + capabilities.WithDefaultAlertAction(uixt.AlertActionAccept) // or uixt.AlertActionDismiss + driverExt, err := device.NewDriver(capabilities) if err != nil { t.Fatal(err) } diff --git a/hrp/pkg/uixt/drag_test.go b/hrp/pkg/uixt/drag_test.go index 653c716e..59a8fbfb 100644 --- a/hrp/pkg/uixt/drag_test.go +++ b/hrp/pkg/uixt/drag_test.go @@ -7,9 +7,7 @@ import ( ) func TestDriverExt_Drag(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png" diff --git a/hrp/pkg/uixt/gesture_test.go b/hrp/pkg/uixt/gesture_test.go index 0e880ecc..c3b8dcad 100644 --- a/hrp/pkg/uixt/gesture_test.go +++ b/hrp/pkg/uixt/gesture_test.go @@ -15,9 +15,7 @@ func TestDriverExt_GesturePassword(t *testing.T) { password[i], _ = strconv.Atoi(split[i]) } - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png" diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 8c6e3d99..93b07179 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -802,7 +802,7 @@ func WithFrequency(frequency int) DataOption { // current implemeted device: IOSDevice, AndroidDevice type Device interface { UUID() string - NewDriver() (driverExt *DriverExt, err error) + NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) } // WebDriver defines methods supported by WebDriver drivers. diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index cd358bae..d8004e0a 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -140,7 +140,7 @@ func (dev *IOSDevice) UUID() string { return dev.UDID } -func (dev *IOSDevice) NewDriver() (driverExt *DriverExt, err error) { +func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { var deviceOptions []IOSDeviceOption if dev.UDID != "" { deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) @@ -156,15 +156,17 @@ func (dev *IOSDevice) NewDriver() (driverExt *DriverExt, err error) { if err != nil { return nil, err } - return iosDevice.InitWDAClient() + return iosDevice.initWDAClient(capabilities) } -func (dev *IOSDevice) InitWDAClient() (driverExt *DriverExt, err error) { +func (dev *IOSDevice) initWDAClient(capabilities Capabilities) (driverExt *DriverExt, err error) { // init WDA driver - capabilities := NewCapabilities() - capabilities.WithDefaultAlertAction(AlertActionAccept) - var driver WebDriver + if capabilities == nil { + capabilities = NewCapabilities() + capabilities.WithDefaultAlertAction(AlertActionAccept) + } + var driver WebDriver if env.WDA_USB_DRIVER == "" { // default use http driver driver, err = dev.NewHTTPDriver(capabilities) diff --git a/hrp/pkg/uixt/ocr_test.go b/hrp/pkg/uixt/ocr_test.go index ce58950e..da868c27 100644 --- a/hrp/pkg/uixt/ocr_test.go +++ b/hrp/pkg/uixt/ocr_test.go @@ -7,9 +7,7 @@ import ( ) func TestDriverExtOCR(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) x, y, width, height, err := driverExt.FindTextByOCR("抖音") diff --git a/hrp/pkg/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go index d9e31947..83502a8f 100644 --- a/hrp/pkg/uixt/swipe_test.go +++ b/hrp/pkg/uixt/swipe_test.go @@ -7,9 +7,7 @@ import ( ) func TestSwipeUntil(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) var point PointF diff --git a/hrp/pkg/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go index d824785b..b1bf9ee9 100644 --- a/hrp/pkg/uixt/tap_test.go +++ b/hrp/pkg/uixt/tap_test.go @@ -6,10 +6,14 @@ import ( "testing" ) +var iosDevice *IOSDevice + +func init() { + iosDevice, _ = NewIOSDevice() +} + func TestDriverExt_TapWithNumber(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" @@ -22,9 +26,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) { } func TestDriverExt_TapXY(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) err = driverExt.TapXY(0.4, 0.5, "") @@ -32,9 +34,7 @@ func TestDriverExt_TapXY(t *testing.T) { } func TestDriverExt_TapAbsXY(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) err = driverExt.TapAbsXY(100, 300, "") @@ -42,9 +42,7 @@ func TestDriverExt_TapAbsXY(t *testing.T) { } func TestDriverExt_TapWithOCR(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) // 需要点击文字上方的图标 diff --git a/hrp/pkg/uixt/touch_test.go b/hrp/pkg/uixt/touch_test.go index 92d73d18..aa5515a8 100644 --- a/hrp/pkg/uixt/touch_test.go +++ b/hrp/pkg/uixt/touch_test.go @@ -7,9 +7,7 @@ import ( ) func TestDriverExt_ForceTouch(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" @@ -25,9 +23,7 @@ func TestDriverExt_ForceTouch(t *testing.T) { } func TestDriverExt_TouchAndHold(t *testing.T) { - device, err := NewIOSDevice() - checkErr(t, err) - driverExt, err := device.InitWDAClient() + driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png" diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 94107d49..ddf5eb93 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -503,7 +503,7 @@ func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, er } } - client, err = device.NewDriver() + client, err = device.NewDriver(nil) if err != nil { return nil, err } From de7b2f566b6b7df8985239b3340a0b11acf02020 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 20:44:24 +0800 Subject: [PATCH 131/169] feat: support config ios ResetHomeOnStartup, SnapshotMaxDepth, AcceptAlertButtonSelector, DismissAlertButtonSelector --- hrp/pkg/uixt/demo/main_test.go | 5 ++- hrp/pkg/uixt/ios_device.go | 78 ++++++++++++++++++++++++++++------ hrp/step_ios_ui.go | 14 +++--- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 26bfb46b..32dd2d9d 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -8,7 +8,10 @@ import ( ) func TestIOSDemo(t *testing.T) { - device, err := uixt.NewIOSDevice(uixt.WithWDAPort(8700), uixt.WithWDAMjpegPort(8800)) + device, err := uixt.NewIOSDevice( + uixt.WithWDAPort(8700), uixt.WithWDAMjpegPort(8800), + uixt.WithResetHomeOnStartup(false), // not reset home on startup + ) if err != nil { t.Fatal(err) } diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index d8004e0a..0e95742f 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -24,6 +24,12 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/json" ) +const ( + defaultWDAPort = 8100 + defaultMjpegPort = 9100 + defaultResetHomeOnStartup = true +) + const ( // Changes the value of maximum depth for traversing elements source tree. // It may help to prevent out of memory or timeout errors while getting the elements source tree, @@ -39,11 +45,6 @@ const ( dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" ) -const ( - defaultWDAPort = 8100 - defaultMjpegPort = 9100 -) - type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -70,6 +71,30 @@ func WithLogOn(logOn bool) IOSDeviceOption { } } +func WithResetHomeOnStartup(reset bool) IOSDeviceOption { + return func(device *IOSDevice) { + device.ResetHomeOnStartup = reset + } +} + +func WithSnapshotMaxDepth(depth int) IOSDeviceOption { + return func(device *IOSDevice) { + device.SnapshotMaxDepth = depth + } +} + +func WithAcceptAlertButtonSelector(selector string) IOSDeviceOption { + return func(device *IOSDevice) { + device.AcceptAlertButtonSelector = selector + } +} + +func WithDismissAlertButtonSelector(selector string) IOSDeviceOption { + return func(device *IOSDevice) { + device.DismissAlertButtonSelector = selector + } +} + func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption { return func(device *IOSDevice) { device.PerfOptions = &giDevice.PerfOptions{} @@ -105,8 +130,12 @@ func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { device = &IOSDevice{ - Port: defaultWDAPort, - MjpegPort: defaultMjpegPort, + Port: defaultWDAPort, + MjpegPort: defaultMjpegPort, + ResetHomeOnStartup: defaultResetHomeOnStartup, + SnapshotMaxDepth: snapshotMaxDepth, + AcceptAlertButtonSelector: acceptAlertButtonSelector, + DismissAlertButtonSelector: dismissAlertButtonSelector, } for _, option := range options { option(device) @@ -134,6 +163,12 @@ type IOSDevice struct { Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is activate such as douyin or wexin + ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` + SnapshotMaxDepth int `json:"snapshot_max_depth,omitempty" yaml:"snapshot_max_depth,omitempty"` + AcceptAlertButtonSelector string `json:"accept_alert_button_selector,omitempty" yaml:"accept_alert_button_selector,omitempty"` + DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"` } func (dev *IOSDevice) UUID() string { @@ -151,6 +186,21 @@ func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt if dev.MjpegPort != 0 { deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) } + if dev.LogOn { + deviceOptions = append(deviceOptions, WithLogOn(true)) + } + if dev.ResetHomeOnStartup { + deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true)) + } + if dev.SnapshotMaxDepth != 0 { + deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth)) + } + if dev.AcceptAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector)) + } + if dev.DismissAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.DismissAlertButtonSelector)) + } iosDevice, err := NewIOSDevice(deviceOptions...) if err != nil { @@ -177,11 +227,11 @@ func (dev *IOSDevice) initWDAClient(capabilities Capabilities) (driverExt *Drive return nil, errors.Wrap(err, "failed to init WDA driver") } - // switch to iOS springboard before init WDA session - // avoid getting stuck when some super app is activate such as douyin or wexin - log.Info().Msg("go back to home screen") - if err = driver.Homescreen(); err != nil { - return nil, errors.Wrap(err, "failed to go back to home screen") + if dev.ResetHomeOnStartup { + log.Info().Msg("go back to home screen") + if err = driver.Homescreen(); err != nil { + return nil, errors.Wrap(err, "failed to go back to home screen") + } } driverExt, err = Extend(driver) @@ -189,8 +239,8 @@ func (dev *IOSDevice) initWDAClient(capabilities Capabilities) (driverExt *Drive return nil, errors.Wrap(err, "failed to extend WebDriver") } settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ - "snapshotMaxDepth": snapshotMaxDepth, - "acceptAlertButtonSelector": acceptAlertButtonSelector, + "snapshotMaxDepth": dev.SnapshotMaxDepth, + "acceptAlertButtonSelector": dev.AcceptAlertButtonSelector, }) if err != nil { return nil, errors.Wrap(err, "failed to set appium WDA settings") diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index ddf5eb93..5613dfe4 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -11,11 +11,15 @@ import ( ) var ( - WithUDID = uixt.WithUDID - WithWDAPort = uixt.WithWDAPort - WithWDAMjpegPort = uixt.WithWDAMjpegPort - WithLogOn = uixt.WithLogOn - WithPerfOptions = uixt.WithPerfOptions + WithUDID = uixt.WithUDID + WithWDAPort = uixt.WithWDAPort + WithWDAMjpegPort = uixt.WithWDAMjpegPort + WithLogOn = uixt.WithLogOn + WithResetHomeOnStartup = uixt.WithResetHomeOnStartup + WithSnapshotMaxDepth = uixt.WithSnapshotMaxDepth + WithAcceptAlertButtonSelector = uixt.WithAcceptAlertButtonSelector + WithDismissAlertButtonSelector = uixt.WithDismissAlertButtonSelector + WithPerfOptions = uixt.WithPerfOptions ) type IOSStep struct { From 5bfd3fd9864963cc91b2efc5e4005444f1262e80 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 21:28:08 +0800 Subject: [PATCH 132/169] fix: ResetHomeOnStartup --- hrp/pkg/uixt/ios_device.go | 12 ++++++------ hrp/runner.go | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 0e95742f..2b148159 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -25,9 +25,8 @@ import ( ) const ( - defaultWDAPort = 8100 - defaultMjpegPort = 9100 - defaultResetHomeOnStartup = true + defaultWDAPort = 8100 + defaultMjpegPort = 9100 ) const ( @@ -132,7 +131,6 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { device = &IOSDevice{ Port: defaultWDAPort, MjpegPort: defaultMjpegPort, - ResetHomeOnStartup: defaultResetHomeOnStartup, SnapshotMaxDepth: snapshotMaxDepth, AcceptAlertButtonSelector: acceptAlertButtonSelector, DismissAlertButtonSelector: dismissAlertButtonSelector, @@ -163,9 +161,11 @@ type IOSDevice struct { Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` + // switch to iOS springboard before init WDA session - // avoid getting stuck when some super app is activate such as douyin or wexin - ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` + ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"` + + // config appium settings SnapshotMaxDepth int `json:"snapshot_max_depth,omitempty" yaml:"snapshot_max_depth,omitempty"` AcceptAlertButtonSelector string `json:"accept_alert_button_selector,omitempty" yaml:"accept_alert_button_selector,omitempty"` DismissAlertButtonSelector string `json:"dismiss_alert_button_selector,omitempty" yaml:"dismiss_alert_button_selector,omitempty"` diff --git a/hrp/runner.go b/hrp/runner.go index a587adc1..9b2380cf 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -397,6 +397,9 @@ func (r *testCaseRunner) parseConfig() error { } iosDeviceConfig.UDID = udid.(string) } + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is activate such as douyin or wexin + iosDeviceConfig.ResetHomeOnStartup = true _, err := r.hrpRunner.initUIClient(iosDeviceConfig) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") From 8183b5b344226f99267dbdac711e1669c77b75a3 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 21:38:05 +0800 Subject: [PATCH 133/169] fix: release UI driver session --- hrp/pkg/uixt/demo/main_test.go | 5 +++++ hrp/runner.go | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 32dd2d9d..e544eee6 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -23,6 +23,11 @@ func TestIOSDemo(t *testing.T) { t.Fatal(err) } + // release session + defer func() { + driverExt.Driver.DeleteSession() + }() + // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 for { _, err1 := driverExt.GetTextXY("青少年模式") diff --git a/hrp/runner.go b/hrp/runner.go index 9b2380cf..72a20ffe 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -211,6 +211,12 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { log.Error().Err(err).Msg("[Run] init session runner failed") return err } + // release UI driver session + defer func() { + for _, client := range sessionRunner.hrpRunner.uiClients { + client.Driver.DeleteSession() + } + }() for it := sessionRunner.parametersIterator; it.HasNext(); { err1 := sessionRunner.Start(it.Next()) From 71f49303be5562937d6efebd2f315c87d680b22e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 11 Oct 2022 22:24:49 +0800 Subject: [PATCH 134/169] fix: get pid by bundle id in perf --- go.mod | 2 +- go.sum | 4 ++-- hrp/internal/version/VERSION | 2 +- hrp/runner.go | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 5a7a6797..386f1875 100644 --- a/go.mod +++ b/go.mod @@ -88,4 +88,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739 +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10 diff --git a/go.sum b/go.sum index 52334bbd..3e5893b6 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739 h1:rMcmtiJJh28z0ILZxdq0C6A2kbobdr4zu9CtxiS6GVY= -github.com/debugtalk/gidevice v0.6.3-0.20221009112329-b36169c16739/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= +github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10 h1:LfX+4+EUZbLNKAN1KfFMxlIxZNW9cgFuo3XGgvjulzg= +github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 5c7cda41..f1f06ca1 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10102150 \ No newline at end of file +v4.3.0-beta-10112220 \ No newline at end of file diff --git a/hrp/runner.go b/hrp/runner.go index 72a20ffe..6a302da2 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -404,7 +404,7 @@ func (r *testCaseRunner) parseConfig() error { iosDeviceConfig.UDID = udid.(string) } // switch to iOS springboard before init WDA session - // avoid getting stuck when some super app is activate such as douyin or wexin + // avoid getting stuck when some super app is active such as douyin or wexin iosDeviceConfig.ResetHomeOnStartup = true _, err := r.hrpRunner.initUIClient(iosDeviceConfig) if err != nil { diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 563312e8..715e1099 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10102150" +__version__ = "v4.3.0-beta-10112220" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 8b34acba..13bdcbc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10102150" +version = "v4.3.0-beta-10112220" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 43b1c181e5c64e457e81e359fc1e8b82dbb2be55 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 12 Oct 2022 19:38:41 +0800 Subject: [PATCH 135/169] refactor: new ios/android device --- go.mod | 2 +- go.sum | 4 +- hrp/pkg/uixt/android_device.go | 35 +++++++---------- hrp/pkg/uixt/ios_device.go | 68 +++++++++++++++++----------------- hrp/runner.go | 21 ++++++++--- hrp/step_android_ui.go | 14 ++++--- hrp/step_ios_ui.go | 34 +++++------------ 7 files changed, 84 insertions(+), 94 deletions(-) diff --git a/go.mod b/go.mod index 386f1875..29a113b8 100644 --- a/go.mod +++ b/go.mod @@ -88,4 +88,4 @@ require ( ) // replace github.com/httprunner/funplugin => ../funplugin -replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10 +replace github.com/electricbubble/gidevice => github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 diff --git a/go.sum b/go.sum index 3e5893b6..f9df4ccf 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10 h1:LfX+4+EUZbLNKAN1KfFMxlIxZNW9cgFuo3XGgvjulzg= -github.com/debugtalk/gidevice v0.6.3-0.20221011141335-8a73d55ade10/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= +github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77 h1:wP/2aKW6YV0ityxp0Ecv8JDwA/cy6gayVhA/t+roO+w= +github.com/debugtalk/gidevice v0.6.3-0.20221012071407-9b59e12ecc77/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index 9e0b4c1b..80bfb5c4 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -53,6 +53,19 @@ func WithAdbLogOn(logOn bool) AndroidDeviceOption { } } +func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceOption) { + if dev.SerialNumber != "" { + deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) + } + if dev.IP != "" { + deviceOptions = append(deviceOptions, WithAdbIP(dev.IP)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithAdbPort(dev.Port)) + } + return +} + func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) { deviceList, err := DeviceList() if err != nil { @@ -107,32 +120,10 @@ func (dev *AndroidDevice) UUID() string { } func (dev *AndroidDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { - var deviceOptions []AndroidDeviceOption - if dev.SerialNumber != "" { - deviceOptions = append(deviceOptions, WithSerialNumber(dev.SerialNumber)) - } - if dev.IP != "" { - deviceOptions = append(deviceOptions, WithAdbIP(dev.IP)) - } - if dev.Port != 0 { - deviceOptions = append(deviceOptions, WithAdbPort(dev.Port)) - } - - androidDevice, err := NewAndroidDevice(deviceOptions...) - if err != nil { - return nil, err - } - return androidDevice.initUIAClient(capabilities) -} - -func (dev *AndroidDevice) initUIAClient(capabilities Capabilities) (*DriverExt, error) { driver, err := dev.NewUSBDriver(capabilities) if err != nil { return nil, errors.Wrap(err, "failed to init UIA driver") } - fmt.Println(driver) - - var driverExt *DriverExt driverExt, err = Extend(driver) if err != nil { diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 2b148159..6ed13d3f 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -127,6 +127,37 @@ func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { return deviceList, nil } +func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) { + if dev.UDID != "" { + deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) + } + if dev.Port != 0 { + deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) + } + if dev.MjpegPort != 0 { + deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) + } + if dev.LogOn { + deviceOptions = append(deviceOptions, WithLogOn(true)) + } + if dev.PerfOptions != nil { + deviceOptions = append(deviceOptions, WithPerfOptions(dev.perfOpitons()...)) + } + if dev.ResetHomeOnStartup { + deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true)) + } + if dev.SnapshotMaxDepth != 0 { + deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth)) + } + if dev.AcceptAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector)) + } + if dev.DismissAlertButtonSelector != "" { + deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.DismissAlertButtonSelector)) + } + return +} + func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { device = &IOSDevice{ Port: defaultWDAPort, @@ -134,6 +165,9 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { SnapshotMaxDepth: snapshotMaxDepth, AcceptAlertButtonSelector: acceptAlertButtonSelector, DismissAlertButtonSelector: dismissAlertButtonSelector, + // switch to iOS springboard before init WDA session + // avoid getting stuck when some super app is active such as douyin or wexin + ResetHomeOnStartup: true, } for _, option := range options { option(device) @@ -176,40 +210,6 @@ func (dev *IOSDevice) UUID() string { } func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt, err error) { - var deviceOptions []IOSDeviceOption - if dev.UDID != "" { - deviceOptions = append(deviceOptions, WithUDID(dev.UDID)) - } - if dev.Port != 0 { - deviceOptions = append(deviceOptions, WithWDAPort(dev.Port)) - } - if dev.MjpegPort != 0 { - deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort)) - } - if dev.LogOn { - deviceOptions = append(deviceOptions, WithLogOn(true)) - } - if dev.ResetHomeOnStartup { - deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true)) - } - if dev.SnapshotMaxDepth != 0 { - deviceOptions = append(deviceOptions, WithSnapshotMaxDepth(dev.SnapshotMaxDepth)) - } - if dev.AcceptAlertButtonSelector != "" { - deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector)) - } - if dev.DismissAlertButtonSelector != "" { - deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.DismissAlertButtonSelector)) - } - - iosDevice, err := NewIOSDevice(deviceOptions...) - if err != nil { - return nil, err - } - return iosDevice.initWDAClient(capabilities) -} - -func (dev *IOSDevice) initWDAClient(capabilities Capabilities) (driverExt *DriverExt, err error) { // init WDA driver if capabilities == nil { capabilities = NewCapabilities() diff --git a/hrp/runner.go b/hrp/runner.go index 6a302da2..68cd00d4 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -395,6 +395,9 @@ func (r *testCaseRunner) parseConfig() error { r.parametersIterator = parametersIterator // init iOS/Android clients + if r.hrpRunner.uiClients == nil { + r.hrpRunner.uiClients = make(map[string]*uixt.DriverExt) + } for _, iosDeviceConfig := range r.parsedConfig.IOS { if iosDeviceConfig.UDID != "" { udid, err := r.parser.ParseString(iosDeviceConfig.UDID, parsedVariables) @@ -403,13 +406,16 @@ func (r *testCaseRunner) parseConfig() error { } iosDeviceConfig.UDID = udid.(string) } - // switch to iOS springboard before init WDA session - // avoid getting stuck when some super app is active such as douyin or wexin - iosDeviceConfig.ResetHomeOnStartup = true - _, err := r.hrpRunner.initUIClient(iosDeviceConfig) + + device, err := uixt.NewIOSDevice(uixt.GetIOSDeviceOptions(iosDeviceConfig)...) + if err != nil { + return errors.Wrap(err, "init iOS device failed") + } + client, err := device.NewDriver(nil) if err != nil { return errors.Wrap(err, "init iOS WDA client failed") } + r.hrpRunner.uiClients[device.UDID] = client } for _, androidDeviceConfig := range r.parsedConfig.Android { if androidDeviceConfig.SerialNumber != "" { @@ -419,10 +425,15 @@ func (r *testCaseRunner) parseConfig() error { } androidDeviceConfig.SerialNumber = sn.(string) } - _, err := r.hrpRunner.initUIClient(androidDeviceConfig) + device, err := uixt.NewAndroidDevice(uixt.GetAndroidDeviceOptions(androidDeviceConfig)...) + if err != nil { + return errors.Wrap(err, "init iOS device failed") + } + client, err := device.NewDriver(nil) if err != nil { return errors.Wrap(err, "init Android UIAutomator client failed") } + r.hrpRunner.uiClients[device.SerialNumber] = client } return nil diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index a479b5c6..03eaac9f 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -517,17 +517,19 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err parser := s.GetParser() // parse device serial - if step.Android.AndroidDevice.SerialNumber != "" { - sn, err := parser.ParseString(step.Android.AndroidDevice.SerialNumber, stepVariables) + serial := step.Android.AndroidDevice.SerialNumber + if serial != "" { + sn, err := parser.ParseString(serial, stepVariables) if err != nil { return stepResult, err } - step.Android.AndroidDevice.SerialNumber = sn.(string) + serial = sn.(string) } - // init uiaClient driver - uiaClient, err := s.hrpRunner.initUIClient(&step.Android.AndroidDevice) - if err != nil { + // get uiaClient driver + uiaClient, ok := s.hrpRunner.uiClients[serial] + if !ok { + err = fmt.Errorf("uia client not found for device %s", serial) return } uiaClient.StartTime = s.startTime diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 5613dfe4..257a0189 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -490,34 +490,19 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { return runStepIOS(r, s.step) } -func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, err error) { - uuid := device.UUID() - - // avoid duplicate init +func (r *HRPRunner) getUIDriver(uuid string) (client *uixt.DriverExt, err error) { if uuid == "" && len(r.uiClients) > 0 { for _, v := range r.uiClients { return v, nil } } - // avoid duplicate init - if uuid != "" { - if client, ok := r.uiClients[uuid]; ok { - return client, nil - } + client, ok := r.uiClients[uuid] + if !ok { + err = fmt.Errorf("driver not found for device %s", uuid) + return } - client, err = device.NewDriver(nil) - if err != nil { - return nil, err - } - - // cache wda client - if r.uiClients == nil { - r.uiClients = make(map[string]*uixt.DriverExt) - } - r.uiClients[client.UUID] = client - return client, nil } @@ -538,16 +523,17 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro parser := s.GetParser() // parse device udid - if step.IOS.IOSDevice.UDID != "" { - udid, err := parser.ParseString(step.IOS.IOSDevice.UDID, stepVariables) + udid := step.IOS.IOSDevice.UDID + if udid != "" { + sn, err := parser.ParseString(udid, stepVariables) if err != nil { return stepResult, err } - step.IOS.IOSDevice.UDID = udid.(string) + udid = sn.(string) } // init wdaClient driver - wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.IOSDevice) + wdaClient, err := s.hrpRunner.getUIDriver(udid) if err != nil { return } From 6b2b662bb16df56ba311114d4613870842fd1c12 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 12 Oct 2022 20:25:02 +0800 Subject: [PATCH 136/169] refactor: vedem ocr get texts --- hrp/pkg/uixt/README.md | 8 +++--- hrp/pkg/uixt/demo/main_test.go | 7 ++--- hrp/pkg/uixt/ocr_off.go | 17 ------------ hrp/pkg/uixt/{ocr_on.go => ocr_vedem.go} | 35 +++++++++--------------- hrp/pkg/uixt/tap.go | 14 ++++------ scripts/build.sh | 6 ++-- 6 files changed, 28 insertions(+), 59 deletions(-) delete mode 100644 hrp/pkg/uixt/ocr_off.go rename hrp/pkg/uixt/{ocr_on.go => ocr_vedem.go} (88%) diff --git a/hrp/pkg/uixt/README.md b/hrp/pkg/uixt/README.md index 6fc5449a..c422d5d6 100644 --- a/hrp/pkg/uixt/README.md +++ b/hrp/pkg/uixt/README.md @@ -26,11 +26,11 @@ You can get more installation introduction on [hybridgroup/gocv]. ### OCR -OCR API is a paid service, you need to pre-purchase and configure the account key. +OCR API is a paid service, you need to pre-purchase and configure the environment variables. -```bash -$ make build tags=ocr -``` +- VEDEM_OCR_URL +- VEDEM_OCR_AK +- VEDEM_OCR_SK ## Thanks diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index e544eee6..044f3f92 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -30,14 +30,13 @@ func TestIOSDemo(t *testing.T) { // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 for { - _, err1 := driverExt.GetTextXY("青少年模式") - point, err2 := driverExt.GetTextXY("我知道了") - if err1 != nil || err2 != nil { + points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}) + if err != nil { time.Sleep(1 * time.Second) continue } - err := driverExt.TapAbsXY(point.X, point.Y, "") + err = driverExt.TapAbsXY(points[1].X, points[1].Y, "") if err != nil { t.Fatal(err) } diff --git a/hrp/pkg/uixt/ocr_off.go b/hrp/pkg/uixt/ocr_off.go deleted file mode 100644 index 03b2e505..00000000 --- a/hrp/pkg/uixt/ocr_off.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !ocr - -package uixt - -import ( - "github.com/rs/zerolog/log" -) - -func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) { - log.Fatal().Msg("OCR is not supported") - return -} - -func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (ps map[string][]float64, err error) { - log.Fatal().Msg("OCR is not supported") - return -} diff --git a/hrp/pkg/uixt/ocr_on.go b/hrp/pkg/uixt/ocr_vedem.go similarity index 88% rename from hrp/pkg/uixt/ocr_on.go rename to hrp/pkg/uixt/ocr_vedem.go index 7a5faf91..5ca2ef37 100644 --- a/hrp/pkg/uixt/ocr_on.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -1,5 +1,3 @@ -//go:build ocr - package uixt import ( @@ -159,25 +157,22 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( return rects[idx], nil } -func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects map[string]image.Rectangle, err error) { +func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects []image.Rectangle, err error) { ocrResults, err := s.getOCRResult(imageBuf) if err != nil { log.Error().Err(err).Msg("getOCRResult failed") return } - var ocrTexts []string - rects = map[string]image.Rectangle{} - for _, text := range texts { + var found bool for _, ocrResult := range ocrResults { - ocrTexts = append(ocrTexts, ocrResult.Text) - // not contains text if !strings.Contains(ocrResult.Text, text) { continue } + found = true rect := image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 Min: image.Point{ @@ -189,12 +184,11 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects map[ Y: int(ocrResult.Points[2].Y), }, } - rects[text] = rect + rects = append(rects, rect) break } - - if _, ok := rects[text]; !ok { - rects[text] = image.Rectangle{} + if !found { + rects = append(rects, image.Rectangle{}) } } @@ -220,12 +214,13 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, return } - log.Info().Str("ocrText", ocrText).Msgf("FindText success") + log.Info().Str("ocrText", ocrText). + Interface("rect", rect).Msgf("FindTextByOCR success") x, y, width, height = dExt.MappingToRectInUIKit(rect) return } -func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (ps map[string][]float64, err error) { +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (points [][]float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) @@ -240,15 +235,11 @@ func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (ps map[string][]float6 return } - ps = map[string][]float64{} - log.Info().Interface("ocrTexts", ocrTexts).Msgf("FindTexts success") - for text, rect := range rects { - if rect == (image.Rectangle{}) { - ps[text] = []float64{} - continue - } + log.Info().Interface("ocrTexts", ocrTexts). + Interface("rects", rects).Msgf("FindTextsByOCR success") + for _, rect := range rects { x, y, width, height := dExt.MappingToRectInUIKit(rect) - ps[text] = []float64{x, y, width, height} + points = append(points, []float64{x, y, width, height}) } return diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index 10e1f79c..7957f3dd 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -41,22 +41,18 @@ func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, er return point, nil } -func (dExt *DriverExt) GetTextXYs(ocrText []string) (points map[string]PointF, err error) { +func (dExt *DriverExt) GetTextXYs(ocrText []string) (points []PointF, err error) { ps, err := dExt.FindTextsByOCR(ocrText) if err != nil { - return map[string]PointF{}, err + return nil, err } - points = map[string]PointF{} - for text, point := range ps { - if len(point) == 0 { - points[text] = PointF{} - continue - } - points[text] = PointF{ + for _, point := range ps { + pointF := PointF{ X: point[0] + point[2]*0.5, Y: point[1] + point[3]*0.5, } + points = append(points, pointF) } return points, nil diff --git a/scripts/build.sh b/scripts/build.sh index 67c514ce..e867f9d3 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,10 +4,10 @@ # Usage: # $ make build -# $ make build tags=ocr +# $ make build tags=opencv # or # $ bash scripts/build.sh -# $ bash scripts/build.sh ocr +# $ bash scripts/build.sh opencv set -e set -x @@ -16,7 +16,7 @@ set -x mkdir -p "output" bin_path="output/hrp" -# optional build tags: opencv ocr +# optional build tags: opencv tags=$1 # build From c22b38f76f97adc0e4d8027f643633f60931171a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 12 Oct 2022 21:07:57 +0800 Subject: [PATCH 137/169] refactor: merge ios and android style --- examples/uitest/demo_douyin_follow_live.json | 4 +- examples/uitest/demo_douyin_follow_live.yaml | 4 +- .../uitest/demo_douyin_follow_live_test.go | 4 +- hrp/runner.go | 1 - hrp/step.go | 4 +- hrp/step_android_ui.go | 603 ------------------ hrp/step_android_ui_test.go | 26 - hrp/{step_ios_ui.go => step_mobile_ui.go} | 307 +++++---- ..._ios_ui_test.go => step_mobile_ui_test.go} | 24 +- hrp/step_request.go | 12 +- hrp/testcase.go | 4 +- 11 files changed, 212 insertions(+), 781 deletions(-) delete mode 100644 hrp/step_android_ui.go delete mode 100644 hrp/step_android_ui_test.go rename hrp/{step_ios_ui.go => step_mobile_ui.go} (50%) rename hrp/{step_ios_ui_test.go => step_mobile_ui_test.go} (86%) diff --git a/examples/uitest/demo_douyin_follow_live.json b/examples/uitest/demo_douyin_follow_live.json index 4f9b411b..171ef268 100644 --- a/examples/uitest/demo_douyin_follow_live.json +++ b/examples/uitest/demo_douyin_follow_live.json @@ -6,8 +6,8 @@ }, "ios": [ { - "port": 8100, - "mjpeg_port": 9100, + "port": 8700, + "mjpeg_port": 8800, "log_on": true } ] diff --git a/examples/uitest/demo_douyin_follow_live.yaml b/examples/uitest/demo_douyin_follow_live.yaml index 454df7c8..f6c34c3f 100644 --- a/examples/uitest/demo_douyin_follow_live.yaml +++ b/examples/uitest/demo_douyin_follow_live.yaml @@ -3,8 +3,8 @@ config: variables: app_name: 抖音 ios: - - port: 8100 - mjpeg_port: 9100 + - port: 8700 + mjpeg_port: 8800 log_on: true teststeps: - name: 启动抖音 diff --git a/examples/uitest/demo_douyin_follow_live_test.go b/examples/uitest/demo_douyin_follow_live_test.go index ad8fc608..1eb42f93 100644 --- a/examples/uitest/demo_douyin_follow_live_test.go +++ b/examples/uitest/demo_douyin_follow_live_test.go @@ -16,8 +16,8 @@ func TestIOSDouyinFollowLive(t *testing.T) { }). SetIOS( hrp.WithLogOn(true), - hrp.WithWDAPort(8100), - hrp.WithWDAMjpegPort(9100), + hrp.WithWDAPort(8700), + hrp.WithWDAMjpegPort(8800), ), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). diff --git a/hrp/runner.go b/hrp/runner.go index 68cd00d4..5fd4a102 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -406,7 +406,6 @@ func (r *testCaseRunner) parseConfig() error { } iosDeviceConfig.UDID = udid.(string) } - device, err := uixt.NewIOSDevice(uixt.GetIOSDeviceOptions(iosDeviceConfig)...) if err != nil { return errors.Wrap(err, "init iOS device failed") diff --git a/hrp/step.go b/hrp/step.go index 98bccab7..ac7481f1 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -71,8 +71,8 @@ type TStep struct { Rendezvous *Rendezvous `json:"rendezvous,omitempty" yaml:"rendezvous,omitempty"` ThinkTime *ThinkTime `json:"think_time,omitempty" yaml:"think_time,omitempty"` WebSocket *WebSocketAction `json:"websocket,omitempty" yaml:"websocket,omitempty"` - Android *AndroidStep `json:"android,omitempty" yaml:"android,omitempty"` - IOS *IOSStep `json:"ios,omitempty" yaml:"ios,omitempty"` + Android *MobileStep `json:"android,omitempty" yaml:"android,omitempty"` + IOS *MobileStep `json:"ios,omitempty" yaml:"ios,omitempty"` Variables map[string]interface{} `json:"variables,omitempty" yaml:"variables,omitempty"` SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go deleted file mode 100644 index 03eaac9f..00000000 --- a/hrp/step_android_ui.go +++ /dev/null @@ -1,603 +0,0 @@ -package hrp - -import ( - "fmt" - "time" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" -) - -var ( - WithSerialNumber = uixt.WithSerialNumber - WithAdbIP = uixt.WithAdbIP - WithAdbPort = uixt.WithAdbPort - WithAdbLogOn = uixt.WithAdbLogOn -) - -type AndroidStep struct { - uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal - uixt.MobileAction - Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` -} - -// StepAndroid implements IStep interface. -type StepAndroid struct { - step *TStep -} - -func (s *StepAndroid) Serial(serial string) *StepAndroid { - s.step.Android.SerialNumber = serial - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) InstallApp(path string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.AppInstall, - Params: path, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) AppLaunch(bundleId string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.AppLaunch, - Params: bundleId, - }) - return s -} - -func (s *StepAndroid) AppLaunchUnattached(bundleId string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.AppLaunchUnattached, - Params: bundleId, - }) - return s -} - -func (s *StepAndroid) AppTerminate(bundleId string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.AppTerminate, - Params: bundleId, - }) - return s -} - -func (s *StepAndroid) Home() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.ACTION_Home, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.AppStart, - Params: activity, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) StartCamera() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.CtlStartCamera, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) StopCamera() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.CtlStopCamera, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) StartRecording() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.RecordStart, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) StopRecording() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.RecordStop, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -// TapXY taps the point {X,Y}, X & Y is percentage of coordinates -func (s *StepAndroid) TapXY(x, y float64, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_TapXY, - Params: []float64{x, y}, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates -func (s *StepAndroid) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_TapAbsXY, - Params: []float64{x, y}, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// Tap taps on the target element -func (s *StepAndroid) Tap(params string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Tap, - Params: params, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// Tap taps on the target element by OCR recognition -func (s *StepAndroid) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_TapByOCR, - Params: ocrText, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// Tap taps on the target element by CV recognition -func (s *StepAndroid) TapByCV(imagePath string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_TapByCV, - Params: imagePath, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) DoubleTap(params string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_DoubleTap, - Params: params, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Swipe, - Params: []float64{sx, sy, ex, ey}, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeUp(options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Swipe, - Params: "up", - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeDown(options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Swipe, - Params: "down", - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeLeft(options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Swipe, - Params: "left", - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeRight(options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Swipe, - Params: "right", - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) Input(text string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_Input, - Params: text, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// Times specify running times for run last action -func (s *StepAndroid) Times(n int) *StepAndroid { - if n <= 0 { - log.Warn().Int("n", n).Msg("times should be positive, set to 1") - n = 1 - } - - actionsTotal := len(s.step.Android.Actions) - if actionsTotal == 0 { - return s - } - - // actionsTotal >=1 && n >= 1 - lastAction := s.step.Android.Actions[actionsTotal-1 : actionsTotal][0] - for i := 0; i < n-1; i++ { - // duplicate last action n-1 times - s.step.Android.Actions = append(s.step.Android.Actions, lastAction) - } - return &StepAndroid{step: s.step} -} - -// Sleep specify sleep seconds after last action -func (s *StepAndroid) Sleep(n float64) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.CtlSleep, - Params: n, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) ScreenShot() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ - Method: uixt.CtlScreenShot, - Params: nil, - }) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_SwipeToTapApp, - Params: appName, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_SwipeToTapText, - Params: text, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -func (s *StepAndroid) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepAndroid { - action := uixt.MobileAction{ - Method: uixt.ACTION_SwipeToTapTexts, - Params: texts, - } - for _, option := range options { - option(&action) - } - s.step.Android.Actions = append(s.step.Android.Actions, action) - return &StepAndroid{step: s.step} -} - -// Validate switches to step validation. -func (s *StepAndroid) Validate() *StepAndroidValidation { - return &StepAndroidValidation{ - step: s.step, - } -} - -func (s *StepAndroid) Name() string { - return s.step.Name -} - -func (s *StepAndroid) Type() StepType { - return stepTypeAndroid -} - -func (s *StepAndroid) Struct() *TStep { - return s.step -} - -func (s *StepAndroid) Run(r *SessionRunner) (*StepResult, error) { - return runStepAndroid(r, s.step) -} - -// StepAndroidValidation implements IStep interface. -type StepAndroidValidation struct { - step *TStep -} - -func (s *StepAndroidValidation) AssertNameExists(expectedName string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorName, - Assert: uixt.AssertionExists, - Expect: expectedName, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("[%s] not found", expectedName) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorName, - Assert: uixt.AssertionNotExists, - Expect: expectedName, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("[%s] should not exist", expectedName) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorLabel, - Assert: uixt.AssertionExists, - Expect: expectedLabel, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorLabel, - Assert: uixt.AssertionNotExists, - Expect: expectedLabel, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertOCRExists(expectedText string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorOCR, - Assert: uixt.AssertionExists, - Expect: expectedText, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorOCR, - Assert: uixt.AssertionNotExists, - Expect: expectedText, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorImage, - Assert: uixt.AssertionExists, - Expect: expectedImagePath, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepAndroidValidation { - v := Validator{ - Check: uixt.SelectorImage, - Assert: uixt.AssertionNotExists, - Expect: expectedImagePath, - } - if len(msg) > 0 { - v.Message = msg[0] - } else { - v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath) - } - s.step.Validators = append(s.step.Validators, v) - return s -} - -func (s *StepAndroidValidation) Name() string { - return s.step.Name -} - -func (s *StepAndroidValidation) Type() StepType { - return stepTypeAndroid -} - -func (s *StepAndroidValidation) Struct() *TStep { - return s.step -} - -func (s *StepAndroidValidation) Run(r *SessionRunner) (*StepResult, error) { - return runStepAndroid(r, s.step) -} - -func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { - stepResult = &StepResult{ - Name: step.Name, - StepType: stepTypeAndroid, - Success: false, - ContentSize: 0, - } - screenshots := make([]string, 0) - - // override step variables - stepVariables, err := s.MergeStepVariables(step.Variables) - if err != nil { - return - } - parser := s.GetParser() - - // parse device serial - serial := step.Android.AndroidDevice.SerialNumber - if serial != "" { - sn, err := parser.ParseString(serial, stepVariables) - if err != nil { - return stepResult, err - } - serial = sn.(string) - } - - // get uiaClient driver - uiaClient, ok := s.hrpRunner.uiClients[serial] - if !ok { - err = fmt.Errorf("uia client not found for device %s", serial) - return - } - uiaClient.StartTime = s.startTime - - defer func() { - attachments := make(map[string]interface{}) - if err != nil { - attachments["error"] = err.Error() - } - - // save attachments - screenshots = append(screenshots, uiaClient.ScreenShots...) - attachments["screenshots"] = screenshots - stepResult.Attachments = attachments - - // update summary - s.summary.Records = append(s.summary.Records, stepResult) - s.summary.Stat.Total += 1 - if stepResult.Success { - s.summary.Stat.Successes += 1 - } else { - s.summary.Stat.Failures += 1 - // update summary result to failed - s.summary.Success = false - } - }() - - // prepare actions - var actions []uixt.MobileAction - if step.Android.Actions == nil { - actions = []uixt.MobileAction{ - { - Method: step.Android.Method, - Params: step.Android.Params, - }, - } - } else { - actions = step.Android.Actions - } - - // run actions - for _, action := range actions { - if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { - return stepResult, errors.Wrap(err, "parse action params failed") - } - if err := uiaClient.DoAction(action); err != nil { - return stepResult, err - } - } - - // take snapshot - screenshotPath, err := uiaClient.ScreenShot( - fmt.Sprintf("%d_validate_%d", uiaClient.StartTime.Unix(), time.Now().Unix())) - if err != nil { - log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") - } else { - log.Info().Str("path", screenshotPath).Msg("take screenshot before validation") - screenshots = append(screenshots, screenshotPath) - } - - // validate - validateResults, err := validateUI(uiaClient, step.Validators) - if err != nil { - return - } - sessionData := newSessionData() - sessionData.Validators = validateResults - stepResult.Data = sessionData - stepResult.Success = true - return stepResult, nil -} diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go deleted file mode 100644 index ad02cfe4..00000000 --- a/hrp/step_android_ui_test.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build localtest - -package hrp - -import ( - "testing" -) - -func TestAndroidAction(t *testing.T) { - testCase := &TestCase{ - Config: NewConfig("android ui action"), - TestSteps: []IStep{ - NewStep("launch douyin"). - Android().Serial("xxx").Tap("抖音"). - Validate(). - AssertNameExists("首页", "首页 tab 不存在"). - AssertNameExists("消息", "消息 tab 不存在"), - NewStep("swipe up and down"). - Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), - }, - } - err := NewRunner(t).Run(testCase) - if err != nil { - t.Fatal(err) - } -} diff --git a/hrp/step_ios_ui.go b/hrp/step_mobile_ui.go similarity index 50% rename from hrp/step_ios_ui.go rename to hrp/step_mobile_ui.go index 257a0189..64e22f65 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_mobile_ui.go @@ -4,12 +4,12 @@ import ( "fmt" "time" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) +// ios setting options var ( WithUDID = uixt.WithUDID WithWDAPort = uixt.WithWDAPort @@ -22,64 +22,79 @@ var ( WithPerfOptions = uixt.WithPerfOptions ) -type IOSStep struct { - uixt.IOSDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal +// android setting options +var ( + WithSerialNumber = uixt.WithSerialNumber + WithAdbIP = uixt.WithAdbIP + WithAdbPort = uixt.WithAdbPort + WithAdbLogOn = uixt.WithAdbLogOn +) + +type MobileStep struct { + Serial string `json:"serial,omitempty" yaml:"serial,omitempty"` uixt.MobileAction `yaml:",inline"` Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"` } -// StepIOS implements IStep interface. -type StepIOS struct { +// StepMobile implements IStep interface. +type StepMobile struct { step *TStep } -func (s *StepIOS) UDID(udid string) *StepIOS { - s.step.IOS.UDID = udid - return &StepIOS{step: s.step} +func (s *StepMobile) mobileStep() *MobileStep { + if s.step.IOS != nil { + return s.step.IOS + } + return s.step.Android } -func (s *StepIOS) InstallApp(path string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) Serial(serial string) *StepMobile { + s.mobileStep().Serial = serial + return &StepMobile{step: s.step} +} + +func (s *StepMobile) InstallApp(path string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.AppInstall, Params: path, }) return s } -func (s *StepIOS) AppLaunch(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) AppLaunch(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.AppLaunch, Params: bundleId, }) return s } -func (s *StepIOS) AppLaunchUnattached(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) AppLaunchUnattached(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.AppLaunchUnattached, Params: bundleId, }) return s } -func (s *StepIOS) AppTerminate(bundleId string) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) AppTerminate(bundleId string) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.AppTerminate, Params: bundleId, }) return s } -func (s *StepIOS) Home() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) Home() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.ACTION_Home, Params: nil, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } // TapXY taps the point {X,Y}, X & Y is percentage of coordinates -func (s *StepIOS) TapXY(x, y float64, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) TapXY(x, y float64, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_TapXY, Params: []float64{x, y}, @@ -87,12 +102,12 @@ func (s *StepIOS) TapXY(x, y float64, options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // TapAbsXY taps the point {X,Y}, X & Y is absolute coordinates -func (s *StepIOS) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_TapAbsXY, Params: []float64{x, y}, @@ -100,12 +115,12 @@ func (s *StepIOS) TapAbsXY(x, y float64, options ...uixt.ActionOption) *StepIOS for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // Tap taps on the target element -func (s *StepIOS) Tap(params string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) Tap(params string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Tap, Params: params, @@ -113,12 +128,12 @@ func (s *StepIOS) Tap(params string, options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // Tap taps on the target element by OCR recognition -func (s *StepIOS) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_TapByOCR, Params: ocrText, @@ -126,12 +141,12 @@ func (s *StepIOS) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepIO for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // Tap taps on the target element by CV recognition -func (s *StepIOS) TapByCV(imagePath string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) TapByCV(imagePath string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_TapByCV, Params: imagePath, @@ -139,20 +154,20 @@ func (s *StepIOS) TapByCV(imagePath string, options ...uixt.ActionOption) *StepI for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // DoubleTapXY double taps the point {X,Y}, X & Y is percentage of coordinates -func (s *StepIOS) DoubleTapXY(x, y float64) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) DoubleTapXY(x, y float64) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.ACTION_DoubleTapXY, Params: []float64{x, y}, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } -func (s *StepIOS) DoubleTap(params string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) DoubleTap(params string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_DoubleTap, Params: params, @@ -160,11 +175,11 @@ func (s *StepIOS) DoubleTap(params string, options ...uixt.ActionOption) *StepIO for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: []float64{sx, sy, ex, ey}, @@ -172,11 +187,11 @@ func (s *StepIOS) Swipe(sx, sy, ex, ey float64, options ...uixt.ActionOption) *S for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeUp(options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeUp(options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "up", @@ -184,11 +199,11 @@ func (s *StepIOS) SwipeUp(options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeDown(options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeDown(options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "down", @@ -196,11 +211,11 @@ func (s *StepIOS) SwipeDown(options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeLeft(options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeLeft(options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "left", @@ -208,11 +223,11 @@ func (s *StepIOS) SwipeLeft(options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeRight(options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeRight(options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "right", @@ -220,11 +235,11 @@ func (s *StepIOS) SwipeRight(options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapApp, Params: appName, @@ -232,11 +247,11 @@ func (s *StepIOS) SwipeToTapApp(appName string, options ...uixt.ActionOption) *S for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeToTapText(text string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapText, Params: text, @@ -244,11 +259,11 @@ func (s *StepIOS) SwipeToTapText(text string, options ...uixt.ActionOption) *Ste for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapTexts, Params: texts, @@ -256,11 +271,11 @@ func (s *StepIOS) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } -func (s *StepIOS) Input(text string, options ...uixt.ActionOption) *StepIOS { +func (s *StepMobile) Input(text string, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_Input, Params: text, @@ -268,93 +283,94 @@ func (s *StepIOS) Input(text string, options ...uixt.ActionOption) *StepIOS { for _, option := range options { option(&action) } - s.step.IOS.Actions = append(s.step.IOS.Actions, action) - return &StepIOS{step: s.step} + s.mobileStep().Actions = append(s.mobileStep().Actions, action) + return &StepMobile{step: s.step} } // Times specify running times for run last action -func (s *StepIOS) Times(n int) *StepIOS { +func (s *StepMobile) Times(n int) *StepMobile { if n <= 0 { log.Warn().Int("n", n).Msg("times should be positive, set to 1") n = 1 } - actionsTotal := len(s.step.IOS.Actions) + mobileStep := s.mobileStep() + actionsTotal := len(mobileStep.Actions) if actionsTotal == 0 { return s } // actionsTotal >=1 && n >= 1 - lastAction := s.step.IOS.Actions[actionsTotal-1 : actionsTotal][0] + lastAction := mobileStep.Actions[actionsTotal-1 : actionsTotal][0] for i := 0; i < n-1; i++ { // duplicate last action n-1 times - s.step.IOS.Actions = append(s.step.IOS.Actions, lastAction) + mobileStep.Actions = append(mobileStep.Actions, lastAction) } - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } // Sleep specify sleep seconds after last action -func (s *StepIOS) Sleep(n float64) *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) Sleep(n float64) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.CtlSleep, Params: n, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } -func (s *StepIOS) ScreenShot() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) ScreenShot() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.CtlScreenShot, Params: nil, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } -func (s *StepIOS) StartCamera() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) StartCamera() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.CtlStartCamera, Params: nil, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } -func (s *StepIOS) StopCamera() *StepIOS { - s.step.IOS.Actions = append(s.step.IOS.Actions, uixt.MobileAction{ +func (s *StepMobile) StopCamera() *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.CtlStopCamera, Params: nil, }) - return &StepIOS{step: s.step} + return &StepMobile{step: s.step} } // Validate switches to step validation. -func (s *StepIOS) Validate() *StepIOSValidation { - return &StepIOSValidation{ +func (s *StepMobile) Validate() *StepMobileUIValidation { + return &StepMobileUIValidation{ step: s.step, } } -func (s *StepIOS) Name() string { +func (s *StepMobile) Name() string { return s.step.Name } -func (s *StepIOS) Type() StepType { +func (s *StepMobile) Type() StepType { return stepTypeIOS } -func (s *StepIOS) Struct() *TStep { +func (s *StepMobile) Struct() *TStep { return s.step } -func (s *StepIOS) Run(r *SessionRunner) (*StepResult, error) { - return runStepIOS(r, s.step) +func (s *StepMobile) Run(r *SessionRunner) (*StepResult, error) { + return runStepMobileUI(r, s.step) } -// StepIOSValidation implements IStep interface. -type StepIOSValidation struct { +// StepMobileUIValidation implements IStep interface. +type StepMobileUIValidation struct { step *TStep } -func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertNameExists(expectedName string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorName, Assert: uixt.AssertionExists, @@ -369,7 +385,7 @@ func (s *StepIOSValidation) AssertNameExists(expectedName string, msg ...string) return s } -func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertNameNotExists(expectedName string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorName, Assert: uixt.AssertionNotExists, @@ -384,7 +400,7 @@ func (s *StepIOSValidation) AssertNameNotExists(expectedName string, msg ...stri return s } -func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorLabel, Assert: uixt.AssertionExists, @@ -399,7 +415,7 @@ func (s *StepIOSValidation) AssertLabelExists(expectedLabel string, msg ...strin return s } -func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorLabel, Assert: uixt.AssertionNotExists, @@ -414,7 +430,7 @@ func (s *StepIOSValidation) AssertLabelNotExists(expectedLabel string, msg ...st return s } -func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertOCRExists(expectedText string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorOCR, Assert: uixt.AssertionExists, @@ -429,7 +445,7 @@ func (s *StepIOSValidation) AssertOCRExists(expectedText string, msg ...string) return s } -func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorOCR, Assert: uixt.AssertionNotExists, @@ -444,7 +460,7 @@ func (s *StepIOSValidation) AssertOCRNotExists(expectedText string, msg ...strin return s } -func (s *StepIOSValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorImage, Assert: uixt.AssertionExists, @@ -459,7 +475,7 @@ func (s *StepIOSValidation) AssertImageExists(expectedImagePath string, msg ...s return s } -func (s *StepIOSValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepIOSValidation { +func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepMobileUIValidation { v := Validator{ Check: uixt.SelectorImage, Assert: uixt.AssertionNotExists, @@ -474,39 +490,62 @@ func (s *StepIOSValidation) AssertImageNotExists(expectedImagePath string, msg . return s } -func (s *StepIOSValidation) Name() string { +func (s *StepMobileUIValidation) Name() string { return s.step.Name } -func (s *StepIOSValidation) Type() StepType { +func (s *StepMobileUIValidation) Type() StepType { return stepTypeIOS } -func (s *StepIOSValidation) Struct() *TStep { +func (s *StepMobileUIValidation) Struct() *TStep { return s.step } -func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) { - return runStepIOS(r, s.step) +func (s *StepMobileUIValidation) Run(r *SessionRunner) (*StepResult, error) { + return runStepMobileUI(r, s.step) } -func (r *HRPRunner) getUIDriver(uuid string) (client *uixt.DriverExt, err error) { +func (r *HRPRunner) initUIClient(uuid string, osType string) (client *uixt.DriverExt, err error) { + // avoid duplicate init if uuid == "" && len(r.uiClients) > 0 { for _, v := range r.uiClients { return v, nil } } - client, ok := r.uiClients[uuid] - if !ok { - err = fmt.Errorf("driver not found for device %s", uuid) - return + // avoid duplicate init + if uuid != "" { + if client, ok := r.uiClients[uuid]; ok { + return client, nil + } } + var device uixt.Device + if osType == "ios" { + device, err = uixt.NewIOSDevice(uixt.WithUDID(uuid)) + } else { + device, err = uixt.NewAndroidDevice(uixt.WithSerialNumber(uuid)) + } + if err != nil { + return nil, errors.Wrapf(err, "init %s device failed", osType) + } + + client, err = device.NewDriver(nil) + if err != nil { + return nil, err + } + + // cache wda client + if r.uiClients == nil { + r.uiClients = make(map[string]*uixt.DriverExt) + } + r.uiClients[client.UUID] = client + return client, nil } -func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { +func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, StepType: stepTypeIOS, @@ -522,22 +561,24 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } parser := s.GetParser() - // parse device udid - udid := step.IOS.IOSDevice.UDID - if udid != "" { - sn, err := parser.ParseString(udid, stepVariables) - if err != nil { - return stepResult, err - } - udid = sn.(string) + var osType string + var mobileStep *MobileStep + if step.IOS != nil { + // ios step + osType = "ios" + mobileStep = step.IOS + } else { + // android step + osType = "android" + mobileStep = step.Android } - // init wdaClient driver - wdaClient, err := s.hrpRunner.getUIDriver(udid) + // init wda/uia driver + uiDriver, err := s.hrpRunner.initUIClient(mobileStep.Serial, osType) if err != nil { return } - wdaClient.StartTime = s.startTime + uiDriver.StartTime = s.startTime defer func() { attachments := make(map[string]interface{}) @@ -546,7 +587,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // save attachments - screenshots = append(screenshots, wdaClient.ScreenShots...) + screenshots = append(screenshots, uiDriver.ScreenShots...) attachments["screenshots"] = screenshots stepResult.Attachments = attachments @@ -564,15 +605,15 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro // prepare actions var actions []uixt.MobileAction - if step.IOS.Actions == nil { + if mobileStep.Actions == nil { actions = []uixt.MobileAction{ { - Method: step.IOS.Method, - Params: step.IOS.Params, + Method: mobileStep.Method, + Params: mobileStep.Params, }, } } else { - actions = step.IOS.Actions + actions = mobileStep.Actions } // run actions @@ -580,14 +621,14 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } - if err := wdaClient.DoAction(action); err != nil { + if err := uiDriver.DoAction(action); err != nil { return stepResult, err } } // take snapshot - screenshotPath, err := wdaClient.ScreenShot( - fmt.Sprintf("%d_validate_%d", wdaClient.StartTime.Unix(), time.Now().Unix())) + screenshotPath, err := uiDriver.ScreenShot( + fmt.Sprintf("%d_validate_%d", uiDriver.StartTime.Unix(), time.Now().Unix())) if err != nil { log.Warn().Err(err).Str("step", step.Name).Msg("take screenshot failed") } else { @@ -596,7 +637,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro } // validate - validateResults, err := validateUI(wdaClient, step.Validators) + validateResults, err := validateUI(uiDriver, step.Validators) if err != nil { return } diff --git a/hrp/step_ios_ui_test.go b/hrp/step_mobile_ui_test.go similarity index 86% rename from hrp/step_ios_ui_test.go rename to hrp/step_mobile_ui_test.go index 0767b8d6..c9c7dae3 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_mobile_ui_test.go @@ -8,7 +8,8 @@ import ( func TestIOSSettingsAction(t *testing.T) { testCase := &TestCase{ - Config: NewConfig("ios ui action on Settings"), + Config: NewConfig("ios ui action on Settings"). + SetIOS(WithWDAPort(8700), WithWDAMjpegPort(8800)), TestSteps: []IStep{ NewStep("launch Settings"). IOS().Home().Tap("设置"). @@ -47,7 +48,7 @@ func TestIOSSearchApp(t *testing.T) { func TestIOSAppLaunch(t *testing.T) { testCase := &TestCase{ Config: NewConfig("启动 & 关闭 App"). - SetIOS(WithWDAPort(8100), WithWDAMjpegPort(9100)), + SetIOS(WithWDAPort(8700), WithWDAMjpegPort(8800)), TestSteps: []IStep{ NewStep("终止今日头条"). IOS().AppTerminate("com.ss.iphone.article.News"), @@ -161,3 +162,22 @@ func TestIOSDouyinAction(t *testing.T) { t.Fatal(err) } } + +func TestAndroidAction(t *testing.T) { + testCase := &TestCase{ + Config: NewConfig("android ui action"), + TestSteps: []IStep{ + NewStep("launch douyin"). + Android().Serial("xxx").Tap("抖音"). + Validate(). + AssertNameExists("首页", "首页 tab 不存在"). + AssertNameExists("消息", "消息 tab 不存在"), + NewStep("swipe up and down"). + Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(), + }, + } + err := NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/step_request.go b/hrp/step_request.go index cc656d34..e5adf26c 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -763,17 +763,17 @@ func (s *StepRequest) WebSocket() *StepWebSocket { } // Android creates a new android action -func (s *StepRequest) Android() *StepAndroid { - s.step.Android = &AndroidStep{} - return &StepAndroid{ +func (s *StepRequest) Android() *StepMobile { + s.step.Android = &MobileStep{} + return &StepMobile{ step: s.step, } } // IOS creates a new ios action -func (s *StepRequest) IOS() *StepIOS { - s.step.IOS = &IOSStep{} - return &StepIOS{ +func (s *StepRequest) IOS() *StepMobile { + s.step.IOS = &MobileStep{} + return &StepMobile{ step: s.step, } } diff --git a/hrp/testcase.go b/hrp/testcase.go index 6f4bd46b..401b179b 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -261,11 +261,11 @@ func (tc *TCase) toTestCase() (*TestCase, error) { step: step, }) } else if step.IOS != nil { - testCase.TestSteps = append(testCase.TestSteps, &StepIOS{ + testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ step: step, }) } else if step.Android != nil { - testCase.TestSteps = append(testCase.TestSteps, &StepAndroid{ + testCase.TestSteps = append(testCase.TestSteps, &StepMobile{ step: step, }) } else { From 245d3884d8e3d8747e6b5d2db0360d3965c67a9b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 12 Oct 2022 21:07:57 +0800 Subject: [PATCH 138/169] refactor: merge ios and android style --- hrp/step_mobile_ui.go | 1 + 1 file changed, 1 insertion(+) diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 64e22f65..9a9d8d61 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -575,6 +575,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // init wda/uia driver uiDriver, err := s.hrpRunner.initUIClient(mobileStep.Serial, osType) + if err != nil { return } From 1392fe293f4ae7ce74f47a884bf4bd60be067d01 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Wed, 12 Oct 2022 18:18:59 +0800 Subject: [PATCH 139/169] fix: add ocr auth --- hrp/internal/builtin/utils.go | 18 ++++++++++++++++++ hrp/internal/env/env.go | 2 ++ hrp/pkg/uixt/ocr_vedem.go | 4 ++++ hrp/step_mobile_ui.go | 1 - 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 4d5fefdc..18b60a93 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -3,6 +3,8 @@ package builtin import ( "bufio" "bytes" + "crypto/hmac" + "crypto/sha256" "encoding/binary" "encoding/csv" builtinJSON "encoding/json" @@ -15,6 +17,7 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -565,3 +568,18 @@ func SplitInteger(m, n int) (ints []int) { } return } + +func sha256HMAC(key []byte, data []byte) []byte { + mac := hmac.New(sha256.New, key) + mac.Write(data) + return []byte(fmt.Sprintf("%x", mac.Sum(nil))) +} + +// ver: auth-v1or auth-v2 +func Sign(ver string, ak string, sk string, body []byte) string { + expiration := 1800 + signKeyInfo := fmt.Sprintf("%s/%s/%d/%d", ver, ak, time.Now().Unix(), expiration) + signKey := sha256HMAC([]byte(sk), []byte(signKeyInfo)) + signResult := sha256HMAC(signKey, body) + return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult)) +} diff --git a/hrp/internal/env/env.go b/hrp/internal/env/env.go index c69ca633..4fbb84e0 100644 --- a/hrp/internal/env/env.go +++ b/hrp/internal/env/env.go @@ -5,6 +5,8 @@ import "os" var ( WDA_USB_DRIVER = os.Getenv("WDA_USB_DRIVER") VEDEM_OCR_URL = os.Getenv("VEDEM_OCR_URL") + VEDEM_OCR_AK = os.Getenv("VEDEM_OCR_AK") + VEDEM_OCR_SK = os.Getenv("VEDEM_OCR_SK") DISABLE_GA = os.Getenv("DISABLE_GA") DISABLE_SENTRY = os.Getenv("DISABLE_SENTRY") PYPI_INDEX_URL = os.Getenv("PYPI_INDEX_URL") diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 5ca2ef37..f160c274 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -13,6 +13,7 @@ import ( "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -64,6 +65,9 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return nil, fmt.Errorf("construct request error: %v", err) } + token := builtin.Sign("auth-v2", env.VEDEM_OCR_AK, env.VEDEM_OCR_SK, bodyBuf.Bytes()) + + req.Header.Add("Agw-Auth", token) req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) resp, err := client.Do(req) if err != nil { diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 9a9d8d61..64e22f65 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -575,7 +575,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // init wda/uia driver uiDriver, err := s.hrpRunner.initUIClient(mobileStep.Serial, osType) - if err != nil { return } From 2a51f8dfb4314cf17fc17083b29301df37d39487 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 13 Oct 2022 11:18:36 +0800 Subject: [PATCH 140/169] change: bump version --- hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index f1f06ca1..64efb591 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10112220 \ No newline at end of file +v4.3.0-beta-10131119 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 715e1099..fad89a02 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10112220" +__version__ = "v4.3.0-beta-10131119" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 13bdcbc0..216a12d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10112220" +version = "v4.3.0-beta-10131119" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From c9279fb4f702bac056ae760f27ee3d7f71779361 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Thu, 13 Oct 2022 14:17:24 +0800 Subject: [PATCH 141/169] feat: input params by funcion in ui automation --- hrp/internal/builtin/function.go | 5 +++++ hrp/pkg/uixt/ext.go | 8 ++++++++ hrp/step.go | 1 + hrp/step_mobile_ui.go | 7 +++++++ 4 files changed, 21 insertions(+) diff --git a/hrp/internal/builtin/function.go b/hrp/internal/builtin/function.go index a5b3c36f..00d01d97 100644 --- a/hrp/internal/builtin/function.go +++ b/hrp/internal/builtin/function.go @@ -28,6 +28,7 @@ var Functions = map[string]interface{}{ "md5": MD5, // call with one argument "parameterize": loadFromCSV, "P": loadFromCSV, + "split_by_comma": splitByComma, // call with one argument "environ": os.Getenv, "ENV": os.Getenv, "load_ws_message": loadMessage, @@ -225,3 +226,7 @@ func multipartContentType(w *TFormDataWriter) string { } return w.Writer.FormDataContentType() } + +func splitByComma(s string) []string { + return strings.Split(s, ",") +} diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 25a3bb44..276b6946 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -67,6 +67,7 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app + Function string `json:"function,omitempty" yaml:"function,omitempty"` // used to replace params Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found @@ -89,6 +90,13 @@ func WithIndex(index int) ActionOption { } } +// WithFunction replaces params +func WithFunction(function string) ActionOption { + return func(o *MobileAction) { + o.Function = function + } +} + // WithDirection inputs direction (up, down, left, right) func WithDirection(direction string) ActionOption { return func(o *MobileAction) { diff --git a/hrp/step.go b/hrp/step.go index ac7481f1..f10fc84f 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -31,6 +31,7 @@ var ( WithDescription = uixt.WithDescription WithDirection = uixt.WithDirection WithCustomDirection = uixt.WithCustomDirection + WithFunction = uixt.WithFunction ) var ( diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 64e22f65..fa924edb 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -618,6 +618,13 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { + if action.Function != "" { + parsedParams, err := parser.ParseString(action.Function, stepVariables) + if err != nil { + return stepResult, err + } + action.Params = parsedParams + } if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } From 7e075e87ad306d48e3fa35606f09091a27ecec6c Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Thu, 13 Oct 2022 14:23:06 +0800 Subject: [PATCH 142/169] update: add kuaishou ui automation case --- .../uitest/ios_kuaishou_follow_live_test.go | 60 +++++++ .../uitest/ios_kuaishou_follow_live_test.json | 154 ++++++++++++++++++ .../uitest/ios_kuaishou_follow_live_test.yaml | 84 ++++++++++ 3 files changed, 298 insertions(+) create mode 100644 examples/uitest/ios_kuaishou_follow_live_test.go create mode 100644 examples/uitest/ios_kuaishou_follow_live_test.json create mode 100644 examples/uitest/ios_kuaishou_follow_live_test.yaml diff --git a/examples/uitest/ios_kuaishou_follow_live_test.go b/examples/uitest/ios_kuaishou_follow_live_test.go new file mode 100644 index 00000000..cfbac1a0 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.go @@ -0,0 +1,60 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSKuaiShouLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("直播_快手_关注天窗_ios"). + WithVariables(map[string]interface{}{ + "device": "${ENV(UDID)}", + "ups": "大哥,王者", + }). + SetIOS(hrp.WithUDID("$device"), hrp.WithLogOn(true), hrp.WithWDAPort(8100), hrp.WithWDAMjpegPort(9100)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动快手"). + IOS(). + AppTerminate("com.jiangjia.gif"). + AppLaunch("com.jiangjia.gif"). + Home(). + SwipeToTapApp("快手", hrp.WithMaxRetryTimes(5)).Sleep(10). + Validate(). + AssertOCRExists("精选", "进入快手失败"), + hrp.NewStep("点击首页"). + IOS(). + TapByOCR("首页", hrp.WithIndex(-1)).Sleep(10), + hrp.NewStep("点击发现页"). + IOS(). + TapByOCR("发现", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("点击关注页"). + IOS(). + TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), + hrp.NewStep("点击直播标签,进入直播间"). + IOS(). + SwipeToTapTexts([]string{}, hrp.WithFunction("${split_by_comma($ups)}"), hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(60). + Validate(). + AssertOCRExists("说点什么", "进入直播间失败"), + hrp.NewStep("下滑进入下一个直播间"). + IOS(). + Swipe(0.9, 0.7, 0.9, 0.3, hrp.WithIdentifier("slide_in_live")).Sleep(60), + }, + } + + if err := testCase.Dump2JSON("ios_kuaishou_follow_live_test.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("ios_kuaishou_follow_live_test.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/ios_kuaishou_follow_live_test.json b/examples/uitest/ios_kuaishou_follow_live_test.json new file mode 100644 index 00000000..fd016081 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.json @@ -0,0 +1,154 @@ +{ + "config": { + "name": "直播_快手_关注天窗_ios", + "variables": { + "device": "${ENV(UDID)}", + "ups": "大哥,王者" + }, + "ios": [ + { + "udid": "$device", + "port": 8100, + "mjpeg_port": 9100, + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动快手", + "ios": { + "actions": [ + { + "method": "app_terminate", + "params": "com.jiangjia.gif" + }, + { + "method": "app_launch", + "params": "com.jiangjia.gif" + }, + { + "method": "home" + }, + { + "method": "swipe_to_tap_app", + "params": "快手", + "max_retry_times": 5 + }, + { + "method": "sleep", + "params": 10 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "精选", + "msg": "进入快手失败" + } + ] + }, + { + "name": "点击首页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "首页", + "index": -1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击发现页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "发现", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击关注页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "关注", + "index": 1 + }, + { + "method": "sleep", + "params": 10 + } + ] + } + }, + { + "name": "点击直播标签,进入直播间", + "ios": { + "actions": [ + { + "method": "swipe_to_tap_texts", + "params": [], + "identifier": "click_live", + "direction": [ + 0.6, + 0.2, + 0.2, + 0.2 + ], + "function": "${split_by_comma($ups)}" + }, + { + "method": "sleep", + "params": 60 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "说点什么", + "msg": "进入直播间失败" + } + ] + }, + { + "name": "下滑进入下一个直播间", + "ios": { + "actions": [ + { + "method": "swipe", + "params": [ + 0.9, + 0.7, + 0.9, + 0.3 + ], + "identifier": "slide_in_live" + }, + { + "method": "sleep", + "params": 60 + } + ] + } + } + ] +} diff --git a/examples/uitest/ios_kuaishou_follow_live_test.yaml b/examples/uitest/ios_kuaishou_follow_live_test.yaml new file mode 100644 index 00000000..c81d75b8 --- /dev/null +++ b/examples/uitest/ios_kuaishou_follow_live_test.yaml @@ -0,0 +1,84 @@ +config: + name: 直播_快手_关注天窗_ios + variables: + device: ${ENV(UDID)} + ups: 大哥,王者 + ios: + - udid: $device + port: 8100 + mjpeg_port: 9100 + log_on: true +teststeps: + - name: 启动快手 + ios: + actions: + - method: app_terminate + params: com.jiangjia.gif + - method: app_launch + params: com.jiangjia.gif + - method: home + - method: swipe_to_tap_app + params: 快手 + max_retry_times: 5 + - method: sleep + params: 10 + validate: + - check: ui_ocr + assert: exists + expect: 精选 + msg: 进入快手失败 + - name: 点击首页 + ios: + actions: + - method: tap_ocr + params: 首页 + index: -1 + - method: sleep + params: 10 + - name: 点击发现页 + ios: + actions: + - method: tap_ocr + params: 发现 + index: 1 + - method: sleep + params: 10 + - name: 点击关注页 + ios: + actions: + - method: tap_ocr + params: 关注 + index: 1 + - method: sleep + params: 10 + - name: 点击直播标签,进入直播间 + ios: + actions: + - method: swipe_to_tap_texts + params: [] + identifier: click_live + direction: + - 0.6 + - 0.2 + - 0.2 + - 0.2 + function: ${split_by_comma($ups)} + - method: sleep + params: 60 + validate: + - check: ui_ocr + assert: exists + expect: 说点什么 + msg: 进入直播间失败 + - name: 下滑进入下一个直播间 + ios: + actions: + - method: swipe + params: + - 0.9 + - 0.7 + - 0.9 + - 0.3 + identifier: slide_in_live + - method: sleep + params: 60 From 465e0554a2cfeca4666d484d48aa9e49a41c0c94 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Thu, 13 Oct 2022 16:39:17 +0800 Subject: [PATCH 143/169] fix: unittest --- .../uitest/ios_kuaishou_follow_live_test.go | 4 ++-- .../uitest/ios_kuaishou_follow_live_test.json | 7 +++---- .../uitest/ios_kuaishou_follow_live_test.yaml | 5 ++--- hrp/pkg/uixt/ext.go | 21 +++++++------------ hrp/step.go | 1 - hrp/step_mobile_ui.go | 9 +------- 6 files changed, 16 insertions(+), 31 deletions(-) diff --git a/examples/uitest/ios_kuaishou_follow_live_test.go b/examples/uitest/ios_kuaishou_follow_live_test.go index cfbac1a0..e16b5be1 100644 --- a/examples/uitest/ios_kuaishou_follow_live_test.go +++ b/examples/uitest/ios_kuaishou_follow_live_test.go @@ -13,7 +13,7 @@ func TestIOSKuaiShouLive(t *testing.T) { Config: hrp.NewConfig("直播_快手_关注天窗_ios"). WithVariables(map[string]interface{}{ "device": "${ENV(UDID)}", - "ups": "大哥,王者", + "ups": "${ENV(LIVEUPLIST)}", }). SetIOS(hrp.WithUDID("$device"), hrp.WithLogOn(true), hrp.WithWDAPort(8100), hrp.WithWDAMjpegPort(9100)), TestSteps: []hrp.IStep{ @@ -36,7 +36,7 @@ func TestIOSKuaiShouLive(t *testing.T) { TapByOCR("关注", hrp.WithIndex(1)).Sleep(10), hrp.NewStep("点击直播标签,进入直播间"). IOS(). - SwipeToTapTexts([]string{}, hrp.WithFunction("${split_by_comma($ups)}"), hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(60). + SwipeToTapTexts("${split_by_comma($ups)}", hrp.WithCustomDirection(0.6, 0.2, 0.2, 0.2), hrp.WithIdentifier("click_live")).Sleep(60). Validate(). AssertOCRExists("说点什么", "进入直播间失败"), hrp.NewStep("下滑进入下一个直播间"). diff --git a/examples/uitest/ios_kuaishou_follow_live_test.json b/examples/uitest/ios_kuaishou_follow_live_test.json index fd016081..3817b167 100644 --- a/examples/uitest/ios_kuaishou_follow_live_test.json +++ b/examples/uitest/ios_kuaishou_follow_live_test.json @@ -3,7 +3,7 @@ "name": "直播_快手_关注天窗_ios", "variables": { "device": "${ENV(UDID)}", - "ups": "大哥,王者" + "ups": "${ENV(LIVEUPLIST)}" }, "ios": [ { @@ -104,15 +104,14 @@ "actions": [ { "method": "swipe_to_tap_texts", - "params": [], + "params": "${split_by_comma($ups)}", "identifier": "click_live", "direction": [ 0.6, 0.2, 0.2, 0.2 - ], - "function": "${split_by_comma($ups)}" + ] }, { "method": "sleep", diff --git a/examples/uitest/ios_kuaishou_follow_live_test.yaml b/examples/uitest/ios_kuaishou_follow_live_test.yaml index c81d75b8..484f0815 100644 --- a/examples/uitest/ios_kuaishou_follow_live_test.yaml +++ b/examples/uitest/ios_kuaishou_follow_live_test.yaml @@ -2,7 +2,7 @@ config: name: 直播_快手_关注天窗_ios variables: device: ${ENV(UDID)} - ups: 大哥,王者 + ups: ${ENV(LIVEUPLIST)} ios: - udid: $device port: 8100 @@ -55,14 +55,13 @@ teststeps: ios: actions: - method: swipe_to_tap_texts - params: [] + params: ${split_by_comma($ups)} identifier: click_live direction: - 0.6 - 0.2 - 0.2 - 0.2 - function: ${split_by_comma($ups)} - method: sleep params: 60 validate: diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 276b6946..e38ffc56 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -67,7 +67,6 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app - Function string `json:"function,omitempty" yaml:"function,omitempty"` // used to replace params Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found @@ -90,13 +89,6 @@ func WithIndex(index int) ActionOption { } } -// WithFunction replaces params -func WithFunction(function string) ActionOption { - return func(o *MobileAction) { - o.Function = function - } -} - // WithDirection inputs direction (up, down, left, right) func WithDirection(direction string) ActionOption { return func(o *MobileAction) { @@ -437,14 +429,17 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { ACTION_SwipeToTapText, action.Params) case ACTION_SwipeToTapTexts: if texts, ok := action.Params.([]interface{}); ok { + var textList []string + for _, t := range texts { + textList = append(textList, t.(string)) + } + action.Params = textList + } + if texts, ok := action.Params.([]string); ok { var point PointF findText := func(d *DriverExt) error { var err error - var ts []string - for _, t := range texts { - ts = append(ts, t.(string)) - } - points, err := d.GetTextXYs(ts) + points, err := d.GetTextXYs(texts) if err != nil { return err } diff --git a/hrp/step.go b/hrp/step.go index f10fc84f..ac7481f1 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -31,7 +31,6 @@ var ( WithDescription = uixt.WithDescription WithDirection = uixt.WithDirection WithCustomDirection = uixt.WithCustomDirection - WithFunction = uixt.WithFunction ) var ( diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index fa924edb..8b16a9ce 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -263,7 +263,7 @@ func (s *StepMobile) SwipeToTapText(text string, options ...uixt.ActionOption) * return &StepMobile{step: s.step} } -func (s *StepMobile) SwipeToTapTexts(texts []string, options ...uixt.ActionOption) *StepMobile { +func (s *StepMobile) SwipeToTapTexts(texts interface{}, options ...uixt.ActionOption) *StepMobile { action := uixt.MobileAction{ Method: uixt.ACTION_SwipeToTapTexts, Params: texts, @@ -618,13 +618,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { - if action.Function != "" { - parsedParams, err := parser.ParseString(action.Function, stepVariables) - if err != nil { - return stepResult, err - } - action.Params = parsedParams - } if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } From 5233e3b2f7cdc03cc7b57de663332dd695798916 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Thu, 13 Oct 2022 18:08:05 +0800 Subject: [PATCH 144/169] fix: failed to recording android adb log --- hrp/pkg/uixt/android_device.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index 80bfb5c4..d2dad7e3 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -63,6 +63,9 @@ func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceO if dev.Port != 0 { deviceOptions = append(deviceOptions, WithAdbPort(dev.Port)) } + if dev.LogOn { + deviceOptions = append(deviceOptions, WithAdbLogOn(true)) + } return } From 463b77d67ca106cb2828b06bda1994af6ce1fb85 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 13 Oct 2022 20:46:54 +0800 Subject: [PATCH 145/169] fix: typo for WithDismissAlertButtonSelector --- hrp/pkg/uixt/ios_device.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 6ed13d3f..b02a34db 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -153,7 +153,7 @@ func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) { deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.AcceptAlertButtonSelector)) } if dev.DismissAlertButtonSelector != "" { - deviceOptions = append(deviceOptions, WithAcceptAlertButtonSelector(dev.DismissAlertButtonSelector)) + deviceOptions = append(deviceOptions, WithDismissAlertButtonSelector(dev.DismissAlertButtonSelector)) } return } From a72690baad2bf026cc4e90e356274a409b87f0cf Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Fri, 14 Oct 2022 13:15:14 +0800 Subject: [PATCH 146/169] fix: failed to load customDirection from ui automation json script --- hrp/pkg/uixt/swipe.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index 5cb8bffa..8f2ba9f9 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -3,6 +3,7 @@ package uixt import ( "fmt" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/rs/zerolog/log" ) @@ -86,6 +87,14 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3]); err != nil { log.Error().Err(err).Msgf("swipe %s failed", d) } + } else if d, ok := direction.([]interface{}); ok { + sx, _ := builtin.Interface2Float64(d[0]) + sy, _ := builtin.Interface2Float64(d[1]) + ex, _ := builtin.Interface2Float64(d[2]) + ey, _ := builtin.Interface2Float64(d[3]) + if err := dExt.SwipeRelative(sx, sy, ex, ey); err != nil { + log.Error().Err(err).Msgf("swipe (%v, %v) to (%v, %v) failed", sx, sy, ex, ey) + } } } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) From e936d8afb2835c075d512869280d476f5281988c Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Fri, 14 Oct 2022 15:28:24 +0800 Subject: [PATCH 147/169] fix: failed to filter out point(0,0) --- hrp/pkg/uixt/ext.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index e38ffc56..564bad59 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -444,7 +444,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return err } for _, point = range points { - if point != (PointF{}) { + if point != (PointF{X: 0, Y: 0}) { return nil } } From 2f73012cfc8853d54afffcc73c169d8c1bfbd6f4 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 16 Oct 2022 09:32:56 +0800 Subject: [PATCH 148/169] change: add ocr response logID --- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/ocr_vedem.go | 18 +++++++++++++++++- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 64efb591..d9e3391e 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10131119 \ No newline at end of file +v4.3.0-beta-10160931 \ No newline at end of file diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index f160c274..82d142dc 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -71,7 +71,11 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) resp, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("http reqeust OCR server error: %v", err) + var logID string + if resp != nil { + logID = getLogID(resp.Header) + } + return nil, fmt.Errorf("http reqeust veDEM OCR server error: %v, logID: %s", err, logID) } defer resp.Body.Close() @@ -93,6 +97,18 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return ocrResult.OCRResult, nil } +func getLogID(header http.Header) string { + if len(header) == 0 { + return "" + } + + logID, ok := header["X-Tt-Logid"] + if !ok || len(logID) == 0 { + return "" + } + return logID[0] +} + func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) { if len(index) == 0 { index = []int{0} // index not specified diff --git a/httprunner/__init__.py b/httprunner/__init__.py index fad89a02..f2195b32 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10131119" +__version__ = "v4.3.0-beta-10160931" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 216a12d5..921a4bc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10131119" +version = "v4.3.0-beta-10160931" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 47020148a84d5b9b3904b8a9553fd7da905182c2 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 16 Oct 2022 12:09:33 +0800 Subject: [PATCH 149/169] refactor: move exec to myexec --- hrp/build.go | 16 +- hrp/cmd/convert.go | 4 +- hrp/cmd/pytest.go | 4 +- hrp/internal/builtin/utils.go | 127 +------------- hrp/internal/dial/curl.go | 8 +- hrp/internal/dial/traceroute_unix.go | 4 +- hrp/internal/dial/traceroute_windows.go | 3 +- hrp/internal/myexec/cmd.go | 162 ++++++++++++++++++ .../utils_unix.go => myexec/cmd_uixt.go} | 56 ++---- .../cmd_windows.go} | 48 ++---- hrp/internal/pytest/main.go | 4 +- hrp/internal/scaffold/main.go | 6 +- hrp/internal/wiki/main.go | 5 +- hrp/pkg/convert/converter.go | 3 +- hrp/pkg/convert/from_gotest.go | 4 +- hrp/pkg/uixt/android_device.go | 21 ++- hrp/plugin.go | 4 +- 17 files changed, 237 insertions(+), 242 deletions(-) create mode 100644 hrp/internal/myexec/cmd.go rename hrp/internal/{builtin/utils_unix.go => myexec/cmd_uixt.go} (51%) rename hrp/internal/{builtin/utils_windows.go => myexec/cmd_windows.go} (77%) diff --git a/hrp/build.go b/hrp/build.go index 9cecccfb..bf950fde 100644 --- a/hrp/build.go +++ b/hrp/build.go @@ -6,7 +6,6 @@ import ( "fmt" "html/template" "os" - "os/exec" "path/filepath" "regexp" "strings" @@ -16,6 +15,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -128,26 +128,26 @@ func (pt *pluginTemplate) generateGo(output string) error { } // check go sdk in tempDir - if err := builtin.ExecCommandInDir(exec.Command("go", "version"), pluginDir); err != nil { + if err := myexec.RunCommand("go", "version"); err != nil { return errors.Wrap(err, "go sdk not installed") } if !builtin.IsFilePathExists(filepath.Join(pluginDir, "go.mod")) { // create go mod - if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "init", "main"), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "init", "main"), pluginDir); err != nil { return err } // download plugin dependency // funplugin version should be locked funplugin := fmt.Sprintf("github.com/httprunner/funplugin@%s", shared.Version) - if err := builtin.ExecCommandInDir(exec.Command("go", "get", funplugin), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "get", funplugin), pluginDir); err != nil { return errors.Wrap(err, "go get funplugin failed") } } // add missing and remove unused modules - if err := builtin.ExecCommandInDir(exec.Command("go", "mod", "tidy"), pluginDir); err != nil { + if err := myexec.ExecCommandInDir(myexec.Command("go", "mod", "tidy"), pluginDir); err != nil { return errors.Wrap(err, "go mod tidy failed") } @@ -161,8 +161,8 @@ func (pt *pluginTemplate) generateGo(output string) error { outputPath, _ := filepath.Abs(output) // build go plugin to debugtalk.bin - cmd := exec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path)) - if err := builtin.ExecCommandInDir(cmd, pluginDir); err != nil { + cmd := myexec.Command("go", "build", "-o", outputPath, PluginGoSourceGenFile, filepath.Base(pt.path)) + if err := myexec.ExecCommandInDir(cmd, pluginDir); err != nil { return errors.Wrap(err, "go build plugin failed") } log.Info().Str("output", outputPath).Str("plugin", pt.path).Msg("build go plugin successfully") @@ -194,7 +194,7 @@ func buildGo(path string, output string) error { func buildPy(path string, output string) error { log.Info().Str("path", path).Str("output", output).Msg("start to prepare python plugin") // check the syntax of debugtalk.py - err := builtin.ExecPython3Command("py_compile", path) + err := myexec.ExecPython3Command("py_compile", path) if err != nil { return errors.Wrap(err, "python plugin syntax invalid") } diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 96d3e985..51f25a9e 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -7,7 +7,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/version" "github.com/httprunner/httprunner/v4/hrp/pkg/convert" ) @@ -63,7 +63,7 @@ func convertRun(cmd *cobra.Command, args []string) error { packages := []string{ fmt.Sprintf("httprunner==%s", version.VERSION), } - _, err := builtin.EnsurePython3Venv(venv, packages...) + _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err).Msg("python3 venv is not ready") return err diff --git a/hrp/cmd/pytest.go b/hrp/cmd/pytest.go index 2e1933ad..0ebfaa69 100644 --- a/hrp/cmd/pytest.go +++ b/hrp/cmd/pytest.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/pytest" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -23,7 +23,7 @@ var pytestCmd = &cobra.Command{ packages := []string{ fmt.Sprintf("httprunner==%s", version.VERSION), } - _, err := builtin.EnsurePython3Venv(venv, packages...) + _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err).Msg("python3 venv is not ready") return err diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 18b60a93..c79939c8 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -12,7 +12,6 @@ import ( "math" "math/rand" "os" - "os/exec" "path/filepath" "reflect" "strconv" @@ -23,7 +22,6 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" - "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -94,117 +92,6 @@ func FormatResponse(raw interface{}) interface{} { return formattedResponse } -var python3Executable string = "python3" // system default python3 - -// EnsurePython3Venv ensures python3 venv with specified packages -// venv should be directory path of target venv -func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) { - // priority: specified > $HOME/.hrp/venv - if venv == "" { - home, err := os.UserHomeDir() - if err != nil { - return "", errors.Wrap(err, "get user home dir failed") - } - venv = filepath.Join(home, ".hrp", "venv") - } - python3, err = ensurePython3Venv(venv, packages...) - if err != nil { - return "", errors.Wrap(err, "prepare python3 venv failed") - } - python3Executable = python3 - log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path") - return python3, nil -} - -func ExecPython3Command(cmdName string, args ...string) error { - args = append([]string{"-m", cmdName}, args...) - return ExecCommand(python3Executable, args...) -} - -func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error { - out, err := exec.Command( - python3, "-c", fmt.Sprintf("import %s; print(%s.__version__)", pkgName, pkgName), - ).Output() - if err != nil { - return fmt.Errorf("python package %s not found", pkgName) - } - - // do not check version if pkgVersion is empty - if pkgVersion == "" { - log.Info().Str("name", pkgName).Msg("python package is ready") - return nil - } - - // check package version equality - version := strings.TrimSpace(string(out)) - if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") { - return fmt.Errorf("python package %s version %s not matched, please upgrade to %s", - pkgName, version, pkgVersion) - } - - log.Info().Str("name", pkgName).Str("version", pkgVersion).Msg("python package is ready") - return nil -} - -func InstallPythonPackage(python3 string, pkg string) (err error) { - var pkgName, pkgVersion string - if strings.Contains(pkg, "==") { - // funppy==0.5.0 - pkgInfo := strings.Split(pkg, "==") - pkgName = pkgInfo[0] - pkgVersion = pkgInfo[1] - } else { - // funppy - pkgName = pkg - } - - // check if package installed and version matched - err = AssertPythonPackage(python3, pkgName, pkgVersion) - if err == nil { - return nil - } - - // check if pip available - err = ExecCommand(python3, "-m", "pip", "--version") - if err != nil { - log.Warn().Msg("pip is not available") - return errors.Wrap(err, "pip is not available") - } - - log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package") - - // install package - pypiIndexURL := env.PYPI_INDEX_URL - if pypiIndexURL == "" { - pypiIndexURL = "https://pypi.org/simple" // default - } - err = ExecCommand(python3, "-m", "pip", "install", "--upgrade", pkg, - "--index-url", pypiIndexURL, - "--quiet", "--disable-pip-version-check") - if err != nil { - return errors.Wrap(err, "pip install package failed") - } - - return AssertPythonPackage(python3, pkgName, pkgVersion) -} - -func ExecCommandInDir(cmd *exec.Cmd, dir string) error { - log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command") - cmd.Dir = dir - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err - } - - return nil -} - func CreateFolder(folderPath string) error { log.Info().Str("path", folderPath).Msg("create folder") err := os.MkdirAll(folderPath, os.ModePerm) @@ -289,19 +176,19 @@ func GetRandomNumber(min, max int) int { } func Interface2Float64(i interface{}) (float64, error) { - switch i.(type) { + switch v := i.(type) { case int: - return float64(i.(int)), nil + return float64(v), nil case int32: - return float64(i.(int32)), nil + return float64(v), nil case int64: - return float64(i.(int64)), nil + return float64(v), nil case float32: - return float64(i.(float32)), nil + return float64(v), nil case float64: - return i.(float64), nil + return v, nil case string: - intVar, err := strconv.Atoi(i.(string)) + intVar, err := strconv.Atoi(v) if err != nil { return 0, err } diff --git a/hrp/internal/dial/curl.go b/hrp/internal/dial/curl.go index 8cd3436a..f3940799 100644 --- a/hrp/internal/dial/curl.go +++ b/hrp/internal/dial/curl.go @@ -4,13 +4,13 @@ import ( "bytes" "fmt" "os" - "os/exec" "path/filepath" "time" "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) const ( @@ -46,7 +46,7 @@ func DoCurl(args []string) (err error) { } }() - cmd := exec.Command("curl", args...) + cmd := myexec.Command("curl", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -60,11 +60,11 @@ func DoCurl(args []string) (err error) { return } if stdout.String() != "" { - fmt.Printf(stdout.String()) + fmt.Println(stdout.String()) curlResult.Result = stdout.String() curlResult.ResultType = normalResult } else if stderr.String() != "" { - fmt.Printf(stderr.String()) + fmt.Println(stderr.String()) curlResult.ErrorMsg = stderr.String() curlResult.ResultType = errorResult } diff --git a/hrp/internal/dial/traceroute_unix.go b/hrp/internal/dial/traceroute_unix.go index d9d309c8..52742666 100644 --- a/hrp/internal/dial/traceroute_unix.go +++ b/hrp/internal/dial/traceroute_unix.go @@ -7,7 +7,6 @@ import ( "fmt" "net/url" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -18,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) var ( @@ -51,7 +51,7 @@ func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err erro traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] } - cmd := exec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL), + cmd := myexec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL), "-q", strconv.Itoa(traceRouteOptions.Queries), traceRouteTarget) stdout, _ := cmd.StdoutPipe() diff --git a/hrp/internal/dial/traceroute_windows.go b/hrp/internal/dial/traceroute_windows.go index f8ce4743..7aa0581c 100644 --- a/hrp/internal/dial/traceroute_windows.go +++ b/hrp/internal/dial/traceroute_windows.go @@ -7,7 +7,6 @@ import ( "fmt" "net/url" "os" - "os/exec" "path/filepath" "regexp" "strconv" @@ -49,7 +48,7 @@ func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err erro traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] } - cmd := exec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget) + cmd := myexec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget) stdout, _ := cmd.StdoutPipe() startT := time.Now() diff --git a/hrp/internal/myexec/cmd.go b/hrp/internal/myexec/cmd.go new file mode 100644 index 00000000..3876008e --- /dev/null +++ b/hrp/internal/myexec/cmd.go @@ -0,0 +1,162 @@ +package myexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/env" +) + +var python3Executable string = "python3" // system default python3 + +func isPython3(python string) bool { + out, err := Command(python, "--version").Output() + if err != nil { + return false + } + if strings.HasPrefix(string(out), "Python 3") { + return true + } + return false +} + +// EnsurePython3Venv ensures python3 venv with specified packages +// venv should be directory path of target venv +func EnsurePython3Venv(venv string, packages ...string) (python3 string, err error) { + // priority: specified > $HOME/.hrp/venv + if venv == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "get user home dir failed") + } + venv = filepath.Join(home, ".hrp", "venv") + } + python3, err = ensurePython3Venv(venv, packages...) + if err != nil { + return "", errors.Wrap(err, "prepare python3 venv failed") + } + python3Executable = python3 + log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path") + return python3, nil +} + +func ExecPython3Command(cmdName string, args ...string) error { + args = append([]string{"-m", cmdName}, args...) + return RunCommand(python3Executable, args...) +} + +func AssertPythonPackage(python3 string, pkgName, pkgVersion string) error { + out, err := exec.Command( + python3, "-c", fmt.Sprintf("import %s; print(%s.__version__)", pkgName, pkgName), + ).Output() + if err != nil { + return fmt.Errorf("python package %s not found", pkgName) + } + + // do not check version if pkgVersion is empty + if pkgVersion == "" { + log.Info().Str("name", pkgName).Msg("python package is ready") + return nil + } + + // check package version equality + version := strings.TrimSpace(string(out)) + if strings.TrimLeft(version, "v") != strings.TrimLeft(pkgVersion, "v") { + return fmt.Errorf("python package %s version %s not matched, please upgrade to %s", + pkgName, version, pkgVersion) + } + + log.Info().Str("name", pkgName).Str("version", pkgVersion).Msg("python package is ready") + return nil +} + +func InstallPythonPackage(python3 string, pkg string) (err error) { + var pkgName, pkgVersion string + if strings.Contains(pkg, "==") { + // funppy==0.5.0 + pkgInfo := strings.Split(pkg, "==") + pkgName = pkgInfo[0] + pkgVersion = pkgInfo[1] + } else { + // funppy + pkgName = pkg + } + + // check if package installed and version matched + err = AssertPythonPackage(python3, pkgName, pkgVersion) + if err == nil { + return nil + } + + // check if pip available + err = RunCommand(python3, "-m", "pip", "--version") + if err != nil { + log.Warn().Msg("pip is not available") + return errors.Wrap(err, "pip is not available") + } + + log.Info().Str("pkgName", pkgName).Str("pkgVersion", pkgVersion).Msg("installing python package") + + // install package + pypiIndexURL := env.PYPI_INDEX_URL + if pypiIndexURL == "" { + pypiIndexURL = "https://pypi.org/simple" // default + } + err = RunCommand(python3, "-m", "pip", "install", "--upgrade", pkg, + "--index-url", pypiIndexURL, + "--quiet", "--disable-pip-version-check") + if err != nil { + return errors.Wrap(err, "pip install package failed") + } + + return AssertPythonPackage(python3, pkgName, pkgVersion) +} + +func RunCommand(cmdName string, args ...string) error { + cmd := Command(cmdName, args...) + log.Info().Str("cmd", cmd.String()).Msg("exec command") + + // add cmd dir path to $PATH + if cmdDir := filepath.Dir(cmdName); cmdDir != "" { + path := fmt.Sprintf("%s:%s", cmdDir, env.PATH) + if err := os.Setenv("PATH", path); err != nil { + log.Error().Err(err).Msg("set env $PATH failed") + return err + } + } + + // print output with colors + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + log.Error().Err(err).Msg("exec command failed") + return err + } + + return nil +} + +func ExecCommandInDir(cmd *exec.Cmd, dir string) error { + log.Info().Str("cmd", cmd.String()).Str("dir", dir).Msg("exec command") + cmd.Dir = dir + + // print output with colors + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + log.Error().Err(err).Msg("exec command failed") + return err + } + + return nil +} diff --git a/hrp/internal/builtin/utils_unix.go b/hrp/internal/myexec/cmd_uixt.go similarity index 51% rename from hrp/internal/builtin/utils_unix.go rename to hrp/internal/myexec/cmd_uixt.go index 89b6a4f5..39c64085 100644 --- a/hrp/internal/builtin/utils_unix.go +++ b/hrp/internal/myexec/cmd_uixt.go @@ -1,31 +1,18 @@ //go:build darwin || linux -package builtin +package myexec import ( "fmt" "os" "os/exec" "path/filepath" - "strings" + "syscall" "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "github.com/httprunner/httprunner/v4/hrp/internal/env" ) -func isPython3(python string) bool { - out, err := exec.Command(python, "--version").Output() - if err != nil { - return false - } - if strings.HasPrefix(string(out), "Python 3") { - return true - } - return false -} - func getPython3Executable(venvDir string) string { return filepath.Join(venvDir, "bin", "python3") } @@ -42,20 +29,20 @@ func ensurePython3Venv(venv string, packages ...string) (python3 string, err err if !isPython3(python3) { // python3 venv not available, create one // check if system python3 is available - if err := ExecCommand("python3", "--version"); err != nil { + if err := RunCommand("python3", "--version"); err != nil { return "", errors.Wrap(err, "python3 not found") } // check if .venv exists if _, err := os.Stat(venv); err == nil { // .venv exists, remove first - if err := ExecCommand("rm", "-rf", venv); err != nil { + if err := RunCommand("rm", "-rf", venv); err != nil { return "", errors.Wrap(err, "remove existed venv failed") } } // create python3 .venv - if err := ExecCommand("python3", "-m", "venv", venv); err != nil { + if err := RunCommand("python3", "-m", "venv", venv); err != nil { return "", errors.Wrap(err, "create python3 venv failed") } } @@ -72,32 +59,13 @@ func ensurePython3Venv(venv string, packages ...string) (python3 string, err err } func Command(name string, arg ...string) *exec.Cmd { - args := strings.Join(arg, " ") - return exec.Command("bash", "-c", name, args) + cmd := exec.Command(name, arg...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + return cmd } -func ExecCommand(cmdName string, args ...string) error { - cmd := exec.Command(cmdName, args...) - log.Info().Str("cmd", cmd.String()).Msg("exec command") - - // add cmd dir path to $PATH - if cmdDir := filepath.Dir(cmdName); cmdDir != "" { - PATH := fmt.Sprintf("%s:%s", cmdDir, env.PATH) - if err := os.Setenv("PATH", PATH); err != nil { - log.Error().Err(err).Msg("set env $PATH failed") - return err - } - } - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err - } - - return nil +func KillProcessesByGpid(cmd *exec.Cmd) error { + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) } diff --git a/hrp/internal/builtin/utils_windows.go b/hrp/internal/myexec/cmd_windows.go similarity index 77% rename from hrp/internal/builtin/utils_windows.go rename to hrp/internal/myexec/cmd_windows.go index 52aea058..c70cac84 100644 --- a/hrp/internal/builtin/utils_windows.go +++ b/hrp/internal/myexec/cmd_windows.go @@ -1,15 +1,11 @@ //go:build windows -package builtin +package myexec import ( - "fmt" - "os" "os/exec" - "path/filepath" - "strings" + "syscall" - "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -20,17 +16,6 @@ func init() { } } -func isPython3(python string) bool { - out, err := exec.Command("cmd", "/c", python, "--version").Output() - if err != nil { - return false - } - if strings.HasPrefix(string(out), "Python 3") { - return true - } - return false -} - func getPython3Executable(venvDir string) string { python := filepath.Join(venvDir, "Scripts", "python3.exe") if isPython3(python) { @@ -101,26 +86,17 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err } func Command(name string, arg ...string) *exec.Cmd { - args := strings.Join(arg, " ") - return exec.Command("cmd", "/c", name, args) -} - -func ExecCommand(cmdName string, args ...string) error { // "cmd /c" carries out the command specified by string and then stops // refer: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd - cmdStr := fmt.Sprintf("%s %s", cmdName, strings.Join(args, " ")) - cmd := exec.Command("cmd", "/c", cmdStr) - log.Info().Str("cmd", cmd.String()).Msg("exec command") - - // print output with colors - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - log.Error().Err(err).Msg("exec command failed") - return err + cmd := exec.Command("cmd.exe") + cmd.SysProcAttr = &syscall.SysProcAttr{ + CmdLine: strings.Join(append([]string{"/c", name}, arg...), " "), + HideWindow: true, } - - return nil + return cmd +} + +func KillProcessesByGpid(cmd *exec.Cmd) error { + killCmd := Command("taskkill", "/T", "/F", "/PID ", strconv.Itoa(cmd.Process.Pid)) + return killCmd.Run() } diff --git a/hrp/internal/pytest/main.go b/hrp/internal/pytest/main.go index dde5f0ae..c9bec74b 100644 --- a/hrp/internal/pytest/main.go +++ b/hrp/internal/pytest/main.go @@ -1,7 +1,7 @@ package pytest import ( - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -12,5 +12,5 @@ func RunPytest(args []string) error { }) args = append([]string{"run"}, args...) - return builtin.ExecPython3Command("httprunner", args...) + return myexec.ExecPython3Command("httprunner", args...) } diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index f27a035a..28b2c851 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -4,7 +4,6 @@ import ( "embed" "fmt" "os" - "os/exec" "path/filepath" "time" @@ -14,6 +13,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -177,7 +177,7 @@ func CreateScaffold(projectName string, pluginType PluginType, venv string, forc func createGoPlugin(projectName string) error { log.Info().Msg("start to create hashicorp go plugin") // check go sdk - if err := builtin.ExecCommandInDir(exec.Command("go", "version"), projectName); err != nil { + if err := myexec.RunCommand("go", "version"); err != nil { return errors.Wrap(err, "go sdk not installed") } @@ -209,7 +209,7 @@ func createPythonPlugin(projectName, venv string) error { fmt.Sprintf("funppy==%s", fungo.Version), fmt.Sprintf("httprunner==%s", version.VERSION), } - _, err = builtin.EnsurePython3Venv(venv, packages...) + _, err = myexec.EnsurePython3Venv(venv, packages...) if err != nil { return err } diff --git a/hrp/internal/wiki/main.go b/hrp/internal/wiki/main.go index 108edca6..2557e499 100644 --- a/hrp/internal/wiki/main.go +++ b/hrp/internal/wiki/main.go @@ -1,10 +1,9 @@ package wiki import ( - "os/exec" - "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -14,5 +13,5 @@ func OpenWiki() error { Action: "hrp wiki", }) log.Info().Msgf("%s https://httprunner.com", openCmd) - return exec.Command(openCmd, "https://httprunner.com").Run() + return myexec.RunCommand(openCmd, "https://httprunner.com") } diff --git a/hrp/pkg/convert/converter.go b/hrp/pkg/convert/converter.go index 4c69296e..cf537869 100644 --- a/hrp/pkg/convert/converter.go +++ b/hrp/pkg/convert/converter.go @@ -13,6 +13,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -212,7 +213,7 @@ func (c *TCaseConverter) ToPyTest() (string, error) { } args := append([]string{"make"}, jsonPath) - err = builtin.ExecPython3Command("httprunner", args...) + err = myexec.ExecPython3Command("httprunner", args...) if err != nil { return "", err } diff --git a/hrp/pkg/convert/from_gotest.go b/hrp/pkg/convert/from_gotest.go index 04897c76..eecde5a5 100644 --- a/hrp/pkg/convert/from_gotest.go +++ b/hrp/pkg/convert/from_gotest.go @@ -7,7 +7,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) func convert2GoTestScripts(paths ...string) error { @@ -48,7 +48,7 @@ func convert2GoTestScripts(paths ...string) error { } // format pytest scripts with black - return builtin.ExecPython3Command("black", pytestPaths...) + return myexec.ExecPython3Command("black", pytestPaths...) } //go:embed testcase.tmpl diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index d2dad7e3..8fbd713b 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -8,14 +8,13 @@ import ( "os/exec" "reflect" "strings" - "syscall" "github.com/electricbubble/gadb" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/json" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) var ( @@ -250,20 +249,25 @@ func (l *DeviceLogcat) Errors() (err error) { func (l *DeviceLogcat) CatchLogcat() (err error) { if l.cmd != nil { - err = fmt.Errorf("logcat already start") + log.Warn().Msg("logcat already start") + return nil + } + + // clear logcat + if err = myexec.RunCommand("adb", "-s", l.serial, "logcat", "-c"); err != nil { return } - cmdLine := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial) - l.cmd = builtin.Command(cmdLine) + + // start logcat + l.cmd = myexec.Command("adb", "-s", l.serial, "logcat", "-v", "time", "-s", "iesqaMonitor:V") l.cmd.Stderr = l.logBuffer l.cmd.Stdout = l.logBuffer - l.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err = l.cmd.Start(); err != nil { return } go func() { <-l.stopping - if e := syscall.Kill(-l.cmd.Process.Pid, syscall.SIGKILL); e != nil { + if e := myexec.KillProcessesByGpid(l.cmd); e != nil { l.errs = append(l.errs, fmt.Errorf("kill logcat process err:%v", e)) } l.done <- struct{}{} @@ -273,8 +277,7 @@ func (l *DeviceLogcat) CatchLogcat() (err error) { func (l *DeviceLogcat) BufferedLogcat() (err error) { // -d: dump the current buffered logcat result and exits - cmdLine := fmt.Sprintf("adb -s %s logcat -d", l.serial) - cmd := builtin.Command(cmdLine) + cmd := myexec.Command("adb", "-s", l.serial, "logcat", "-d") cmd.Stdout = l.logBuffer cmd.Stderr = l.logBuffer if err = cmd.Run(); err != nil { diff --git a/hrp/plugin.go b/hrp/plugin.go index fad9aa7a..f28558a2 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -11,7 +11,7 @@ import ( "github.com/httprunner/funplugin/fungo" "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -59,7 +59,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er packages := []string{ fmt.Sprintf("funppy==%s", fungo.Version), } - python3, err := builtin.EnsurePython3Venv(venv, packages...) + python3, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { log.Error().Err(err). Interface("packages", packages). From efbc89a8dfccfc17bf766ef693bcff2561ce4e6d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 16 Oct 2022 14:22:53 +0800 Subject: [PATCH 150/169] fix: unittests --- converter.py | 176 ----------- demo/.debugtalk_gen.py | 23 -- demo/.env | 3 - demo/.gitignore | 14 - demo/debugtalk.py | 62 ---- demo/har/.keep | 0 demo/proj.json | 5 - demo/testcases/demo.json | 176 ----------- demo/testcases/ref_testcase.yml | 33 -- demo/testcases/requests.json | 136 -------- demo/testcases/requests.yml | 62 ---- docs/CHANGELOG.md | 12 +- .../data/postman/postman_collection_test.json | 81 ----- .../postman/postman_collection_test_test.py | 55 ---- google_style.py | 297 ------------------ hrp/internal/myexec/cmd_windows.go | 9 +- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/demo/main_test.go | 2 + httprunner/__init__.py | 3 +- httprunner/step_android.py | 136 -------- httprunner/step_android_test.py | 42 --- main.go | 59 ---- pyproject.toml | 2 +- 23 files changed, 21 insertions(+), 1369 deletions(-) delete mode 100644 converter.py delete mode 100644 demo/.debugtalk_gen.py delete mode 100644 demo/.env delete mode 100644 demo/.gitignore delete mode 100644 demo/debugtalk.py delete mode 100644 demo/har/.keep delete mode 100644 demo/proj.json delete mode 100644 demo/testcases/demo.json delete mode 100644 demo/testcases/ref_testcase.yml delete mode 100644 demo/testcases/requests.json delete mode 100644 demo/testcases/requests.yml delete mode 100644 examples/data/postman/postman_collection_test.json delete mode 100644 examples/data/postman/postman_collection_test_test.py delete mode 100644 google_style.py delete mode 100644 httprunner/step_android.py delete mode 100644 httprunner/step_android_test.py delete mode 100644 main.go diff --git a/converter.py b/converter.py deleted file mode 100644 index a54bc377..00000000 --- a/converter.py +++ /dev/null @@ -1,176 +0,0 @@ -import io -import json -import pprint -import re -import textwrap -from typing import Any - -from mitmproxy import http -from mitmproxy.utils import strutils - - -def curl_command(flow: http.HTTPFlow) -> str: - data = "curl " - - request = flow.request.copy() - request.decode(strict=False) - - for k, v in request.headers.items(multi=True): - data += "-H '%s:%s' " % (k, v) - - if request.method != "GET": - data += "-X %s " % request.method - - data += "'%s'" % request.url - - if request.content: - data += " --data-binary '%s'" % strutils.bytes_to_escaped_str( - request.content, escape_single_quotes=True - ) - - return data - - -def python_arg(arg: str, val: Any) -> str: - if not val: - return "" - if arg: - arg += "=" - arg_str = "{}{},\n".format(arg, pprint.pformat(val, 79 - len(arg))) - return textwrap.indent(arg_str, " " * 4) - - -def python_code(flow: http.HTTPFlow): - code = io.StringIO() - - def writearg(arg, val): - code.write(python_arg(arg, val)) - - code.write("import requests\n") - code.write("\n") - if flow.request.method.lower() in ("get", "post", "put", "head", "delete", "patch"): - code.write("response = requests.{}(\n".format(flow.request.method.lower())) - else: - code.write("response = requests.request(\n") - writearg("", flow.request.method) - url_without_query = flow.request.url.split("?", 1)[0] - writearg("", url_without_query) - - writearg("params", list(flow.request.query.fields)) - - headers = flow.request.headers.copy() - # requests adds those by default. - for x in (":authority", "host", "content-length"): - headers.pop(x, None) - writearg("headers", dict(headers)) - try: - if "json" not in flow.request.headers.get("content-type", ""): - raise ValueError() - writearg("json", json.loads(flow.request.text)) - except ValueError: - writearg("data", flow.request.content) - - code.seek(code.tell() - 2) # remove last comma - code.write("\n)\n") - code.write("\n") - code.write("print(response.text)") - - return code.getvalue() - - -def locust_code(flow): - code = textwrap.dedent( - """ - from locust import HttpLocust, TaskSet, task - class UserBehavior(TaskSet): - def on_start(self): - ''' on_start is called when a Locust start before any task is scheduled ''' - self.{name}() - @task() - def {name}(self): - url = self.locust.host + '{path}' - {headers}{params}{data} - self.response = self.client.request( - method='{method}', - url=url,{args} - ) - ### Additional tasks can go here ### - class WebsiteUser(HttpLocust): - task_set = UserBehavior - min_wait = 1000 - max_wait = 3000 -""" - ).strip() - - name = re.sub("\W|^(?=\d)", "_", flow.request.path.strip("/").split("?", 1)[0]) - if not name: - new_name = "_".join([str(flow.request.host), str(flow.request.timestamp_start)]) - name = re.sub("\W|^(?=\d)", "_", new_name) - - path_without_query = flow.request.path.split("?")[0] - - args = "" - headers = "" - - def conv(x): - return strutils.bytes_to_escaped_str(x, escape_single_quotes=True) - - if flow.request.headers: - lines = [ - (conv(k), conv(v)) - for k, v in flow.request.headers.fields - if conv(k).lower() not in [":authority", "host", "cookie"] - ] - lines = [" '%s': '%s',\n" % (k, v) for k, v in lines] - headers += "\n headers = {\n%s }\n" % "".join(lines) - args += "\n headers=headers," - - params = "" - if flow.request.query: - lines = [ - " %s: %s,\n" % (repr(k), repr(v)) - for k, v in flow.request.query.collect() - ] - params = "\n params = {\n%s }\n" % "".join(lines) - args += "\n params=params," - - data = "" - if flow.request.content: - data = "\n data = '''%s'''\n" % conv(flow.request.content) - args += "\n data=data," - - code = code.format( - name=name, - path=path_without_query, - headers=headers, - params=params, - data=data, - method=flow.request.method, - args=args, - ) - - return code - - -def locust_task(flow): - code = locust_code(flow) - start_task = len(code.split("@task")[0]) - 4 - end_task = -19 - len(code.split("### Additional")[1]) - task_code = code[start_task:end_task] - - return task_code - - -def url(flow): - return flow.request.url - - -EXPORTERS = [ - ("content", "c", None), - ("headers+content", "h", None), - ("url", "u", url), - ("as curl command", "r", curl_command), - ("as python code", "p", python_code), - ("as locust code", "l", locust_code), - ("as locust task", "t", locust_task), -] diff --git a/demo/.debugtalk_gen.py b/demo/.debugtalk_gen.py deleted file mode 100644 index a57fef72..00000000 --- a/demo/.debugtalk_gen.py +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: Generated By hrp v4.1.4, DO NOT EDIT! - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from debugtalk import * - - -if __name__ == "__main__": - import funppy - funppy.register("get_user_agent", get_user_agent) - funppy.register("sleep", sleep) - funppy.register("sum", sum) - funppy.register("sum_ints", sum_ints) - funppy.register("sum_two_int", sum_two_int) - funppy.register("sum_two_string", sum_two_string) - funppy.register("sum_strings", sum_strings) - funppy.register("concatenate", concatenate) - funppy.register("setup_hook_example", setup_hook_example) - funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() diff --git a/demo/.env b/demo/.env deleted file mode 100644 index 59ecc742..00000000 --- a/demo/.env +++ /dev/null @@ -1,3 +0,0 @@ -base_url=https://postman-echo.com -USERNAME=debugtalk -PASSWORD=123456 \ No newline at end of file diff --git a/demo/.gitignore b/demo/.gitignore deleted file mode 100644 index 4c8cb60c..00000000 --- a/demo/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -reports/ -*.so -.vscode/ -.idea/ -.DS_Store -output/ -__pycache__/ -*.pyc -.python-version -logs/ - -# plugin -debugtalk.bin -debugtalk.so diff --git a/demo/debugtalk.py b/demo/debugtalk.py deleted file mode 100644 index 334a46c7..00000000 --- a/demo/debugtalk.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -import time -from typing import List - - -# commented out function will be filtered -# def get_headers(): -# return {"User-Agent": "hrp"} - - -def get_user_agent(): - return "hrp/funppy" - - -def sleep(n_secs): - time.sleep(n_secs) - - -def sum(*args): - result = 0 - for arg in args: - result += arg - return result - - -def sum_ints(*args: List[int]) -> int: - result = 0 - for arg in args: - result += arg - return result - - -def sum_two_int(a: int, b: int) -> int: - return a + b - - -def sum_two_string(a: str, b: str) -> str: - return a + b - - -def sum_strings(*args: List[str]) -> str: - result = "" - for arg in args: - result += arg - return result - - -def concatenate(*args: List[str]) -> str: - result = "" - for arg in args: - result += str(arg) - return result - - -def setup_hook_example(name): - logging.warning("setup_hook_example") - return f"setup_hook_example: {name}" - - -def teardown_hook_example(name): - logging.warning("teardown_hook_example") - return f"teardown_hook_example: {name}" diff --git a/demo/har/.keep b/demo/har/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/demo/proj.json b/demo/proj.json deleted file mode 100644 index 08277e5f..00000000 --- a/demo/proj.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "project_name": "demo", - "create_time": "2022-06-23T11:15:39.635136+08:00", - "hrp_version": "v4.1.4" -} diff --git a/demo/testcases/demo.json b/demo/testcases/demo.json deleted file mode 100644 index 8e50e2aa..00000000 --- a/demo/testcases/demo.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "config": { - "name": "demo with complex mechanisms", - "base_url": "https://postman-echo.com", - "variables": { - "a": "${sum(10, 2.3)}", - "b": 3.45, - "n": "${sum_ints(1, 2, 2)}", - "varFoo1": "${gen_random_string($n)}", - "varFoo2": "${max($a, $b)}" - } - }, - "teststeps": [ - { - "name": "transaction 1 start", - "transaction": { - "name": "tran1", - "type": "start" - } - }, - { - "name": "get with params", - "request": { - "method": "GET", - "url": "/get", - "params": { - "foo1": "$varFoo1", - "foo2": "$varFoo2" - }, - "headers": { - "User-Agent": "HttpRunnerPlus" - } - }, - "variables": { - "b": 34.5, - "n": 3, - "name": "get with params", - "varFoo2": "${max($a, $b)}" - }, - "setup_hooks": [ - "${setup_hook_example($name)}" - ], - "teardown_hooks": [ - "${teardown_hook_example($name)}" - ], - "extract": { - "varFoo1": "body.args.foo1" - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "check response status code" - }, - { - "check": "headers.\"Content-Type\"", - "assert": "startswith", - "expect": "application/json" - }, - { - "check": "body.args.foo1", - "assert": "length_equals", - "expect": 5, - "msg": "check args foo1" - }, - { - "check": "$varFoo1", - "assert": "length_equals", - "expect": 5, - "msg": "check args foo1" - }, - { - "check": "body.args.foo2", - "assert": "equals", - "expect": "34.5", - "msg": "check args foo2" - } - ] - }, - { - "name": "transaction 1 end", - "transaction": { - "name": "tran1", - "type": "end" - } - }, - { - "name": "post json data", - "request": { - "method": "POST", - "url": "/post", - "body": { - "foo1": "$varFoo1", - "foo2": "${max($a, $b)}" - } - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "check status code" - }, - { - "check": "body.json.foo1", - "assert": "length_equals", - "expect": 5, - "msg": "check args foo1" - }, - { - "check": "body.json.foo2", - "assert": "equals", - "expect": 12.3, - "msg": "check args foo2" - } - ] - }, - { - "name": "post form data", - "request": { - "method": "POST", - "url": "/post", - "headers": { - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" - }, - "body": { - "foo1": "$varFoo1", - "foo2": "${max($a, $b)}", - "time": "${get_timestamp()}" - } - }, - "extract": { - "varTime": "body.form.time" - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "check status code" - }, - { - "check": "body.form.foo1", - "assert": "length_equals", - "expect": 5, - "msg": "check args foo1" - }, - { - "check": "body.form.foo2", - "assert": "equals", - "expect": "12.3", - "msg": "check args foo2" - } - ] - }, - { - "name": "get with timestamp", - "request": { - "method": "GET", - "url": "/get", - "params": { - "time": "$varTime" - } - }, - "validate": [ - { - "check": "body.args.time", - "assert": "length_equals", - "expect": 13, - "msg": "check extracted var timestamp" - } - ] - } - ] -} diff --git a/demo/testcases/ref_testcase.yml b/demo/testcases/ref_testcase.yml deleted file mode 100644 index c0932124..00000000 --- a/demo/testcases/ref_testcase.yml +++ /dev/null @@ -1,33 +0,0 @@ -config: - name: "request methods testcase: reference testcase" - variables: - foo1: testsuite_config_bar1 - expect_foo1: testsuite_config_bar1 - expect_foo2: config_bar2 - base_url: "https://postman-echo.com" - verify: False - -teststeps: -- - name: request with functions - variables: - foo1: testcase_ref_bar1 - expect_foo1: testcase_ref_bar1 - testcase: testcases/requests.yml - export: - - foo3 -- - name: post form data - variables: - foo1: bar1 - request: - method: POST - url: /post - headers: - User-Agent: ${get_user_agent()} - Content-Type: "application/x-www-form-urlencoded" - body: "foo1=$foo1&foo2=$foo3" - validate: - - eq: ["status_code", 200] - - eq: ["body.form.foo1", "bar1"] - - eq: ["body.form.foo2", "bar21"] \ No newline at end of file diff --git a/demo/testcases/requests.json b/demo/testcases/requests.json deleted file mode 100644 index 4c01ec74..00000000 --- a/demo/testcases/requests.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "config": { - "name": "request methods testcase with functions", - "variables": { - "foo1": "config_bar1", - "foo2": "config_bar2", - "expect_foo1": "config_bar1", - "expect_foo2": "config_bar2" - }, - "headers": { - "User-Agent": "${get_user_agent()}" - }, - "base_url": "https://postman-echo.com", - "verify": false, - "export": [ - "foo3" - ] - }, - "teststeps": [ - { - "name": "get with params", - "variables": { - "foo1": "${ENV(USERNAME)}", - "foo2": "bar21", - "sum_v": "${sum_two_int(10000000, 20000000)}" - }, - "request": { - "method": "GET", - "url": "/get", - "params": { - "foo1": "$foo1", - "foo2": "$foo2", - "sum_v": "$sum_v" - } - }, - "extract": { - "foo3": "body.args.foo2" - }, - "validate": [ - { - "check": "status_code", - "assert": "equal", - "expect": 200, - "msg": "check status_code" - }, - { - "check": "body.args.foo1", - "assert": "equal", - "expect": "debugtalk", - "msg": "check body.args.foo1" - }, - { - "check": "body.args.sum_v", - "assert": "equal", - "expect": "30000000", - "msg": "check body.args.sum_v" - }, - { - "check": "body.args.foo2", - "assert": "equal", - "expect": "bar21", - "msg": "check body.args.foo2" - } - ] - }, - { - "name": "post raw text", - "variables": { - "foo1": "bar12", - "foo3": "bar32" - }, - "request": { - "method": "POST", - "url": "/post", - "headers": { - "Content-Type": "text/plain" - }, - "body": "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." - }, - "validate": [ - { - "check": "status_code", - "assert": "equal", - "expect": 200, - "msg": "check status_code" - }, - { - "check": "body.data", - "assert": "equal", - "expect": "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.", - "msg": "check body.data" - } - ] - }, - { - "name": "post form data", - "variables": { - "foo2": "bar23" - }, - "request": { - "method": "POST", - "url": "/post", - "headers": { - "Content-Type": "application/x-www-form-urlencoded" - }, - "body": "foo1=$foo1&foo2=$foo2&foo3=$foo3" - }, - "validate": [ - { - "check": "status_code", - "assert": "equal", - "expect": 200, - "msg": "check status_code" - }, - { - "check": "body.form.foo1", - "assert": "equal", - "expect": "$expect_foo1", - "msg": "check body.form.foo1" - }, - { - "check": "body.form.foo2", - "assert": "equal", - "expect": "bar23", - "msg": "check body.form.foo2" - }, - { - "check": "body.form.foo3", - "assert": "equal", - "expect": "bar21", - "msg": "check body.form.foo3" - } - ] - } - ] -} \ No newline at end of file diff --git a/demo/testcases/requests.yml b/demo/testcases/requests.yml deleted file mode 100644 index 5922ab12..00000000 --- a/demo/testcases/requests.yml +++ /dev/null @@ -1,62 +0,0 @@ -config: - name: "request methods testcase with functions" - variables: - foo1: config_bar1 - foo2: config_bar2 - expect_foo1: config_bar1 - expect_foo2: config_bar2 - headers: - User-Agent: ${get_user_agent()} - verify: False - export: ["foo3"] - -teststeps: -- - name: get with params - variables: - foo1: ${ENV(USERNAME)} - foo2: bar21 - sum_v: "${sum_two_int(10000000, 20000000)}" - request: - method: GET - url: $base_url/get - params: - foo1: $foo1 - foo2: $foo2 - sum_v: $sum_v - extract: - foo3: "body.args.foo2" - validate: - - eq: ["status_code", 200] - - eq: ["body.args.foo1", "debugtalk"] - - eq: ["body.args.sum_v", "30000000"] - - eq: ["body.args.foo2", "bar21"] -- - name: post raw text - variables: - foo1: "bar12" - foo3: "bar32" - request: - method: POST - url: $base_url/post - headers: - Content-Type: "text/plain" - body: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." - validate: - - eq: ["status_code", 200] - - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] -- - name: post form data - variables: - foo2: bar23 - request: - method: POST - url: $base_url/post - headers: - Content-Type: "application/x-www-form-urlencoded" - body: "foo1=$foo1&foo2=$foo2&foo3=$foo3" - validate: - - eq: ["status_code", 200] - - eq: ["body.form.foo1", "$expect_foo1"] - - eq: ["body.form.foo2", "bar23"] - - eq: ["body.form.foo3", "bar21"] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3a22afa5..432d07d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,11 @@ # Release History -## v4.3.0 (2022-09-01) +## v4.3.0 (2022-10-16) -- feat: support iOS UI automation with WebDriverAgent +- feat: support iOS UI automation with [WebDriverAgent] +- feat support Android UI automation with [uiautomator2] +- feat: integrage ios device management with [gidevice] +- refactor: make boomer/uixt/httpstat as sub package ## v4.2.1 (2022-09-01) @@ -673,4 +676,7 @@ reference: [v2-changelog] [locust]: https://locust.io/ [black]: https://github.com/psf/black [loguru]: https://github.com/Delgan/loguru -[v2-changelog]: https://github.com/httprunner/httprunner/blob/v2/docs/CHANGELOG.md \ No newline at end of file +[v2-changelog]: https://github.com/httprunner/httprunner/blob/v2/docs/CHANGELOG.md +[WebDriverAgent]: https://github.com/appium/WebDriverAgent +[uiautomator2]: https://github.com/appium/appium-uiautomator2-server +[gidevice]: https://github.com/electricbubble/gidevice diff --git a/examples/data/postman/postman_collection_test.json b/examples/data/postman/postman_collection_test.json deleted file mode 100644 index 8592d93b..00000000 --- a/examples/data/postman/postman_collection_test.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "config": { - "name": "postman collection demo" - }, - "teststeps": [ - { - "name": "folder1 - folder2 - Get with params", - "request": { - "method": "GET", - "url": "https://postman-echo.com/get", - "params": { - "k1": "v1", - "k2": "v2" - } - } - }, - { - "name": "folder3 - Post form-data", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "upload": { - "intro_key": "intro.txt", - "k1": "v1", - "k2": "v2", - "logo_key": "logo.jpeg" - } - } - }, - { - "name": "folder3 - Post x-www-form-urlencoded", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "headers": { - "Content-Type": "application/x-www-form-urlencoded" - }, - "body": { - "k1": "v1", - "k2": "v2" - } - } - }, - { - "name": "folder3 - Post raw json", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "headers": { - "Content-Type": "application/json" - }, - "body": { - "k1": "v1", - "k2": "v2" - } - } - }, - { - "name": "folder3 - Post raw text", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "headers": { - "Content-Type": "text/plain" - }, - "body": "have a nice day" - } - }, - { - "name": "Get request headers", - "request": { - "method": "GET", - "url": "https://postman-echo.com/headers", - "headers": { - "Connection": "close", - "User-Agent": "HttpRunner" - } - } - } - ] -} diff --git a/examples/data/postman/postman_collection_test_test.py b/examples/data/postman/postman_collection_test_test.py deleted file mode 100644 index 4a813aa8..00000000 --- a/examples/data/postman/postman_collection_test_test.py +++ /dev/null @@ -1,55 +0,0 @@ -# NOTE: Generated By HttpRunner v4.1.4 -# FROM: postman/postman_collection_test.json -from httprunner import HttpRunner, Config, Step, RunRequest - - -class TestCasePostmanCollectionTest(HttpRunner): - - config = Config("postman collection demo") - - teststeps = [ - Step( - RunRequest("folder1 - folder2 - Get with params") - .get("https://postman-echo.com/get") - .with_params(**{"k1": "v1", "k2": "v2"}) - ), - Step( - RunRequest("folder3 - Post form-data") - .post("https://postman-echo.com/post") - .upload( - **{ - "intro_key": "intro.txt", - "k1": "v1", - "k2": "v2", - "logo_key": "logo.jpeg", - } - ) - ), - Step( - RunRequest("folder3 - Post x-www-form-urlencoded") - .post("https://postman-echo.com/post") - .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"}) - .with_data({"k1": "v1", "k2": "v2"}) - ), - Step( - RunRequest("folder3 - Post raw json") - .post("https://postman-echo.com/post") - .with_headers(**{"Content-Type": "application/json"}) - .with_json({"k1": "v1", "k2": "v2"}) - ), - Step( - RunRequest("folder3 - Post raw text") - .post("https://postman-echo.com/post") - .with_headers(**{"Content-Type": "text/plain"}) - .with_data("have a nice day") - ), - Step( - RunRequest("Get request headers") - .get("https://postman-echo.com/headers") - .with_headers(**{"Connection": "close", "User-Agent": "HttpRunner"}) - ), - ] - - -if __name__ == "__main__": - TestCasePostmanCollectionTest().test_start() diff --git a/google_style.py b/google_style.py deleted file mode 100644 index 9ba4349f..00000000 --- a/google_style.py +++ /dev/null @@ -1,297 +0,0 @@ -# -*- coding: utf-8 -*- -"""Example Google style docstrings. - -This module demonstrates documentation as specified by the `Google Python -Style Guide`_. Docstrings may extend over multiple lines. Sections are created -with a section header and a colon followed by a block of indented text. - -Example: - Examples can be given using either the ``Example`` or ``Examples`` - sections. Sections support any reStructuredText formatting, including - literal blocks:: - - $ python example_google.py - -Section breaks are created by resuming unindented text. Section breaks -are also implicitly created anytime a new section starts. - -Attributes: - module_level_variable1 (int): Module level variables may be documented in - either the ``Attributes`` section of the module docstring, or in an - inline docstring immediately following the variable. - - Either form is acceptable, but the two should not be mixed. Choose - one convention to document module level variables and be consistent - with it. - -Todo: - * For module TODOs - * You have to also use ``sphinx.ext.todo`` extension - -.. _Google Python Style Guide: - http://google.github.io/styleguide/pyguide.html - -""" - -module_level_variable1 = 12345 - -module_level_variable2 = 98765 -"""int: Module level variable documented inline. - -The docstring may span multiple lines. The type may optionally be specified -on the first line, separated by a colon. -""" - - -def function_with_types_in_docstring(param1, param2): - """Example function with types documented in the docstring. - - `PEP 484`_ type annotations are supported. If attribute, parameter, and - return types are annotated according to `PEP 484`_, they do not need to be - included in the docstring: - - Args: - param1 (int): The first parameter. - param2 (str): The second parameter. - - Returns: - bool: The return value. True for success, False otherwise. - - .. _PEP 484: - https://www.python.org/dev/peps/pep-0484/ - - """ - - -def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: - """Example function with PEP 484 type annotations. - - Args: - param1: The first parameter. - param2: The second parameter. - - Returns: - The return value. True for success, False otherwise. - - """ - - -def module_level_function(param1, param2=None, *args, **kwargs): - """This is an example of a module level function. - - Function parameters should be documented in the ``Args`` section. The name - of each parameter is required. The type and description of each parameter - is optional, but should be included if not obvious. - - If \*args or \*\*kwargs are accepted, - they should be listed as ``*args`` and ``**kwargs``. - - The format for a parameter is:: - - name (type): description - The description may span multiple lines. Following - lines should be indented. The "(type)" is optional. - - Multiple paragraphs are supported in parameter - descriptions. - - Args: - param1 (int): The first parameter. - param2 (:obj:`str`, optional): The second parameter. Defaults to None. - Second line of description should be indented. - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - - Returns: - bool: True if successful, False otherwise. - - The return type is optional and may be specified at the beginning of - the ``Returns`` section followed by a colon. - - The ``Returns`` section may span multiple lines and paragraphs. - Following lines should be indented to match the first line. - - The ``Returns`` section supports any reStructuredText formatting, - including literal blocks:: - - { - 'param1': param1, - 'param2': param2 - } - - Raises: - AttributeError: The ``Raises`` section is a list of all exceptions - that are relevant to the interface. - ValueError: If `param2` is equal to `param1`. - - """ - if param1 == param2: - raise ValueError("param1 may not be equal to param2") - return True - - -def example_generator(n): - """Generators have a ``Yields`` section instead of a ``Returns`` section. - - Args: - n (int): The upper limit of the range to generate, from 0 to `n` - 1. - - Yields: - int: The next number in the range of 0 to `n` - 1. - - Examples: - Examples should be written in doctest format, and should illustrate how - to use the function. - - >>> print([i for i in example_generator(4)]) - [0, 1, 2, 3] - - """ - for i in range(n): - yield i - - -class ExampleError(Exception): - """Exceptions are documented in the same way as classes. - - The __init__ method may be documented in either the class level - docstring, or as a docstring on the __init__ method itself. - - Either form is acceptable, but the two should not be mixed. Choose one - convention to document the __init__ method and be consistent with it. - - Note: - Do not include the `self` parameter in the ``Args`` section. - - Args: - msg (str): Human readable string describing the exception. - code (:obj:`int`, optional): Error code. - - Attributes: - msg (str): Human readable string describing the exception. - code (int): Exception error code. - - """ - - def __init__(self, msg, code): - self.msg = msg - self.code = code - - -class ExampleClass(object): - """The summary line for a class docstring should fit on one line. - - If the class has public attributes, they may be documented here - in an ``Attributes`` section and follow the same formatting as a - function's ``Args`` section. Alternatively, attributes may be documented - inline with the attribute's declaration (see __init__ method below). - - Properties created with the ``@property`` decorator should be documented - in the property's getter method. - - Attributes: - attr1 (str): Description of `attr1`. - attr2 (:obj:`int`, optional): Description of `attr2`. - - """ - - def __init__(self, param1, param2, param3): - """Example of docstring on the __init__ method. - - The __init__ method may be documented in either the class level - docstring, or as a docstring on the __init__ method itself. - - Either form is acceptable, but the two should not be mixed. Choose one - convention to document the __init__ method and be consistent with it. - - Note: - Do not include the `self` parameter in the ``Args`` section. - - Args: - param1 (str): Description of `param1`. - param2 (:obj:`int`, optional): Description of `param2`. Multiple - lines are supported. - param3 (:obj:`list` of :obj:`str`): Description of `param3`. - - """ - self.attr1 = param1 - self.attr2 = param2 - self.attr3 = param3 #: Doc comment *inline* with attribute - - #: list of str: Doc comment *before* attribute, with type specified - self.attr4 = ["attr4"] - - self.attr5 = None - """str: Docstring *after* attribute, with type specified.""" - - @property - def readonly_property(self): - """str: Properties should be documented in their getter method.""" - return "readonly_property" - - @property - def readwrite_property(self): - """:obj:`list` of :obj:`str`: Properties with both a getter and setter - should only be documented in their getter method. - - If the setter method contains notable behavior, it should be - mentioned here. - """ - return ["readwrite_property"] - - @readwrite_property.setter - def readwrite_property(self, value): - value - - def example_method(self, param1, param2): - """Class methods are similar to regular functions. - - Note: - Do not include the `self` parameter in the ``Args`` section. - - Args: - param1: The first parameter. - param2: The second parameter. - - Returns: - True if successful, False otherwise. - - """ - return True - - def __special__(self): - """By default special members with docstrings are not included. - - Special members are any methods or attributes that start with and - end with a double underscore. Any special member with a docstring - will be included in the output, if - ``napoleon_include_special_with_doc`` is set to True. - - This behavior can be enabled by changing the following setting in - Sphinx's conf.py:: - - napoleon_include_special_with_doc = True - - """ - pass - - def __special_without_docstring__(self): - pass - - def _private(self): - """By default private members are not included. - - Private members are any methods or attributes that start with an - underscore and are *not* special. By default they are not included - in the output. - - This behavior can be changed such that private members *are* included - by changing the following setting in Sphinx's conf.py:: - - napoleon_include_private_with_doc = True - - """ - pass - - def _private_without_docstring(self): - pass diff --git a/hrp/internal/myexec/cmd_windows.go b/hrp/internal/myexec/cmd_windows.go index c70cac84..6844058f 100644 --- a/hrp/internal/myexec/cmd_windows.go +++ b/hrp/internal/myexec/cmd_windows.go @@ -3,9 +3,12 @@ package myexec import ( + "os" "os/exec" + "path/filepath" "syscall" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -48,7 +51,7 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err // check if .venv exists if _, err := os.Stat(venvDir); err == nil { // .venv exists, remove first - if err := ExecCommand("del", "/q", venvDir); err != nil { + if err := RunCommand("del", "/q", venvDir); err != nil { return "", errors.Wrap(err, "remove existed venv failed") } } @@ -56,10 +59,10 @@ func ensurePython3Venv(venvDir string, packages ...string) (python3 string, err // create python3 .venv // notice: --symlinks should be specified for windows // https://github.com/actions/virtual-environments/issues/2690 - if err := ExecCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil { + if err := RunCommand(systemPython, "-m", "venv", "--symlinks", venvDir); err != nil { // fix: failed to symlink on Windows log.Warn().Msg("failed to create python3 .venv by using --symlinks, try to use --copies") - if err := ExecCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil { + if err := RunCommand(systemPython, "-m", "venv", "--copies", venvDir); err != nil { return "", errors.Wrap(err, "create python3 venv failed") } } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index d9e3391e..a28eeaba 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10160931 \ No newline at end of file +v4.3.0-beta-10161439 \ No newline at end of file diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 044f3f92..6dac96d2 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package demo import ( diff --git a/httprunner/__init__.py b/httprunner/__init__.py index f2195b32..4592b08c 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10160931" +__version__ = "v4.3.0-beta-10161439" __description__ = "One-stop solution for HTTP(S) testing." @@ -19,6 +19,7 @@ from httprunner.step_thrift_request import ( StepThriftRequestValidation, ) + __all__ = [ "__version__", "__description__", diff --git a/httprunner/step_android.py b/httprunner/step_android.py deleted file mode 100644 index 6a485cc8..00000000 --- a/httprunner/step_android.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import Text - -from loguru import logger -import uiautomator2 as u2 - -from httprunner.models import IStep, StepResult, TStep, TStepAndroidUI -from httprunner.runner import HttpRunner - - -def run_android_ui(runner: HttpRunner, step: TStep) -> StepResult: - step_result = StepResult( - name=step.name, - step_type="android_ui", - success=False, - ) - logger.info(f"run android ui action: {step.android.method}, param: {step.android.param}") - - return step_result - - -class StepAndroidControl(IStep): - - def __init__(self, step: TStep): - self.__step = step - - def start_app(self, package_name: Text) -> "StepAndroidControl": - return self - - def stop_app(self, package_name: Text) -> "StepAndroidControl": - return self - - def start_watcher(self) -> "StepAndroidControl": - return self - - def stop_watcher(self) -> "StepAndroidControl": - return self - - def start_camera(self) -> "StepAndroidControl": - return self - - def stop_camera(self) -> "StepAndroidControl": - return self - - def start_record(self) -> "StepAndroidControl": - return self - - def stop_record(self) -> "StepAndroidControl": - return self - - def struct(self) -> TStep: - return self.__step - - def name(self) -> Text: - return self.__step.name - - def type(self) -> Text: - return "android-control" - - def run(self, runner: HttpRunner): - return run_android_ui(runner, self.__step) - - -class StepAndroidUI(IStep): - - def __init__(self, step: TStep): - self.__step = step - - def press_back(self) -> "StepAndroidUI": - self.__step.android.method = "press" - self.__step.android.param = "back" - return self - - def press_home(self) -> "StepAndroidUI": - self.__step.android.method = "press" - self.__step.android.param = "home" - return self - - def sleep(self, time: int) -> "StepAndroidUI": - self.__step.android.method = "sleep" - self.__step.android.param = time - return self - - def swipe_up(self) -> "StepAndroidUI": - self.__step.android.method = "swipe" - self.__step.android.param = [0.25, 0.5, 0.75, 0.5] - return self - - def swipe_down(self) -> "StepAndroidUI": - self.__step.android.method = "swipe" - self.__step.android.param = [0.75, 0.5, 0.25, 0.5] - return self - - def swipe_left(self) -> "StepAndroidUI": - self.__step.android.method = "swipe" - self.__step.android.param = [0.5, 0.75, 0.5, 0.25] - return self - - def swipe_right(self) -> "StepAndroidUI": - self.__step.android.method = "swipe" - self.__step.android.param = [0.5, 0.25, 0.5, 0.75] - return self - - def swipe(self, from_x: float, from_y: float, to_x: float, to_y: float) -> "StepAndroidUI": - self.__step.android.method = "swipe" - self.__step.android.param = [from_x, from_y, to_x, to_y] - return self - - def click(self, text: Text) -> "StepAndroidUI": - self.__step.android.method = "click" - self.__step.android.param = text - return self - - def struct(self) -> TStep: - return self.__step - - def name(self) -> Text: - return self.__step.name - - def type(self) -> Text: - return "android-ui" - - def run(self, runner: HttpRunner): - return run_android_ui(runner, self.__step) - - -class RunAndroidUI(object): - - def __init__(self, name: Text): - self.__step = TStep(name=name) - self.__step.android = TStepAndroidUI() - - def control(self) -> StepAndroidControl: - return StepAndroidControl(self.__step) - - def ui(self) -> StepAndroidUI: - return StepAndroidUI(self.__step) diff --git a/httprunner/step_android_test.py b/httprunner/step_android_test.py deleted file mode 100644 index fe484a89..00000000 --- a/httprunner/step_android_test.py +++ /dev/null @@ -1,42 +0,0 @@ -from httprunner import HttpRunner, Config, Step, RunAndroidUI - - -class TestCaseAndroidDemo(HttpRunner): - - config = ( - Config("demo for android UI test") - .variables( - **{ - "foo1": "config_bar1", - "foo2": "config_bar2", - "expect_foo1": "config_bar1", - "expect_foo2": "config_bar2", - } - ) - .android() - .serial("xxx") - .package_name("xxx") - .install_apk("xxx") - ) - - teststeps = [ - # Step( - # RunAndroidUI("start app").control().start_app("com.ss.android.ugc.aweme") - # ), - Step( - RunAndroidUI("back home").ui().press_home() - ), - Step( - RunAndroidUI("back home").control().start_app() - ), - Step( - RunAndroidUI("swipe up").ui().swipe_up() - ), - Step( - RunAndroidUI("swipe up").ui().swipe_up() - ), - ] - - -if __name__ == "__main__": - TestCaseAndroidDemo().test_start() diff --git a/main.go b/main.go deleted file mode 100644 index 5d81a823..00000000 --- a/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "log" - "os" - "strings" - - "github.com/electricbubble/gadb" -) - -func main() { - adbClient, err := gadb.NewClient() - checkErr(err, "fail to connect adb server") - - devices, err := adbClient.DeviceList() - checkErr(err) - - if len(devices) == 0 { - log.Fatalln("list of devices is empty") - } - - dev := devices[0] - - userHomeDir, _ := os.UserHomeDir() - apk, err := os.Open(userHomeDir + "/Desktop/xuexi_android_10002068.apk") - checkErr(err) - - log.Println("starting to push apk") - - remotePath := "/data/local/tmp/xuexi_android_10002068.apk" - err = dev.PushFile(apk, remotePath) - checkErr(err, "adb push") - - log.Println("push completed") - - log.Println("starting to install apk") - - shellOutput, err := dev.RunShellCommand("pm install", remotePath) - checkErr(err, "pm install") - if !strings.Contains(shellOutput, "Success") { - log.Fatalln("fail to install: ", shellOutput) - } - - log.Println("install completed") - -} - -func checkErr(err error, msg ...string) { - if err == nil { - return - } - - var output string - if len(msg) != 0 { - output = msg[0] + " " - } - output += err.Error() - log.Fatalln(output) -} diff --git a/pyproject.toml b/pyproject.toml index 921a4bc8..c3339ecf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10160931" +version = "v4.3.0-beta-10161439" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 3ccdb00fc29e3cf191a743fa4a1489f82ab9bd23 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 16 Oct 2022 20:21:20 +0800 Subject: [PATCH 151/169] fix: avoid data race for httpstat --- hrp/cmd/convert.go | 2 +- hrp/cmd/pytest.go | 2 +- hrp/internal/dial/traceroute_windows.go | 1 + hrp/internal/myexec/cmd_windows.go | 3 +++ hrp/internal/scaffold/main.go | 2 +- hrp/internal/version/init.go | 3 +++ hrp/pkg/httpstat/main.go | 8 ++++++++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 51f25a9e..5ec00480 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -61,7 +61,7 @@ func convertRun(cmd *cobra.Command, args []string) error { outputType = convert.OutputTypePyTest packages := []string{ - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { diff --git a/hrp/cmd/pytest.go b/hrp/cmd/pytest.go index 0ebfaa69..55538e78 100644 --- a/hrp/cmd/pytest.go +++ b/hrp/cmd/pytest.go @@ -21,7 +21,7 @@ var pytestCmd = &cobra.Command{ DisableFlagParsing: true, // allow to pass any args to pytest RunE: func(cmd *cobra.Command, args []string) error { packages := []string{ - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } _, err := myexec.EnsurePython3Venv(venv, packages...) if err != nil { diff --git a/hrp/internal/dial/traceroute_windows.go b/hrp/internal/dial/traceroute_windows.go index 7aa0581c..b80d199e 100644 --- a/hrp/internal/dial/traceroute_windows.go +++ b/hrp/internal/dial/traceroute_windows.go @@ -17,6 +17,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) var ( diff --git a/hrp/internal/myexec/cmd_windows.go b/hrp/internal/myexec/cmd_windows.go index 6844058f..3e202002 100644 --- a/hrp/internal/myexec/cmd_windows.go +++ b/hrp/internal/myexec/cmd_windows.go @@ -3,9 +3,12 @@ package myexec import ( + "fmt" "os" "os/exec" "path/filepath" + "strconv" + "strings" "syscall" "github.com/pkg/errors" diff --git a/hrp/internal/scaffold/main.go b/hrp/internal/scaffold/main.go index 28b2c851..13a81696 100644 --- a/hrp/internal/scaffold/main.go +++ b/hrp/internal/scaffold/main.go @@ -207,7 +207,7 @@ func createPythonPlugin(projectName, venv string) error { packages := []string{ fmt.Sprintf("funppy==%s", fungo.Version), - fmt.Sprintf("httprunner==%s", version.VERSION), + fmt.Sprintf("httprunner==%s", version.HttpRunnerMinimumVersion), } _, err = myexec.EnsurePython3Venv(venv, packages...) if err != nil { diff --git a/hrp/internal/version/init.go b/hrp/internal/version/init.go index 4887463b..d1f1513e 100644 --- a/hrp/internal/version/init.go +++ b/hrp/internal/version/init.go @@ -6,3 +6,6 @@ import ( //go:embed VERSION var VERSION string + +// httprunner python version +const HttpRunnerMinimumVersion = "v4.2.0" diff --git a/hrp/pkg/httpstat/main.go b/hrp/pkg/httpstat/main.go index 49242193..8688bf25 100644 --- a/hrp/pkg/httpstat/main.go +++ b/hrp/pkg/httpstat/main.go @@ -11,6 +11,7 @@ import ( "net/http/httptrace" "strconv" "strings" + "sync" "time" "github.com/fatih/color" @@ -100,6 +101,8 @@ type Stat struct { // connected network info network, addr string + + mux *sync.RWMutex // avoid data race } // Finish sets the time when reading response is done. @@ -176,6 +179,7 @@ func (s *Stat) Print() { // WithHTTPStat is a wrapper of httptrace.WithClientTrace. // It records the time of each httptrace hooks. func WithHTTPStat(req *http.Request, s *Stat) context.Context { + s.mux = new(sync.RWMutex) s.schema = req.URL.Scheme return httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{ DNSStart: func(i httptrace.DNSStartInfo) { @@ -228,6 +232,8 @@ func WithHTTPStat(req *http.Request, s *Stat) context.Context { }, WroteRequest: func(info httptrace.WroteRequestInfo) { + s.mux.Lock() + defer s.mux.Unlock() now := time.Now() s.serverStart = now @@ -259,6 +265,8 @@ func WithHTTPStat(req *http.Request, s *Stat) context.Context { }, GotFirstResponseByte: func() { + s.mux.Lock() + defer s.mux.Unlock() s.serverDone = time.Now() s.ServerProcessing = s.serverDone.Sub(s.serverStart) s.StartTransfer = s.serverDone.Sub(s.dnsStart) From 7b0a442a7aecd9f3276d664ed8c57f643141ad71 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Sat, 15 Oct 2022 23:50:31 +0800 Subject: [PATCH 152/169] feat: get ocr position by given recognition area --- hrp/pkg/uixt/demo/main_test.go | 2 +- hrp/pkg/uixt/ext.go | 20 ++++-- hrp/pkg/uixt/ocr_vedem.go | 117 ++++++++++++++++++++++++++------- hrp/pkg/uixt/tap.go | 12 ++-- hrp/step.go | 1 + 5 files changed, 116 insertions(+), 36 deletions(-) diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 6dac96d2..9456a550 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -32,7 +32,7 @@ func TestIOSDemo(t *testing.T) { // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 for { - points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}) + points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}, nil) if err != nil { time.Sleep(1 * time.Second) continue diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 564bad59..4696c656 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -67,6 +67,7 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app + RecognitionArea []float64 `json:"recognition_area,omitempty" yaml:"recognition_area,omitempty"` // used by ocr to get text position in the recognition area Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found @@ -103,6 +104,13 @@ func WithCustomDirection(sx, sy, ex, ey float64) ActionOption { } } +// WithRecognitionArea inputs area of [(x1,y1), (x2,y2)] +func WithRecognitionArea(x1, y1, x2, y2 float64) ActionOption { + return func(o *MobileAction) { + o.RecognitionArea = []float64{x1, y1, x2, y2} + } +} + func WithText(text string) ActionOption { return func(o *MobileAction) { o.Text = text @@ -302,7 +310,7 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { func (dExt *DriverExt) FindUIRectInUIKit(search string, index ...int) (x, y, width, height float64, err error) { // click on text, using OCR if !isPathExists(search) { - return dExt.FindTextByOCR(search, index...) + return dExt.FindTextByOCR(search, nil, index...) } // click on image, using opencv return dExt.FindImageRectInUIKit(search, index...) @@ -339,7 +347,7 @@ func (dExt *DriverExt) IsLabelExist(label string) bool { } func (dExt *DriverExt) IsOCRExist(text string) bool { - _, _, _, _, err := dExt.FindTextByOCR(text) + _, _, _, _, err := dExt.FindTextByOCR(text, nil) return err == nil } @@ -374,7 +382,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var point PointF findApp := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(appName, action.Index) + point, err = d.GetTextXY(appName, action.RecognitionArea, action.Index) return err } foundAppAction := func(d *DriverExt) error { @@ -406,7 +414,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var point PointF findText := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(text, action.Index) + point, err = d.GetTextXY(text, action.RecognitionArea, action.Index) return err } foundTextAction := func(d *DriverExt) error { @@ -439,7 +447,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { var point PointF findText := func(d *DriverExt) error { var err error - points, err := d.GetTextXYs(texts) + points, err := d.GetTextXYs(texts, action.RecognitionArea) if err != nil { return err } @@ -511,7 +519,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) case ACTION_TapByOCR: if ocrText, ok := action.Params.(string); ok { - return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, action.Index) + return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, action.RecognitionArea, action.Index) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) case ACTION_TapByCV: diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 82d142dc..11388ba6 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -5,6 +5,7 @@ import ( "fmt" "image" "io/ioutil" + "math" "mime/multipart" "net/http" "os" @@ -109,7 +110,7 @@ func getLogID(header http.Header) string { return logID[0] } -func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) { +func (s *veDEMOCRService) FindText(text string, imageBuf []byte, recAbsArea []int, index ...int) (rect image.Rectangle, err error) { if len(index) == 0 { index = []int{0} // index not specified } @@ -120,16 +121,25 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( return } + if len(recAbsArea) != 4 { + recAbsArea = []int{0, 0, math.MaxInt64, math.MaxInt64} + } + + var minX, minY, maxX, maxY int + if recAbsArea[0] < recAbsArea[2] { + minX, maxX = recAbsArea[0], recAbsArea[2] + } else { + minX, maxX = recAbsArea[2], recAbsArea[0] + } + if recAbsArea[1] < recAbsArea[3] { + minY, maxY = recAbsArea[1], recAbsArea[3] + } else { + minY, maxY = recAbsArea[3], recAbsArea[1] + } + var rects []image.Rectangle var ocrTexts []string for _, ocrResult := range ocrResults { - ocrTexts = append(ocrTexts, ocrResult.Text) - - // not contains text - if !strings.Contains(ocrResult.Text, text) { - continue - } - rect = image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 Min: image.Point{ @@ -141,7 +151,16 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( Y: int(ocrResult.Points[2].Y), }, } - rects = append(rects, rect) + if rect.Min.X > minX && rect.Max.X < maxX && rect.Min.Y < maxY && rect.Max.Y > minY { + ocrTexts = append(ocrTexts, ocrResult.Text) + + // not contains text + if !strings.Contains(ocrResult.Text, text) { + continue + } + + rects = append(rects, rect) + } // contains text while not match exactly if ocrResult.Text != text { @@ -177,23 +196,36 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) ( return rects[idx], nil } -func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects []image.Rectangle, err error) { +func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, recAbsArea []int) (rects []image.Rectangle, err error) { ocrResults, err := s.getOCRResult(imageBuf) if err != nil { log.Error().Err(err).Msg("getOCRResult failed") return } + if len(recAbsArea) != 4 { + recAbsArea = []int{0, 0, math.MaxInt64, math.MaxInt64} + } + + var minX, minY, maxX, maxY int + if recAbsArea[0] < recAbsArea[2] { + minX, maxX = recAbsArea[0], recAbsArea[2] + } else { + minX, maxX = recAbsArea[2], recAbsArea[0] + } + if recAbsArea[1] < recAbsArea[3] { + minY, maxY = recAbsArea[1], recAbsArea[3] + } else { + minY, maxY = recAbsArea[3], recAbsArea[1] + } + + var success bool + var rect image.Rectangle + var ocrTexts []string for _, text := range texts { var found bool for _, ocrResult := range ocrResults { - // not contains text - if !strings.Contains(ocrResult.Text, text) { - continue - } - - found = true - rect := image.Rectangle{ + rect = image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 Min: image.Point{ X: int(ocrResult.Points[0].X), @@ -204,12 +236,29 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte) (rects []im Y: int(ocrResult.Points[2].Y), }, } - rects = append(rects, rect) - break + + if rect.Min.X > minX && rect.Max.X < maxX && rect.Min.Y < maxY && rect.Max.Y > minY { + ocrTexts = append(ocrTexts, ocrResult.Text) + + // not contains text + if !strings.Contains(ocrResult.Text, text) { + continue + } + + found = true + rects = append(rects, rect) + break + } } if !found { rects = append(rects, image.Rectangle{}) } + success = found || success + } + + if !success { + return rects, + fmt.Errorf("texts %s not found in %v", texts, ocrTexts) } return rects, nil @@ -219,15 +268,26 @@ type OCRService interface { FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) } -func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindTextByOCR(ocrText string, recognitionArea []float64, index ...int) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) return } + if len(recognitionArea) != 4 { + recognitionArea = []float64{0, 0, 1, 1} + } + + absArea := []int{ + int(recognitionArea[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(recognitionArea[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(recognitionArea[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(recognitionArea[3] * float64(dExt.windowSize.Height) * dExt.scale), + } + service := &veDEMOCRService{} - rect, err := service.FindText(ocrText, bufSource.Bytes(), index...) + rect, err := service.FindText(ocrText, bufSource.Bytes(), absArea, index...) if err != nil { log.Warn().Msgf("FindText failed: %s", err.Error()) err = fmt.Errorf("FindText failed: %v", err) @@ -240,15 +300,26 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, return } -func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string) (points [][]float64, err error) { +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, recognitionArea []float64) (points [][]float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) return } + if len(recognitionArea) != 4 { + recognitionArea = []float64{0, 0, 1, 1} + } + + absArea := []int{ + int(recognitionArea[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(recognitionArea[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(recognitionArea[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(recognitionArea[3] * float64(dExt.windowSize.Height) * dExt.scale), + } + service := &veDEMOCRService{} - rects, err := service.FindTexts(ocrTexts, bufSource.Bytes()) + rects, err := service.FindTexts(ocrTexts, bufSource.Bytes(), absArea) if err != nil { log.Warn().Msgf("FindTexts failed: %s", err.Error()) err = fmt.Errorf("FindTexts failed: %v", err) diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index 7957f3dd..611a5174 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -28,8 +28,8 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { return dExt.TapAbsXY(x, y, identifier) } -func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, err error) { - x, y, width, height, err := dExt.FindTextByOCR(ocrText, index...) +func (dExt *DriverExt) GetTextXY(ocrText string, recognitionArea []float64, index ...int) (point PointF, err error) { + x, y, width, height, err := dExt.FindTextByOCR(ocrText, recognitionArea, index...) if err != nil { return PointF{}, err } @@ -41,8 +41,8 @@ func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, er return point, nil } -func (dExt *DriverExt) GetTextXYs(ocrText []string) (points []PointF, err error) { - ps, err := dExt.FindTextsByOCR(ocrText) +func (dExt *DriverExt) GetTextXYs(ocrText []string, recognitionArea []float64) (points []PointF, err error) { + ps, err := dExt.FindTextsByOCR(ocrText, recognitionArea) if err != nil { return nil, err } @@ -71,8 +71,8 @@ func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, return point, nil } -func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error { - point, err := dExt.GetTextXY(ocrText, index...) +func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, recognitionArea []float64, index ...int) error { + point, err := dExt.GetTextXY(ocrText, recognitionArea, index...) if err != nil { if ignoreNotFoundError { return nil diff --git a/hrp/step.go b/hrp/step.go index ac7481f1..8873f947 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -31,6 +31,7 @@ var ( WithDescription = uixt.WithDescription WithDirection = uixt.WithDirection WithCustomDirection = uixt.WithCustomDirection + WithRecognitionArea = uixt.WithRecognitionArea ) var ( From 1ed4fcd1e0d19cfa6382d0b49c505a46fdfe94e2 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Sun, 16 Oct 2022 13:11:13 +0800 Subject: [PATCH 153/169] fix: modify function call parameters --- hrp/pkg/uixt/demo/main_test.go | 2 +- hrp/pkg/uixt/ext.go | 78 +++++++++++++++++++++++---- hrp/pkg/uixt/ocr_vedem.go | 99 ++++++++++++++-------------------- hrp/pkg/uixt/tap.go | 12 ++--- hrp/step.go | 2 +- 5 files changed, 116 insertions(+), 77 deletions(-) diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 9456a550..6dac96d2 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -32,7 +32,7 @@ func TestIOSDemo(t *testing.T) { // 持续监测手机屏幕,直到出现青少年模式弹窗后,点击「我知道了」 for { - points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}, nil) + points, err := driverExt.GetTextXYs([]string{"青少年模式", "我知道了"}) if err != nil { time.Sleep(1 * time.Second) continue diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 4696c656..d61aad8b 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -67,7 +67,7 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app - RecognitionArea []float64 `json:"recognition_area,omitempty" yaml:"recognition_area,omitempty"` // used by ocr to get text position in the recognition area + Scope []float64 `json:"scope,omitempty" yaml:"scope,omitempty"` // used by ocr to get text position in the scope Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found @@ -104,10 +104,10 @@ func WithCustomDirection(sx, sy, ex, ey float64) ActionOption { } } -// WithRecognitionArea inputs area of [(x1,y1), (x2,y2)] -func WithRecognitionArea(x1, y1, x2, y2 float64) ActionOption { +// WithScope inputs area of [(x1,y1), (x2,y2)] +func WithScope(x1, y1, x2, y2 float64) ActionOption { return func(o *MobileAction) { - o.RecognitionArea = []float64{x1, y1, x2, y2} + o.Scope = []float64{x1, y1, x2, y2} } } @@ -310,7 +310,7 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { func (dExt *DriverExt) FindUIRectInUIKit(search string, index ...int) (x, y, width, height float64, err error) { // click on text, using OCR if !isPathExists(search) { - return dExt.FindTextByOCR(search, nil, index...) + return dExt.FindTextByOCR(search, WithCustomOption("index", index)) } // click on image, using opencv return dExt.FindImageRectInUIKit(search, index...) @@ -347,7 +347,7 @@ func (dExt *DriverExt) IsLabelExist(label string) bool { } func (dExt *DriverExt) IsOCRExist(text string) bool { - _, _, _, _, err := dExt.FindTextByOCR(text, nil) + _, _, _, _, err := dExt.FindTextByOCR(text) return err == nil } @@ -379,10 +379,25 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { AppLaunchUnattached, action.Params) case ACTION_SwipeToTapApp: if appName, ok := action.Params.(string); ok { + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + var options []DataOption + options = append(options, + WithCustomOption("index", []int{action.Index}), + WithCustomOption("scope", []int{ + int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), + }), + ) + var point PointF findApp := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(appName, action.RecognitionArea, action.Index) + point, err = d.GetTextXY(appName, options...) return err } foundAppAction := func(d *DriverExt) error { @@ -411,10 +426,25 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: if text, ok := action.Params.(string); ok { + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + var options []DataOption + options = append(options, + WithCustomOption("index", []int{action.Index}), + WithCustomOption("scope", []int{ + int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), + }), + ) + var point PointF findText := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(text, action.RecognitionArea, action.Index) + point, err = d.GetTextXY(text, options...) return err } foundTextAction := func(d *DriverExt) error { @@ -444,10 +474,24 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Params = textList } if texts, ok := action.Params.([]string); ok { + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + var options []DataOption + options = append(options, + WithCustomOption("scope", []int{ + int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), + }), + ) + var point PointF findText := func(d *DriverExt) error { var err error - points, err := d.GetTextXYs(texts, action.RecognitionArea) + points, err := d.GetTextXYs(texts, options...) if err != nil { return err } @@ -519,7 +563,21 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) case ACTION_TapByOCR: if ocrText, ok := action.Params.(string); ok { - return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, action.RecognitionArea, action.Index) + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + var options []DataOption + options = append(options, + WithCustomOption("index", []int{action.Index}), + WithCustomOption("scope", []int{ + int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), + int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), + int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), + }), + ) + return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, options...) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) case ACTION_TapByCV: diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 11388ba6..cc79dcb5 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -110,9 +110,29 @@ func getLogID(header http.Header) string { return logID[0] } -func (s *veDEMOCRService) FindText(text string, imageBuf []byte, recAbsArea []int, index ...int) (rect image.Rectangle, err error) { - if len(index) == 0 { - index = []int{0} // index not specified +func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...DataOption) (rect image.Rectangle, err error) { + data := map[string]interface{}{} + for _, option := range options { + option(data) + } + + if _, ok := data["index"]; !ok { + data["index"] = []int{0} // index not specified + } + + index, ok := data["index"].([]int) + if !ok || len(index) == 0 { + index = []int{0} + } + + _, ok = data["scope"] + if !ok { + data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified + } + + scope, ok := data["scope"].([]int) + if !ok || len(scope) != 4 { + scope = []int{0, 0, math.MaxInt64, math.MaxInt64} } ocrResults, err := s.getOCRResult(imageBuf) @@ -121,22 +141,6 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, recAbsArea []in return } - if len(recAbsArea) != 4 { - recAbsArea = []int{0, 0, math.MaxInt64, math.MaxInt64} - } - - var minX, minY, maxX, maxY int - if recAbsArea[0] < recAbsArea[2] { - minX, maxX = recAbsArea[0], recAbsArea[2] - } else { - minX, maxX = recAbsArea[2], recAbsArea[0] - } - if recAbsArea[1] < recAbsArea[3] { - minY, maxY = recAbsArea[1], recAbsArea[3] - } else { - minY, maxY = recAbsArea[3], recAbsArea[1] - } - var rects []image.Rectangle var ocrTexts []string for _, ocrResult := range ocrResults { @@ -151,7 +155,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, recAbsArea []in Y: int(ocrResult.Points[2].Y), }, } - if rect.Min.X > minX && rect.Max.X < maxX && rect.Min.Y < maxY && rect.Max.Y > minY { + if rect.Min.X > scope[0] && rect.Max.X < scope[2] && rect.Min.Y > scope[1] && rect.Max.Y < scope[3] { ocrTexts = append(ocrTexts, ocrResult.Text) // not contains text @@ -196,27 +200,26 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, recAbsArea []in return rects[idx], nil } -func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, recAbsArea []int) (rects []image.Rectangle, err error) { +func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ...DataOption) (rects []image.Rectangle, err error) { ocrResults, err := s.getOCRResult(imageBuf) if err != nil { log.Error().Err(err).Msg("getOCRResult failed") return } - if len(recAbsArea) != 4 { - recAbsArea = []int{0, 0, math.MaxInt64, math.MaxInt64} + data := map[string]interface{}{} + for _, option := range options { + option(data) } - var minX, minY, maxX, maxY int - if recAbsArea[0] < recAbsArea[2] { - minX, maxX = recAbsArea[0], recAbsArea[2] - } else { - minX, maxX = recAbsArea[2], recAbsArea[0] + _, ok := data["scope"] + if !ok { + data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified } - if recAbsArea[1] < recAbsArea[3] { - minY, maxY = recAbsArea[1], recAbsArea[3] - } else { - minY, maxY = recAbsArea[3], recAbsArea[1] + + scope, ok := data["scope"].([]int) + if !ok || len(scope) != 4 { + scope = []int{0, 0, math.MaxInt64, math.MaxInt64} } var success bool @@ -237,7 +240,7 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, recAbsArea }, } - if rect.Min.X > minX && rect.Max.X < maxX && rect.Min.Y < maxY && rect.Max.Y > minY { + if rect.Min.X > scope[0] && rect.Max.X < scope[2] && rect.Min.Y > scope[1] && rect.Max.Y < scope[3] { ocrTexts = append(ocrTexts, ocrResult.Text) // not contains text @@ -268,26 +271,15 @@ type OCRService interface { FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) } -func (dExt *DriverExt) FindTextByOCR(ocrText string, recognitionArea []float64, index ...int) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) return } - if len(recognitionArea) != 4 { - recognitionArea = []float64{0, 0, 1, 1} - } - - absArea := []int{ - int(recognitionArea[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(recognitionArea[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(recognitionArea[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(recognitionArea[3] * float64(dExt.windowSize.Height) * dExt.scale), - } - service := &veDEMOCRService{} - rect, err := service.FindText(ocrText, bufSource.Bytes(), absArea, index...) + rect, err := service.FindText(ocrText, bufSource.Bytes(), options...) if err != nil { log.Warn().Msgf("FindText failed: %s", err.Error()) err = fmt.Errorf("FindText failed: %v", err) @@ -300,26 +292,15 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, recognitionArea []float64, return } -func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, recognitionArea []float64) (points [][]float64, err error) { +func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption) (points [][]float64, err error) { var bufSource *bytes.Buffer if bufSource, err = dExt.takeScreenShot(); err != nil { err = fmt.Errorf("takeScreenShot error: %v", err) return } - if len(recognitionArea) != 4 { - recognitionArea = []float64{0, 0, 1, 1} - } - - absArea := []int{ - int(recognitionArea[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(recognitionArea[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(recognitionArea[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(recognitionArea[3] * float64(dExt.windowSize.Height) * dExt.scale), - } - service := &veDEMOCRService{} - rects, err := service.FindTexts(ocrTexts, bufSource.Bytes(), absArea) + rects, err := service.FindTexts(ocrTexts, bufSource.Bytes(), options...) if err != nil { log.Warn().Msgf("FindTexts failed: %s", err.Error()) err = fmt.Errorf("FindTexts failed: %v", err) diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index 611a5174..d46c34aa 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -28,8 +28,8 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { return dExt.TapAbsXY(x, y, identifier) } -func (dExt *DriverExt) GetTextXY(ocrText string, recognitionArea []float64, index ...int) (point PointF, err error) { - x, y, width, height, err := dExt.FindTextByOCR(ocrText, recognitionArea, index...) +func (dExt *DriverExt) GetTextXY(ocrText string, options ...DataOption) (point PointF, err error) { + x, y, width, height, err := dExt.FindTextByOCR(ocrText, options...) if err != nil { return PointF{}, err } @@ -41,8 +41,8 @@ func (dExt *DriverExt) GetTextXY(ocrText string, recognitionArea []float64, inde return point, nil } -func (dExt *DriverExt) GetTextXYs(ocrText []string, recognitionArea []float64) (points []PointF, err error) { - ps, err := dExt.FindTextsByOCR(ocrText, recognitionArea) +func (dExt *DriverExt) GetTextXYs(ocrText []string, options ...DataOption) (points []PointF, err error) { + ps, err := dExt.FindTextsByOCR(ocrText, options...) if err != nil { return nil, err } @@ -71,8 +71,8 @@ func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, return point, nil } -func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, recognitionArea []float64, index ...int) error { - point, err := dExt.GetTextXY(ocrText, recognitionArea, index...) +func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, options ...DataOption) error { + point, err := dExt.GetTextXY(ocrText, options...) if err != nil { if ignoreNotFoundError { return nil diff --git a/hrp/step.go b/hrp/step.go index 8873f947..1f3cb5ee 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -31,7 +31,7 @@ var ( WithDescription = uixt.WithDescription WithDirection = uixt.WithDirection WithCustomDirection = uixt.WithCustomDirection - WithRecognitionArea = uixt.WithRecognitionArea + WithScope = uixt.WithScope ) var ( From 2df2792fe4d7b31262c74c1f5ec3fe0cc2546ef3 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Sun, 16 Oct 2022 23:31:13 +0800 Subject: [PATCH 154/169] change: update function optional parameters to DataOptions --- hrp/pkg/uixt/android_driver.go | 4 +- hrp/pkg/uixt/android_elment.go | 2 +- hrp/pkg/uixt/demo/main_test.go | 2 +- hrp/pkg/uixt/drag.go | 2 +- hrp/pkg/uixt/ext.go | 95 +++++++++++++--------------------- hrp/pkg/uixt/interface.go | 38 ++++++++++++-- hrp/pkg/uixt/ios_driver.go | 6 +-- hrp/pkg/uixt/ios_test.go | 2 +- hrp/pkg/uixt/ocr_vedem.go | 37 +++++-------- hrp/pkg/uixt/opencv_off.go | 2 +- hrp/pkg/uixt/opencv_on.go | 2 +- hrp/pkg/uixt/swipe.go | 37 ++++++------- hrp/pkg/uixt/tap.go | 70 ++++++++++++++----------- 13 files changed, 149 insertions(+), 150 deletions(-) diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index b4a2b713..f9f065ce 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -573,7 +573,7 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...D } // append options in post data for extra uiautomator configurations - // e.g. use WithPressDuration to set pressForDuration + // e.g. use WithPressDurationOption to set pressForDuration for _, option := range options { option(data) } @@ -590,7 +590,7 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...D // per step. So for a 100 steps, the swipe will take about 1/2 second to complete. // `steps` is the number of move steps sent to the system func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { - options = append(options, WithSteps(12)) + options = append(options, WithStepsOption(12)) return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } diff --git a/hrp/pkg/uixt/android_elment.go b/hrp/pkg/uixt/android_elment.go index e03c792d..e8fa67b1 100644 --- a/hrp/pkg/uixt/android_elment.go +++ b/hrp/pkg/uixt/android_elment.go @@ -113,7 +113,7 @@ func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error { func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { options := []DataOption{ - WithSteps(12), + WithStepsOption(12), WithCustomOption("elementId", ue.id), } return ue.parent._swipe(fromX, fromY, toX, toY, options...) diff --git a/hrp/pkg/uixt/demo/main_test.go b/hrp/pkg/uixt/demo/main_test.go index 6dac96d2..1ff036ce 100644 --- a/hrp/pkg/uixt/demo/main_test.go +++ b/hrp/pkg/uixt/demo/main_test.go @@ -38,7 +38,7 @@ func TestIOSDemo(t *testing.T) { continue } - err = driverExt.TapAbsXY(points[1].X, points[1].Y, "") + err = driverExt.TapAbsXY(points[1].X, points[1].Y) if err != nil { t.Fatal(err) } diff --git a/hrp/pkg/uixt/drag.go b/hrp/pkg/uixt/drag.go index 27a13501..c049d71b 100644 --- a/hrp/pkg/uixt/drag.go +++ b/hrp/pkg/uixt/drag.go @@ -26,5 +26,5 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs fromY := y + height*yOffset return dExt.Driver.DragFloat(fromX, fromY, toX, toY, - WithPressDuration(pressForDuration[0])) + WithPressDurationOption(pressForDuration[0])) } diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index d61aad8b..516eda63 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -307,13 +307,13 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { return dExt.Driver.FindElement(selector) } -func (dExt *DriverExt) FindUIRectInUIKit(search string, index ...int) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindUIRectInUIKit(search string, options ...DataOption) (x, y, width, height float64, err error) { // click on text, using OCR if !isPathExists(search) { - return dExt.FindTextByOCR(search, WithCustomOption("index", index)) + return dExt.FindTextByOCR(search, options...) } // click on image, using opencv - return dExt.FindImageRectInUIKit(search, index...) + return dExt.FindImageRectInUIKit(search, options...) } func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) { @@ -383,26 +383,19 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - var options []DataOption - options = append(options, - WithCustomOption("index", []int{action.Index}), - WithCustomOption("scope", []int{ - int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), - }), - ) + identifierOption := WithIdentifierOption(action.Identifier) + indexOption := WithIndexOption(action.Index) + scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findApp := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(appName, options...) + point, err = d.GetTextXY(appName, scopeOption, indexOption) return err } foundAppAction := func(d *DriverExt) error { // click app to launch - return d.TapAbsXY(point.X, point.Y-25, action.Identifier) + return d.TapAbsXY(point.X, point.Y-25, identifierOption) } // go to home screen @@ -430,26 +423,19 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - var options []DataOption - options = append(options, - WithCustomOption("index", []int{action.Index}), - WithCustomOption("scope", []int{ - int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), - }), - ) + identifierOption := WithIdentifierOption(action.Identifier) + indexOption := WithIndexOption(action.Index) + scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findText := func(d *DriverExt) error { var err error - point, err = d.GetTextXY(text, options...) + point, err = d.GetTextXY(text, indexOption, scopeOption) return err } foundTextAction := func(d *DriverExt) error { // tap text - return d.TapAbsXY(point.X, point.Y, action.Identifier) + return d.TapAbsXY(point.X, point.Y, identifierOption) } // default to retry 10 times @@ -478,20 +464,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - var options []DataOption - options = append(options, - WithCustomOption("scope", []int{ - int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), - }), - ) + scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findText := func(d *DriverExt) error { var err error - points, err := d.GetTextXYs(texts, options...) + points, err := d.GetTextXYs(texts, scopeOption) if err != nil { return err } @@ -504,7 +482,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } foundTextAction := func(d *DriverExt) error { // tap text - return d.TapAbsXY(point.X, point.Y, action.Identifier) + return d.TapAbsXY(point.X, point.Y, WithIdentifierOption(action.Identifier)) } // default to retry 10 times @@ -542,7 +520,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } x, _ := location[0].(float64) y, _ := location[1].(float64) - return dExt.TapXY(x, y, action.Identifier) + return dExt.TapXY(x, y, WithIdentifierOption(action.Identifier)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) case ACTION_TapAbsXY: @@ -553,12 +531,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } x, _ := location[0].(float64) y, _ := location[1].(float64) - return dExt.TapAbsXY(x, y, action.Identifier) + return dExt.TapAbsXY(x, y, WithIdentifierOption(action.Identifier)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params) case ACTION_Tap: if param, ok := action.Params.(string); ok { - return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError, action.Index) + return dExt.Tap(param, WithIdentifierOption(action.Identifier), WithIgnoreNotFoundErrorOption(true), WithIndexOption(action.Index)) } return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) case ACTION_TapByOCR: @@ -567,22 +545,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - var options []DataOption - options = append(options, - WithCustomOption("index", []int{action.Index}), - WithCustomOption("scope", []int{ - int(action.Scope[0] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[1] * float64(dExt.windowSize.Height) * dExt.scale), - int(action.Scope[2] * float64(dExt.windowSize.Width) * dExt.scale), - int(action.Scope[3] * float64(dExt.windowSize.Height) * dExt.scale), - }), - ) - return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, options...) + indexOption := WithIndexOption(action.Index) + scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + identifierOption := WithIdentifierOption(action.Identifier) + IgnoreNotFoundErrorOption := WithIgnoreNotFoundErrorOption(action.IgnoreNotFoundError) + return dExt.TapByOCR(ocrText, identifierOption, IgnoreNotFoundErrorOption, indexOption, scopeOption) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) case ACTION_TapByCV: if imagePath, ok := action.Params.(string); ok { - return dExt.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError, action.Index) + return dExt.TapByCV(imagePath, WithIdentifierOption(action.Identifier), WithIgnoreNotFoundErrorOption(true), WithIndexOption(action.Index)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params) case ACTION_DoubleTapXY: @@ -602,6 +574,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params) case ACTION_Swipe: + identifierOption := WithIdentifierOption(action.Identifier) if positions, ok := action.Params.([]interface{}); ok { // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] if len(positions) != 4 { @@ -611,10 +584,10 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { fromY, _ := positions[1].(float64) toX, _ := positions[2].(float64) toY, _ := positions[3].(float64) - return dExt.SwipeRelative(fromX, fromY, toX, toY, action.Identifier) + return dExt.SwipeRelative(fromX, fromY, toX, toY, identifierOption) } if direction, ok := action.Params.(string); ok { - return dExt.SwipeTo(direction, action.Identifier) + return dExt.SwipeTo(direction, identifierOption) } return fmt.Errorf("invalid %s params: %v", ACTION_Swipe, action.Params) case ACTION_Input: @@ -633,10 +606,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { options = append(options, WithCustomOption("description", action.Description)) } if action.Identifier != "" { - options = append(options, WithCustomOption("log", map[string]interface{}{ - "enable": true, - "data": action.Identifier, - })) + options = append(options, WithIdentifierOption(action.Identifier)) } return dExt.Driver.Input(param, options...) case CtlSleep: @@ -671,6 +641,13 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return nil } +func (dExt *DriverExt) GetAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) { + return int(x1 * float64(dExt.windowSize.Width) * dExt.scale), + int(y1 * float64(dExt.windowSize.Height) * dExt.scale), + int(x2 * float64(dExt.windowSize.Width) * dExt.scale), + int(y2 * float64(dExt.windowSize.Height) * dExt.scale) +} + func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool { var exists bool if assert == AssertionExists { diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 93b07179..1bf107ec 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -781,24 +781,54 @@ func WithCustomOption(key string, value interface{}) DataOption { } } -func WithPressDuration(duraion float64) DataOption { +func WithPressDurationOption(duraion float64) DataOption { return func(data map[string]interface{}) { data["duration"] = duraion } } -func WithSteps(steps int) DataOption { +func WithStepsOption(steps int) DataOption { return func(data map[string]interface{}) { data["steps"] = steps } } -func WithFrequency(frequency int) DataOption { +func WithFrequencyOption(frequency int) DataOption { return func(data map[string]interface{}) { data["frequency"] = frequency } } +func WithIndexOption(index int) DataOption { + return func(data map[string]interface{}) { + data["index"] = index + } +} + +func WithScopeOption(x1, x2, y1, y2 int) DataOption { + return func(data map[string]interface{}) { + data["scope"] = []int{x1, x2, y1, y2} + } +} + +func WithIdentifierOption(identifier string) DataOption { + if identifier == "" { + return func(data map[string]interface{}) {} + } + return func(data map[string]interface{}) { + data["log"] = map[string]interface{}{ + "enable": true, + "data": identifier, + } + } +} + +func WithIgnoreNotFoundErrorOption(ignoreError bool) DataOption { + return func(data map[string]interface{}) { + data["ignoreNotFoundError"] = ignoreError + } +} + // current implemeted device: IOSDevice, AndroidDevice type Device interface { UUID() string @@ -905,7 +935,7 @@ type WebDriver interface { TouchAndHoldFloat(x, y float64, second ...float64) error // Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate. - // WithPressDuration option can be used to set pressForDuration (default to 1 second). + // WithPressDurationOption option can be used to set pressForDuration (default to 1 second). Drag(fromX, fromY, toX, toY int, options ...DataOption) error DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) error diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index 4e4e131f..e0a97c6f 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -434,7 +434,7 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp } // append options in post data for extra WDA configurations - // e.g. use WithPressDuration to set pressForDuration + // e.g. use WithPressDurationOption to set pressForDuration for _, option := range options { option(data) } @@ -447,12 +447,12 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp } func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { - options = append(options, WithPressDuration(0)) + options = append(options, WithPressDurationOption(0)) return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { - options = append(options, WithPressDuration(0)) + options = append(options, WithPressDurationOption(0)) return wd.DragFloat(fromX, fromY, toX, toY, options...) } diff --git a/hrp/pkg/uixt/ios_test.go b/hrp/pkg/uixt/ios_test.go index 136d4876..a635e305 100644 --- a/hrp/pkg/uixt/ios_test.go +++ b/hrp/pkg/uixt/ios_test.go @@ -443,7 +443,7 @@ func Test_remoteWD_TouchAndHold(t *testing.T) { func Test_remoteWD_Drag(t *testing.T) { setup(t) - // err := driver.Drag(200, 300, 200, 500, WithPressDuration(0.5)) + // err := driver.Drag(200, 300, 200, 500, WithPressDurationOption(0.5)) err := driver.Swipe(200, 300, 200, 500) if err != nil { t.Fatal(err) diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index cc79dcb5..96a59d43 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -117,23 +117,14 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data } if _, ok := data["index"]; !ok { - data["index"] = []int{0} // index not specified + data["index"] = 0 // index not specified } + index, _ := data["index"].(int) - index, ok := data["index"].([]int) - if !ok || len(index) == 0 { - index = []int{0} - } - - _, ok = data["scope"] - if !ok { + if _, ok := data["scope"]; !ok { data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified } - - scope, ok := data["scope"].([]int) - if !ok || len(scope) != 4 { - scope = []int{0, 0, math.MaxInt64, math.MaxInt64} - } + scope, _ := data["scope"].([]int) ocrResults, err := s.getOCRResult(imageBuf) if err != nil { @@ -172,7 +163,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data } // match exactly, and not specify index, return the first one - if index[0] == 0 { + if index == 0 { return rect, nil } } @@ -183,7 +174,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data } // get index - idx := index[0] + idx := index if idx > 0 { // NOTICE: index start from 1 idx = idx - 1 @@ -212,19 +203,15 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... option(data) } - _, ok := data["scope"] - if !ok { + if _, ok := data["scope"]; !ok { data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified } - - scope, ok := data["scope"].([]int) - if !ok || len(scope) != 4 { - scope = []int{0, 0, math.MaxInt64, math.MaxInt64} - } + scope, _ := data["scope"].([]int) var success bool var rect image.Rectangle - var ocrTexts []string + ocrTexts := map[string]bool{} + for _, text := range texts { var found bool for _, ocrResult := range ocrResults { @@ -240,8 +227,8 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... }, } - if rect.Min.X > scope[0] && rect.Max.X < scope[2] && rect.Min.Y > scope[1] && rect.Max.Y < scope[3] { - ocrTexts = append(ocrTexts, ocrResult.Text) + if rect.Min.X >= scope[0] && rect.Max.X <= scope[2] && rect.Min.Y >= scope[1] && rect.Max.Y <= scope[3] { + ocrTexts[ocrResult.Text] = true // not contains text if !strings.Contains(ocrResult.Text, text) { diff --git a/hrp/pkg/uixt/opencv_off.go b/hrp/pkg/uixt/opencv_off.go index db1865ae..ade2c4cd 100644 --- a/hrp/pkg/uixt/opencv_off.go +++ b/hrp/pkg/uixt/opencv_off.go @@ -17,7 +17,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, return } -func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, err error) { log.Fatal().Msg("opencv is not supported") return } diff --git a/hrp/pkg/uixt/opencv_on.go b/hrp/pkg/uixt/opencv_on.go index d4acda45..e8b3d407 100644 --- a/hrp/pkg/uixt/opencv_on.go +++ b/hrp/pkg/uixt/opencv_on.go @@ -111,7 +111,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, return } -func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) { +func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOption) (x, y, width, height float64, err error) { var bufSource, bufSearch *bytes.Buffer if bufSearch, err = getBufFromDisk(imagePath); err != nil { return 0, 0, 0, 0, err diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index 8f2ba9f9..41a11d2a 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -12,7 +12,7 @@ func assertRelative(p float64) bool { } // SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY] -func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier ...string) error { +func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...DataOption) error { width := dExt.windowSize.Width height := dExt.windowSize.Height @@ -27,44 +27,37 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier toX = float64(width) * toX toY = float64(height) * toY - if len(identifier) > 0 && identifier[0] != "" { - option := WithCustomOption("log", map[string]interface{}{ - "enable": true, - "data": identifier[0], - }) - return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, option) - } - return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY) + return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...) } -func (dExt *DriverExt) SwipeTo(direction string, identifier ...string) (err error) { +func (dExt *DriverExt) SwipeTo(direction string, options ...DataOption) (err error) { switch direction { case "up": - return dExt.SwipeUp(identifier...) + return dExt.SwipeUp(options...) case "down": - return dExt.SwipeDown(identifier...) + return dExt.SwipeDown(options...) case "left": - return dExt.SwipeLeft(identifier...) + return dExt.SwipeLeft(options...) case "right": - return dExt.SwipeRight(identifier...) + return dExt.SwipeRight(options...) } return fmt.Errorf("unexpected direction: %s", direction) } -func (dExt *DriverExt) SwipeUp(identifier ...string) (err error) { - return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, identifier...) +func (dExt *DriverExt) SwipeUp(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, options...) } -func (dExt *DriverExt) SwipeDown(identifier ...string) (err error) { - return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, identifier...) +func (dExt *DriverExt) SwipeDown(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, options...) } -func (dExt *DriverExt) SwipeLeft(identifier ...string) (err error) { - return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, identifier...) +func (dExt *DriverExt) SwipeLeft(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, options...) } -func (dExt *DriverExt) SwipeRight(identifier ...string) (err error) { - return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, identifier...) +func (dExt *DriverExt) SwipeRight(options ...DataOption) (err error) { + return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, options...) } // FindCondition indicates the condition to find a UI element diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index d46c34aa..dfe0e487 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -4,19 +4,12 @@ import ( "fmt" ) -func (dExt *DriverExt) TapAbsXY(x, y float64, identifier string) error { +func (dExt *DriverExt) TapAbsXY(x, y float64, options ...DataOption) error { // tap on absolute coordinate [x, y] - if len(identifier) > 0 { - option := WithCustomOption("log", map[string]interface{}{ - "enable": true, - "data": identifier, - }) - return dExt.Driver.TapFloat(x, y, option) - } - return dExt.Driver.TapFloat(x, y) + return dExt.Driver.TapFloat(x, y, options...) } -func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { +func (dExt *DriverExt) TapXY(x, y float64, options ...DataOption) error { // tap on [x, y] percent of window size if x > 1 || y > 1 { return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y) @@ -25,7 +18,7 @@ func (dExt *DriverExt) TapXY(x, y float64, identifier string) error { x = x * float64(dExt.windowSize.Width) y = y * float64(dExt.windowSize.Height) - return dExt.TapAbsXY(x, y, identifier) + return dExt.TapAbsXY(x, y, options...) } func (dExt *DriverExt) GetTextXY(ocrText string, options ...DataOption) (point PointF, err error) { @@ -58,8 +51,8 @@ func (dExt *DriverExt) GetTextXYs(ocrText []string, options ...DataOption) (poin return points, nil } -func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, err error) { - x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, index...) +func (dExt *DriverExt) GetImageXY(imagePath string, options ...DataOption) (point PointF, err error) { + x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, options...) if err != nil { return PointF{}, err } @@ -71,50 +64,69 @@ func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, return point, nil } -func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, options ...DataOption) error { +func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error { + data := map[string]interface{}{} + for _, option := range options { + option(data) + } point, err := dExt.GetTextXY(ocrText, options...) if err != nil { - if ignoreNotFoundError { - return nil + if d, ok := data["ignoreNotFoundError"]; ok { + if b, ok := d.(bool); b && ok { + return nil + } } return err } - return dExt.TapAbsXY(point.X, point.Y, identifier) + return dExt.TapAbsXY(point.X, point.Y, options...) } -func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool, index ...int) error { - point, err := dExt.GetImageXY(imagePath, index...) +func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error { + data := map[string]interface{}{} + for _, option := range options { + option(data) + } + point, err := dExt.GetImageXY(imagePath, options...) if err != nil { - if ignoreNotFoundError { - return nil + if d, ok := data["ignoreNotFoundError"]; ok { + if b, ok := d.(bool); b && ok { + return nil + } } return err } - return dExt.TapAbsXY(point.X, point.Y, identifier) + return dExt.TapAbsXY(point.X, point.Y, options...) } -func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool, index ...int) error { - return dExt.TapOffset(param, 0.5, 0.5, identifier, ignoreNotFoundError, index...) +func (dExt *DriverExt) Tap(param string, options ...DataOption) error { + return dExt.TapOffset(param, 0.5, 0.5, options...) } -func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identifier string, ignoreNotFoundError bool, index ...int) (err error) { +func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, options ...DataOption) (err error) { // click on element, find by name attribute ele, err := dExt.FindUIElement(param) if err == nil { return ele.Click() } - x, y, width, height, err := dExt.FindUIRectInUIKit(param, index...) + data := map[string]interface{}{} + for _, option := range options { + option(data) + } + + x, y, width, height, err := dExt.FindUIRectInUIKit(param, options...) if err != nil { - if ignoreNotFoundError { - return nil + if d, ok := data["ignoreNotFoundError"]; ok { + if b, ok := d.(bool); b && ok { + return nil + } } return err } - return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, identifier) + return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, options...) } func (dExt *DriverExt) DoubleTapXY(x, y float64) error { From 7579c10cb71571f865ba0f85ed897043ee767e63 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Oct 2022 11:47:20 +0800 Subject: [PATCH 155/169] feat: add retry when requesting ocr service --- hrp/pkg/uixt/ocr_vedem.go | 30 +++++++++++++++++++++--------- httprunner/utils.py | 11 ++++++++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 82d142dc..91c73e2f 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -45,7 +45,7 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { if err != nil { return nil, fmt.Errorf("create form file error: %v", err) } - _, err = formWriter.Write(imageBuf) + size, err := formWriter.Write(imageBuf) if err != nil { return nil, fmt.Errorf("write form error: %v", err) } @@ -55,8 +55,9 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { return nil, fmt.Errorf("close body writer error: %v", err) } - if env.VEDEM_OCR_URL == "" { - log.Error().Msg("VEDEM_OCR_URL env missed for OCR service") + if env.VEDEM_OCR_URL == "" || env.VEDEM_OCR_AK == "" || env.VEDEM_OCR_SK == "" { + log.Error().Msg( + "missed env missed for veDEM OCR service: VEDEM_OCR_URL/VEDEM_OCR_AK/VEDEM_OCR_SK") os.Exit(1) } @@ -69,14 +70,25 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { req.Header.Add("Agw-Auth", token) req.Header.Add("Content-Type", bodyWriter.FormDataContentType()) - resp, err := client.Do(req) - if err != nil { - var logID string - if resp != nil { - logID = getLogID(resp.Header) + + var resp *http.Response + // retry 3 times + for i := 1; i <= 3; i++ { + resp, err = client.Do(req) + if err == nil { + break } - return nil, fmt.Errorf("http reqeust veDEM OCR server error: %v, logID: %s", err, logID) + logID := getLogID(resp.Header) + log.Error().Err(err). + Str("logID", logID). + Int("imageBufSize", size). + Msgf("request OCR service failed, retry %d", i) + time.Sleep(1 * time.Second) } + if resp == nil { + return nil, fmt.Errorf("veDEM OCR service is not available") + } + defer resp.Body.Close() results, err := ioutil.ReadAll(resp.Body) diff --git a/httprunner/utils.py b/httprunner/utils.py index b95d3ecb..cbcc8f8e 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -243,7 +243,8 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List): class ExtendJSONEncoder(json.JSONEncoder): - """especially used to safely dump json data with python object, such as MultipartEncoder""" + """especially used to safely dump json data with python object, + such as MultipartEncoder""" def default(self, obj): try: @@ -275,7 +276,8 @@ def is_support_multiprocessing() -> bool: Queue() return True except (ImportError, OSError): - # system that does not support semaphores(dependency of multiprocessing), like Android termux + # system that does not support semaphores + # (dependency of multiprocessing), like Android termux return False @@ -320,7 +322,10 @@ def gen_cartesian_product(*args: List[Dict]) -> List[Dict]: return product_list -LOGGER_FORMAT = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {message}" +LOGGER_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS}" + + " | {level} | {message}" +) def init_logger(level: str): From 0e248882b1d529c92f6d4ddddd4cdc98c251d588 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Mon, 17 Oct 2022 13:45:00 +0800 Subject: [PATCH 156/169] change: update DataOptions --- hrp/pkg/uixt/android_driver.go | 63 ++++++++-------------- hrp/pkg/uixt/android_elment.go | 15 ++---- hrp/pkg/uixt/drag.go | 2 +- hrp/pkg/uixt/ext.go | 36 ++++++------- hrp/pkg/uixt/interface.go | 99 +++++++++++++++++++++++++--------- hrp/pkg/uixt/ios_driver.go | 41 ++++++-------- hrp/pkg/uixt/ios_element.go | 13 ++--- hrp/pkg/uixt/ios_test.go | 2 +- hrp/pkg/uixt/ocr_vedem.go | 37 +++---------- hrp/pkg/uixt/tap.go | 35 ++++-------- 10 files changed, 159 insertions(+), 184 deletions(-) diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index f9f065ce..f4ed98f9 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -491,12 +491,11 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { "x": x, "y": y, } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap") + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "appium/tap") return } @@ -551,16 +550,11 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp "endY": toY, } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["steps"]; !ok { - data["steps"] = 12 // default steps - } - - return ud._drag(data) + return ud._drag(d.Data) } func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...DataOption) (err error) { @@ -572,16 +566,11 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...D "endY": endY, } - // append options in post data for extra uiautomator configurations - // e.g. use WithPressDurationOption to set pressForDuration - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["steps"]; !ok { - data["steps"] = 12 // default steps - } - _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/perform") + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "touch/perform") return } @@ -590,7 +579,7 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...D // per step. So for a 100 steps, the swipe will take about 1/2 second to complete. // `steps` is the number of move steps sent to the system func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { - options = append(options, WithStepsOption(12)) + options = append(options, WithDataSteps(12)) return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } @@ -670,16 +659,11 @@ func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { data := map[string]interface{}{ "text": text, } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["isReplace"]; !ok { - data["isReplace"] = true // default true - } - - _, err = ud.httpPOST(data, "/session", ud.sessionId, "keys") + _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "keys") return } @@ -687,17 +671,16 @@ func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { data := map[string]interface{}{ "view": text, } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) var element WebElement - if valuetext, ok := data["textview"]; ok { + if valuetext, ok := d.Data["textview"]; ok { element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().TextContains(fmt.Sprintf("%v", valuetext)).String()}) - } else if valueid, ok := data["id"]; ok { + } else if valueid, ok := d.Data["id"]; ok { element, err = ud.FindElement(BySelector{ResourceIdID: fmt.Sprintf("%v", valueid)}) - } else if valuedesc, ok := data["description"]; ok { + } else if valuedesc, ok := d.Data["description"]; ok { element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().Description(fmt.Sprintf("%v", valuedesc)).String()}) } else { element, err = ud.FindElement(BySelector{ClassName: ElementType{EditText: true}}) diff --git a/hrp/pkg/uixt/android_elment.go b/hrp/pkg/uixt/android_elment.go index e8fa67b1..60538955 100644 --- a/hrp/pkg/uixt/android_elment.go +++ b/hrp/pkg/uixt/android_elment.go @@ -29,16 +29,11 @@ func (ue uiaElement) SendKeys(text string, options ...DataOption) (err error) { "text": text, } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["isReplace"]; !ok { - data["isReplace"] = true // default true - } - - _, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") + _, err = ue.parent.httpPOST(d.Data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") return } @@ -113,7 +108,7 @@ func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error { func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error { options := []DataOption{ - WithStepsOption(12), + WithDataSteps(12), WithCustomOption("elementId", ue.id), } return ue.parent._swipe(fromX, fromY, toX, toY, options...) diff --git a/hrp/pkg/uixt/drag.go b/hrp/pkg/uixt/drag.go index c049d71b..31d33b1d 100644 --- a/hrp/pkg/uixt/drag.go +++ b/hrp/pkg/uixt/drag.go @@ -26,5 +26,5 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs fromY := y + height*yOffset return dExt.Driver.DragFloat(fromX, fromY, toX, toY, - WithPressDurationOption(pressForDuration[0])) + WithDataPressDuration(pressForDuration[0])) } diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 516eda63..f78279d4 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -383,9 +383,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - identifierOption := WithIdentifierOption(action.Identifier) - indexOption := WithIndexOption(action.Index) - scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + identifierOption := WithDataIdentifier(action.Identifier) + indexOption := WithDataIndex(action.Index) + scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findApp := func(d *DriverExt) error { @@ -423,9 +423,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - identifierOption := WithIdentifierOption(action.Identifier) - indexOption := WithIndexOption(action.Index) - scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + identifierOption := WithDataIdentifier(action.Identifier) + indexOption := WithDataIndex(action.Index) + scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findText := func(d *DriverExt) error { @@ -464,7 +464,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) var point PointF findText := func(d *DriverExt) error { @@ -482,7 +482,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } foundTextAction := func(d *DriverExt) error { // tap text - return d.TapAbsXY(point.X, point.Y, WithIdentifierOption(action.Identifier)) + return d.TapAbsXY(point.X, point.Y, WithDataIdentifier(action.Identifier)) } // default to retry 10 times @@ -520,7 +520,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } x, _ := location[0].(float64) y, _ := location[1].(float64) - return dExt.TapXY(x, y, WithIdentifierOption(action.Identifier)) + return dExt.TapXY(x, y, WithDataIdentifier(action.Identifier)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params) case ACTION_TapAbsXY: @@ -531,12 +531,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } x, _ := location[0].(float64) y, _ := location[1].(float64) - return dExt.TapAbsXY(x, y, WithIdentifierOption(action.Identifier)) + return dExt.TapAbsXY(x, y, WithDataIdentifier(action.Identifier)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params) case ACTION_Tap: if param, ok := action.Params.(string); ok { - return dExt.Tap(param, WithIdentifierOption(action.Identifier), WithIgnoreNotFoundErrorOption(true), WithIndexOption(action.Index)) + return dExt.Tap(param, WithDataIdentifier(action.Identifier), WithDataIgnoreNotFoundError(true), WithDataIndex(action.Index)) } return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params) case ACTION_TapByOCR: @@ -545,16 +545,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - indexOption := WithIndexOption(action.Index) - scopeOption := WithScopeOption(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) - identifierOption := WithIdentifierOption(action.Identifier) - IgnoreNotFoundErrorOption := WithIgnoreNotFoundErrorOption(action.IgnoreNotFoundError) + indexOption := WithDataIndex(action.Index) + scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + identifierOption := WithDataIdentifier(action.Identifier) + IgnoreNotFoundErrorOption := WithDataIgnoreNotFoundError(action.IgnoreNotFoundError) return dExt.TapByOCR(ocrText, identifierOption, IgnoreNotFoundErrorOption, indexOption, scopeOption) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params) case ACTION_TapByCV: if imagePath, ok := action.Params.(string); ok { - return dExt.TapByCV(imagePath, WithIdentifierOption(action.Identifier), WithIgnoreNotFoundErrorOption(true), WithIndexOption(action.Index)) + return dExt.TapByCV(imagePath, WithDataIdentifier(action.Identifier), WithDataIgnoreNotFoundError(true), WithDataIndex(action.Index)) } return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params) case ACTION_DoubleTapXY: @@ -574,7 +574,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params) case ACTION_Swipe: - identifierOption := WithIdentifierOption(action.Identifier) + identifierOption := WithDataIdentifier(action.Identifier) if positions, ok := action.Params.([]interface{}); ok { // relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1] if len(positions) != 4 { @@ -606,7 +606,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { options = append(options, WithCustomOption("description", action.Description)) } if action.Identifier != "" { - options = append(options, WithIdentifierOption(action.Identifier)) + options = append(options, WithDataIdentifier(action.Identifier)) } return dExt.Driver.Input(param, options...) case CtlSleep: diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 1bf107ec..ce07f28a 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -3,6 +3,7 @@ package uixt import ( "bytes" "fmt" + "math" "reflect" "strconv" "strings" @@ -773,62 +774,110 @@ type Rect struct { Size } -type DataOption func(data map[string]interface{}) +type DataOption func(data *DataOptions) func WithCustomOption(key string, value interface{}) DataOption { - return func(data map[string]interface{}) { - data[key] = value + return func(data *DataOptions) { + data.Data[key] = value } } -func WithPressDurationOption(duraion float64) DataOption { - return func(data map[string]interface{}) { - data["duration"] = duraion +func WithDataPressDuration(duration float64) DataOption { + return func(data *DataOptions) { + data.Data["duration"] = duration } } -func WithStepsOption(steps int) DataOption { - return func(data map[string]interface{}) { - data["steps"] = steps +func WithDataSteps(steps int) DataOption { + return func(data *DataOptions) { + data.Data["steps"] = steps } } -func WithFrequencyOption(frequency int) DataOption { - return func(data map[string]interface{}) { - data["frequency"] = frequency +func WithDataFrequency(frequency int) DataOption { + return func(data *DataOptions) { + data.Data["frequency"] = frequency } } -func WithIndexOption(index int) DataOption { - return func(data map[string]interface{}) { - data["index"] = index +func WithDataIndex(index int) DataOption { + return func(data *DataOptions) { + data.Index = index } } -func WithScopeOption(x1, x2, y1, y2 int) DataOption { - return func(data map[string]interface{}) { - data["scope"] = []int{x1, x2, y1, y2} +func WithDataScope(x1, x2, y1, y2 int) DataOption { + return func(data *DataOptions) { + data.Scope = []int{x1, x2, y1, y2} } } -func WithIdentifierOption(identifier string) DataOption { +func WithDataIdentifier(identifier string) DataOption { if identifier == "" { - return func(data map[string]interface{}) {} + return func(data *DataOptions) {} } - return func(data map[string]interface{}) { - data["log"] = map[string]interface{}{ + return func(data *DataOptions) { + data.Data["log"] = map[string]interface{}{ "enable": true, "data": identifier, } } } -func WithIgnoreNotFoundErrorOption(ignoreError bool) DataOption { - return func(data map[string]interface{}) { - data["ignoreNotFoundError"] = ignoreError +func WithDataIgnoreNotFoundError(ignoreError bool) DataOption { + return func(data *DataOptions) { + data.IgnoreNotFoundError = ignoreError } } +type DataOptions struct { + Data map[string]interface{} // configurations used by ios/android driver + Scope []int // used by ocr to get text position in the scope + Index int // index of the target element, should start from 1 + IgnoreNotFoundError bool // ignore error if target element not found +} + +func NewData(options ...DataOption) *DataOptions { + data := &DataOptions{ + Data: map[string]interface{}{}, + } + for _, option := range options { + option(data) + } + + if len(data.Scope) == 0 { + data.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope + } + + if _, ok := data.Data["steps"]; !ok { + data.Data["steps"] = 12 // default steps + } + + if _, ok := data.Data["duration"]; !ok { + data.Data["duration"] = 1.0 // default duration + } + + if _, ok := data.Data["frequency"]; !ok { + data.Data["frequency"] = 60 // default frequency + } + + if _, ok := data.Data["isReplace"]; !ok { + data.Data["isReplace"] = true // default true + } + + return data +} + +func (d *DataOptions) MergeData(data map[string]interface{}) { + for key, value := range data { + d.Data[key] = value + } +} + +func (d *DataOptions) AddData(key string, value interface{}) { + d.Data[key] = value +} + // current implemeted device: IOSDevice, AndroidDevice type Device interface { UUID() string diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index e0a97c6f..e9593fbf 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -379,12 +379,11 @@ func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { "x": x, "y": y, } - // append options in post data for extra WDA configurations - // e.g. add identifier in tap event logs - for _, option := range options { - option(data) - } - _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/tap/0") + // new data options in post data for extra WDA configurations + d := NewData(options...) + d.MergeData(data) + + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/tap/0") return } @@ -433,26 +432,21 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp "toY": toY, } - // append options in post data for extra WDA configurations - // e.g. use WithPressDurationOption to set pressForDuration - for _, option := range options { - option(data) - } + // new data options in post data for extra WDA configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["duration"]; !ok { - data["duration"] = 1.0 // default duration - } - _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration") + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/dragfromtoforduration") return } func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { - options = append(options, WithPressDurationOption(0)) + options = append(options, WithDataPressDuration(0)) return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { - options = append(options, WithPressDurationOption(0)) + options = append(options, WithDataPressDuration(0)) return wd.DragFloat(fromX, fromY, toX, toY, options...) } @@ -514,16 +508,11 @@ func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) { // [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] data := map[string]interface{}{"value": strings.Split(text, "")} - // append options in post data for extra WDA configurations - // e.g. use WithFrequency to set frequency of typing - for _, option := range options { - option(data) - } + // new data options in post data for extra WDA configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["frequency"]; !ok { - data["frequency"] = 60 // default frequency - } - _, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keys") + _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/keys") return } diff --git a/hrp/pkg/uixt/ios_element.go b/hrp/pkg/uixt/ios_element.go index 74006479..e4ca6300 100644 --- a/hrp/pkg/uixt/ios_element.go +++ b/hrp/pkg/uixt/ios_element.go @@ -30,16 +30,11 @@ func (we wdaElement) SendKeys(text string, options ...DataOption) (err error) { data := map[string]interface{}{ "value": strings.Split(text, ""), } - // append options in post data for extra uiautomator configurations - for _, option := range options { - option(data) - } + // new data options in post data for extra uiautomator configurations + d := NewData(options...) + d.MergeData(data) - if _, ok := data["frequency"]; !ok { - data["frequency"] = 60 - } - - _, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/value") + _, err = we.parent.httpPOST(d.Data, "/session", we.parent.sessionId, "/element", we.id, "/value") return } diff --git a/hrp/pkg/uixt/ios_test.go b/hrp/pkg/uixt/ios_test.go index a635e305..07377317 100644 --- a/hrp/pkg/uixt/ios_test.go +++ b/hrp/pkg/uixt/ios_test.go @@ -443,7 +443,7 @@ func Test_remoteWD_TouchAndHold(t *testing.T) { func Test_remoteWD_Drag(t *testing.T) { setup(t) - // err := driver.Drag(200, 300, 200, 500, WithPressDurationOption(0.5)) + // err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5)) err := driver.Swipe(200, 300, 200, 500) if err != nil { t.Fatal(err) diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 96a59d43..cca5ea7d 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -5,7 +5,6 @@ import ( "fmt" "image" "io/ioutil" - "math" "mime/multipart" "net/http" "os" @@ -111,20 +110,7 @@ func getLogID(header http.Header) string { } func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...DataOption) (rect image.Rectangle, err error) { - data := map[string]interface{}{} - for _, option := range options { - option(data) - } - - if _, ok := data["index"]; !ok { - data["index"] = 0 // index not specified - } - index, _ := data["index"].(int) - - if _, ok := data["scope"]; !ok { - data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified - } - scope, _ := data["scope"].([]int) + data := NewData(options...) ocrResults, err := s.getOCRResult(imageBuf) if err != nil { @@ -146,7 +132,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data Y: int(ocrResult.Points[2].Y), }, } - if rect.Min.X > scope[0] && rect.Max.X < scope[2] && rect.Min.Y > scope[1] && rect.Max.Y < scope[3] { + if rect.Min.X > data.Scope[0] && rect.Max.X < data.Scope[2] && rect.Min.Y > data.Scope[1] && rect.Max.Y < data.Scope[3] { ocrTexts = append(ocrTexts, ocrResult.Text) // not contains text @@ -163,7 +149,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data } // match exactly, and not specify index, return the first one - if index == 0 { + if data.Index == 0 { return rect, nil } } @@ -174,7 +160,7 @@ func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...Data } // get index - idx := index + idx := data.Index if idx > 0 { // NOTICE: index start from 1 idx = idx - 1 @@ -198,20 +184,11 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... return } - data := map[string]interface{}{} - for _, option := range options { - option(data) - } - - if _, ok := data["scope"]; !ok { - data["scope"] = []int{0, 0, math.MaxInt64, math.MaxInt64} // scope not specified - } - scope, _ := data["scope"].([]int) + data := NewData(options...) + ocrTexts := map[string]bool{} var success bool var rect image.Rectangle - ocrTexts := map[string]bool{} - for _, text := range texts { var found bool for _, ocrResult := range ocrResults { @@ -227,7 +204,7 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... }, } - if rect.Min.X >= scope[0] && rect.Max.X <= scope[2] && rect.Min.Y >= scope[1] && rect.Max.Y <= scope[3] { + if rect.Min.X >= data.Scope[0] && rect.Max.X <= data.Scope[2] && rect.Min.Y >= data.Scope[1] && rect.Max.Y <= data.Scope[3] { ocrTexts[ocrResult.Text] = true // not contains text diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index dfe0e487..d7a642a2 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -65,16 +65,12 @@ func (dExt *DriverExt) GetImageXY(imagePath string, options ...DataOption) (poin } func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error { - data := map[string]interface{}{} - for _, option := range options { - option(data) - } + data := NewData(options...) + point, err := dExt.GetTextXY(ocrText, options...) if err != nil { - if d, ok := data["ignoreNotFoundError"]; ok { - if b, ok := d.(bool); b && ok { - return nil - } + if data.IgnoreNotFoundError { + return nil } return err } @@ -83,16 +79,12 @@ func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error { } func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error { - data := map[string]interface{}{} - for _, option := range options { - option(data) - } + data := NewData(options...) + point, err := dExt.GetImageXY(imagePath, options...) if err != nil { - if d, ok := data["ignoreNotFoundError"]; ok { - if b, ok := d.(bool); b && ok { - return nil - } + if data.IgnoreNotFoundError { + return nil } return err } @@ -111,17 +103,12 @@ func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, options return ele.Click() } - data := map[string]interface{}{} - for _, option := range options { - option(data) - } + data := NewData(options...) x, y, width, height, err := dExt.FindUIRectInUIKit(param, options...) if err != nil { - if d, ok := data["ignoreNotFoundError"]; ok { - if b, ok := d.(bool); b && ok { - return nil - } + if data.IgnoreNotFoundError { + return nil } return err } From 317ec4fde80df5daf72ac1de6fe3ed2e79eed488 Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Mon, 17 Oct 2022 15:28:46 +0800 Subject: [PATCH 157/169] change: update NewData function --- hrp/pkg/uixt/android_driver.go | 15 +++++---------- hrp/pkg/uixt/android_elment.go | 3 +-- hrp/pkg/uixt/interface.go | 14 ++------------ hrp/pkg/uixt/ios_driver.go | 9 +++------ hrp/pkg/uixt/ios_element.go | 3 +-- hrp/pkg/uixt/ocr_vedem.go | 4 ++-- hrp/pkg/uixt/tap.go | 6 +++--- 7 files changed, 17 insertions(+), 37 deletions(-) diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index f4ed98f9..3f1bdea0 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -492,8 +492,7 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { "y": y, } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "appium/tap") return @@ -551,8 +550,7 @@ func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) return ud._drag(d.Data) } @@ -567,8 +565,7 @@ func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...D } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "touch/perform") return @@ -660,8 +657,7 @@ func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) { "text": text, } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = ud.httpPOST(d.Data, "/session", ud.sessionId, "keys") return @@ -672,8 +668,7 @@ func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) { "view": text, } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) var element WebElement if valuetext, ok := d.Data["textview"]; ok { diff --git a/hrp/pkg/uixt/android_elment.go b/hrp/pkg/uixt/android_elment.go index 60538955..9d45cd39 100644 --- a/hrp/pkg/uixt/android_elment.go +++ b/hrp/pkg/uixt/android_elment.go @@ -30,8 +30,7 @@ func (ue uiaElement) SendKeys(text string, options ...DataOption) (err error) { } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = ue.parent.httpPOST(d.Data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") return diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index ce07f28a..f922ee00 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -837,9 +837,9 @@ type DataOptions struct { IgnoreNotFoundError bool // ignore error if target element not found } -func NewData(options ...DataOption) *DataOptions { +func NewData(d map[string]interface{}, options ...DataOption) *DataOptions { data := &DataOptions{ - Data: map[string]interface{}{}, + Data: d, } for _, option := range options { option(data) @@ -868,16 +868,6 @@ func NewData(options ...DataOption) *DataOptions { return data } -func (d *DataOptions) MergeData(data map[string]interface{}) { - for key, value := range data { - d.Data[key] = value - } -} - -func (d *DataOptions) AddData(key string, value interface{}) { - d.Data[key] = value -} - // current implemeted device: IOSDevice, AndroidDevice type Device interface { UUID() string diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index e9593fbf..9db911e8 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -380,8 +380,7 @@ func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) { "y": y, } // new data options in post data for extra WDA configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/tap/0") return @@ -433,8 +432,7 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOp } // new data options in post data for extra WDA configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/dragfromtoforduration") return @@ -509,8 +507,7 @@ func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) { data := map[string]interface{}{"value": strings.Split(text, "")} // new data options in post data for extra WDA configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = wd.httpPOST(d.Data, "/session", wd.sessionId, "/wda/keys") return diff --git a/hrp/pkg/uixt/ios_element.go b/hrp/pkg/uixt/ios_element.go index e4ca6300..9e2208b5 100644 --- a/hrp/pkg/uixt/ios_element.go +++ b/hrp/pkg/uixt/ios_element.go @@ -31,8 +31,7 @@ func (we wdaElement) SendKeys(text string, options ...DataOption) (err error) { "value": strings.Split(text, ""), } // new data options in post data for extra uiautomator configurations - d := NewData(options...) - d.MergeData(data) + d := NewData(data, options...) _, err = we.parent.httpPOST(d.Data, "/session", we.parent.sessionId, "/element", we.id, "/value") return diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index cca5ea7d..f1b58366 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -110,7 +110,7 @@ func getLogID(header http.Header) string { } func (s *veDEMOCRService) FindText(text string, imageBuf []byte, options ...DataOption) (rect image.Rectangle, err error) { - data := NewData(options...) + data := NewData(map[string]interface{}{}, options...) ocrResults, err := s.getOCRResult(imageBuf) if err != nil { @@ -184,7 +184,7 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... return } - data := NewData(options...) + data := NewData(map[string]interface{}{}, options...) ocrTexts := map[string]bool{} var success bool diff --git a/hrp/pkg/uixt/tap.go b/hrp/pkg/uixt/tap.go index d7a642a2..1629f137 100644 --- a/hrp/pkg/uixt/tap.go +++ b/hrp/pkg/uixt/tap.go @@ -65,7 +65,7 @@ func (dExt *DriverExt) GetImageXY(imagePath string, options ...DataOption) (poin } func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error { - data := NewData(options...) + data := NewData(map[string]interface{}{}, options...) point, err := dExt.GetTextXY(ocrText, options...) if err != nil { @@ -79,7 +79,7 @@ func (dExt *DriverExt) TapByOCR(ocrText string, options ...DataOption) error { } func (dExt *DriverExt) TapByCV(imagePath string, options ...DataOption) error { - data := NewData(options...) + data := NewData(map[string]interface{}{}, options...) point, err := dExt.GetImageXY(imagePath, options...) if err != nil { @@ -103,7 +103,7 @@ func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, options return ele.Click() } - data := NewData(options...) + data := NewData(map[string]interface{}{}, options...) x, y, width, height, err := dExt.FindUIRectInUIKit(param, options...) if err != nil { From 9b0d1a4ed0e72c89d5a7a9afb3a49bc658799a1f Mon Sep 17 00:00:00 2001 From: "xucong.053" Date: Mon, 17 Oct 2022 16:23:18 +0800 Subject: [PATCH 158/169] feat: add wait time between swipe and ocr --- hrp/pkg/uixt/ext.go | 17 ++++++++++++----- hrp/pkg/uixt/swipe.go | 5 ++++- hrp/step.go | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index f78279d4..39438cb1 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -66,6 +66,7 @@ type MobileAction struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times + WaitTime float64 `json:"wait_time,omitempty" yaml:"wait_time,omitempty"` // wait time between swipe and ocr, unit: second Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app Scope []float64 `json:"scope,omitempty" yaml:"scope,omitempty"` // used by ocr to get text position in the scope Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1 @@ -90,6 +91,12 @@ func WithIndex(index int) ActionOption { } } +func WithWaitTime(sec float64) ActionOption { + return func(o *MobileAction) { + o.WaitTime = sec + } +} + // WithDirection inputs direction (up, down, left, right) func WithDirection(direction string) ActionOption { return func(o *MobileAction) { @@ -413,7 +420,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.MaxRetryTimes = 5 } // swipe next screen until app found - return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes) + return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes, action.WaitTime) } return fmt.Errorf("invalid %s params, should be app name(string), got %v", ACTION_SwipeToTapApp, action.Params) @@ -444,10 +451,10 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) + return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) } return fmt.Errorf("invalid %s params, should be app text(string), got %v", ACTION_SwipeToTapText, action.Params) @@ -491,10 +498,10 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes) + return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes) + return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) } return fmt.Errorf("invalid %s params, should be app text([]string), got %v", ACTION_SwipeToTapText, action.Params) diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index 41a11d2a..b52bd770 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -2,6 +2,7 @@ package uixt import ( "fmt" + "time" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/rs/zerolog/log" @@ -66,7 +67,7 @@ type FindCondition func(driver *DriverExt) error // FoundAction indicates the action to do after a UI element is found type FoundAction func(driver *DriverExt) error -func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition, action FoundAction, maxTimes int) error { +func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition, action FoundAction, maxTimes int, waitTime float64) error { for i := 0; i < maxTimes; i++ { if err := condition(dExt); err == nil { // do action after found @@ -89,6 +90,8 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition log.Error().Err(err).Msgf("swipe (%v, %v) to (%v, %v) failed", sx, sy, ex, ey) } } + // wait for swipe action to completed and content to load completely + time.Sleep(time.Duration(1000*waitTime) * time.Millisecond) } return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) } diff --git a/hrp/step.go b/hrp/step.go index 1f3cb5ee..b721faea 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -23,6 +23,7 @@ const ( var ( WithIdentifier = uixt.WithIdentifier WithMaxRetryTimes = uixt.WithMaxRetryTimes + WithWaitTime = uixt.WithWaitTime WithIndex = uixt.WithIndex WithTimeout = uixt.WithTimeout WithIgnoreNotFoundError = uixt.WithIgnoreNotFoundError From d56597dc628fe9f0a4f9aad2adf14d0b67db2f54 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Oct 2022 16:52:25 +0800 Subject: [PATCH 159/169] fix merge --- hrp/internal/code/code.go | 61 ++++++++++++++++++++++++++++++++++ hrp/internal/code/code_test.go | 12 +++++++ 2 files changed, 73 insertions(+) create mode 100644 hrp/internal/code/code.go create mode 100644 hrp/internal/code/code_test.go diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go new file mode 100644 index 00000000..ef009358 --- /dev/null +++ b/hrp/internal/code/code.go @@ -0,0 +1,61 @@ +package code + +import ( + "github.com/pkg/errors" +) + +// general: [0, 20) +const ( + SUCCESS = 0 + FAIL = 1 +) + +// loader: [20, 40) +var ( + LoadError = errors.New("load error") // 20 + LoadJSONError = errors.New("load json error") // 21 + LoadYAMLError = errors.New("load yaml error") // 22 +) + +// parser: [40, 60) + +// runner: [60, 100) + +// ios related: [100, 120) +var ( + IOSScreenShotError = errors.New("ios screenshot error") // 100 +) + +// android related: [120, 140) + +// OCR related: [140, 160) +var ( + OCREnvMissedError = errors.New("veDEM OCR env missed error") // 140 + OCRRequestError = errors.New("vedem ocr prepare request error") // 141 + OCRServiceConnectionError = errors.New("vedem ocr service connect error") // 142 + OCRResponseStatusCodeNot200 = errors.New("vedem ocr response status code is not 200") // 143 + OCRResponseError = errors.New("vedem ocr parse response error") // 143 + OCRTextNotFoundError = errors.New("vedem ocr text not found") // 144 +) + +// CV related: [160, 180) + +// report related: [200, 220) + +var errorsMap = map[error]int{ + LoadJSONError: 10, + LoadYAMLError: 11, +} + +func GetErrorCode(err error) int { + if err == nil { + return SUCCESS + } + + e := errors.Cause(err) + if code, ok := errorsMap[e]; ok { + return code + } + + return FAIL +} diff --git a/hrp/internal/code/code_test.go b/hrp/internal/code/code_test.go new file mode 100644 index 00000000..bbf538bf --- /dev/null +++ b/hrp/internal/code/code_test.go @@ -0,0 +1,12 @@ +package code + +import ( + "fmt" + "testing" +) + +func TestGetErrorCode(t *testing.T) { + err := LoadYAMLError + code := GetErrorCode(err) + fmt.Println(code) +} From d06694312edd5e981a8a9c39e78970fd7a53f034 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Oct 2022 20:02:39 +0800 Subject: [PATCH 160/169] feat: swipeToTapApp --- hrp/pkg/uixt/ext.go | 79 ++++++++++++--------------------- hrp/pkg/uixt/interface.go | 57 +++++++++++++++--------- hrp/pkg/uixt/swipe.go | 91 +++++++++++++++++++++++++++++++++----- hrp/pkg/uixt/swipe_test.go | 8 ++-- hrp/pkg/uixt/tap_test.go | 6 +-- 5 files changed, 153 insertions(+), 88 deletions(-) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 39438cb1..f7b353bf 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -386,45 +386,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { AppLaunchUnattached, action.Params) case ACTION_SwipeToTapApp: if appName, ok := action.Params.(string); ok { - if len(action.Scope) != 4 { - action.Scope = []float64{0, 0, 1, 1} - } - - identifierOption := WithDataIdentifier(action.Identifier) - indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) - - var point PointF - findApp := func(d *DriverExt) error { - var err error - point, err = d.GetTextXY(appName, scopeOption, indexOption) - return err - } - foundAppAction := func(d *DriverExt) error { - // click app to launch - return d.TapAbsXY(point.X, point.Y-25, identifierOption) - } - - // go to home screen - if err := dExt.Driver.Homescreen(); err != nil { - return errors.Wrap(err, "go to home screen failed") - } - - // swipe to first screen - for i := 0; i < 5; i++ { - dExt.SwipeRight() - } - - // default to retry 5 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 5 - } - // swipe next screen until app found - return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes, action.WaitTime) + return dExt.swipeToTapApp(appName, action) } return fmt.Errorf("invalid %s params, should be app name(string), got %v", ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: + // TODO: merge to LoopUntil if text, ok := action.Params.(string); ok { if len(action.Scope) != 4 { action.Scope = []float64{0, 0, 1, 1} @@ -432,10 +399,20 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { identifierOption := WithDataIdentifier(action.Identifier) indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) var point PointF - findText := func(d *DriverExt) error { + // findTextAction := func(d *DriverExt) error { + // return nil + // } + findTextCondition := func(d *DriverExt) error { var err error point, err = d.GetTextXY(text, indexOption, scopeOption) return err @@ -445,20 +422,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return d.TapAbsXY(point.X, point.Y, identifierOption) } - // default to retry 10 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 10 - } - if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil(action.Direction, findTextCondition, foundTextAction, maxRetryOption, waitTimeOption) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil("up", findTextCondition, foundTextAction, maxRetryOption, waitTimeOption) } return fmt.Errorf("invalid %s params, should be app text(string), got %v", ACTION_SwipeToTapText, action.Params) case ACTION_SwipeToTapTexts: + // TODO: merge to LoopUntil if texts, ok := action.Params.([]interface{}); ok { var textList []string for _, t := range texts { @@ -471,10 +444,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) var point PointF - findText := func(d *DriverExt) error { + findTexts := func(d *DriverExt) error { var err error points, err := d.GetTextXYs(texts, scopeOption) if err != nil { @@ -498,10 +477,10 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil(action.Direction, findTexts, foundTextAction, maxRetryOption, waitTimeOption) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil("up", findTexts, foundTextAction, maxRetryOption, waitTimeOption) } return fmt.Errorf("invalid %s params, should be app text([]string), got %v", ACTION_SwipeToTapText, action.Params) @@ -553,7 +532,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) identifierOption := WithDataIdentifier(action.Identifier) IgnoreNotFoundErrorOption := WithDataIgnoreNotFoundError(action.IgnoreNotFoundError) return dExt.TapByOCR(ocrText, identifierOption, IgnoreNotFoundErrorOption, indexOption, scopeOption) @@ -648,7 +627,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return nil } -func (dExt *DriverExt) GetAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) { +func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) { return int(x1 * float64(dExt.windowSize.Width) * dExt.scale), int(y1 * float64(dExt.windowSize.Height) * dExt.scale), int(x2 * float64(dExt.windowSize.Width) * dExt.scale), diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index f922ee00..547197a4 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -774,6 +774,15 @@ type Rect struct { Size } +type DataOptions struct { + Data map[string]interface{} // configurations used by ios/android driver + Scope []int // used by ocr to get text position in the scope + Index int // index of the target element, should start from 1 + IgnoreNotFoundError bool // ignore error if target element not found + MaxRetryTimes int // max retry times if target element not found + Interval float64 // interval between retries in seconds +} + type DataOption func(data *DataOptions) func WithCustomOption(key string, value interface{}) DataOption { @@ -830,42 +839,50 @@ func WithDataIgnoreNotFoundError(ignoreError bool) DataOption { } } -type DataOptions struct { - Data map[string]interface{} // configurations used by ios/android driver - Scope []int // used by ocr to get text position in the scope - Index int // index of the target element, should start from 1 - IgnoreNotFoundError bool // ignore error if target element not found +func WithDataMaxRetryTimes(maxRetryTimes int) DataOption { + return func(data *DataOptions) { + data.MaxRetryTimes = maxRetryTimes + } } -func NewData(d map[string]interface{}, options ...DataOption) *DataOptions { - data := &DataOptions{ - Data: d, +func WithDataWaitTime(sec float64) DataOption { + return func(data *DataOptions) { + data.Interval = sec + } +} + +func NewData(data map[string]interface{}, options ...DataOption) *DataOptions { + if data == nil { + data = make(map[string]interface{}) + } + dataOptions := &DataOptions{ + Data: data, } for _, option := range options { - option(data) + option(dataOptions) } - if len(data.Scope) == 0 { - data.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope + if len(dataOptions.Scope) == 0 { + dataOptions.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope } - if _, ok := data.Data["steps"]; !ok { - data.Data["steps"] = 12 // default steps + if _, ok := dataOptions.Data["steps"]; !ok { + dataOptions.Data["steps"] = 12 // default steps } - if _, ok := data.Data["duration"]; !ok { - data.Data["duration"] = 1.0 // default duration + if _, ok := dataOptions.Data["duration"]; !ok { + dataOptions.Data["duration"] = 1.0 // default duration } - if _, ok := data.Data["frequency"]; !ok { - data.Data["frequency"] = 60 // default frequency + if _, ok := dataOptions.Data["frequency"]; !ok { + dataOptions.Data["frequency"] = 60 // default frequency } - if _, ok := data.Data["isReplace"]; !ok { - data.Data["isReplace"] = true // default true + if _, ok := dataOptions.Data["isReplace"]; !ok { + dataOptions.Data["isReplace"] = true // default true } - return data + return dataOptions } // current implemeted device: IOSDevice, AndroidDevice diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index b52bd770..bd1aedf9 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -4,8 +4,10 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) func assertRelative(p float64) bool { @@ -61,17 +63,19 @@ func (dExt *DriverExt) SwipeRight(options ...DataOption) (err error) { return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, options...) } -// FindCondition indicates the condition to find a UI element -type FindCondition func(driver *DriverExt) error +type Action func(driver *DriverExt) error -// FoundAction indicates the action to do after a UI element is found -type FoundAction func(driver *DriverExt) error +// findCondition indicates the condition to find a UI element +// foundAction indicates the action to do after a UI element is found +func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, foundAction Action, options ...DataOption) error { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.Interval -func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition, action FoundAction, maxTimes int, waitTime float64) error { - for i := 0; i < maxTimes; i++ { - if err := condition(dExt); err == nil { + for i := 0; i < maxRetryTimes; i++ { + if err := findCondition(dExt); err == nil { // do action after found - return action(dExt) + return foundAction(dExt) } if d, ok := direction.(string); ok { if err := dExt.SwipeTo(d); err != nil { @@ -91,7 +95,72 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition } } // wait for swipe action to completed and content to load completely - time.Sleep(time.Duration(1000*waitTime) * time.Millisecond) + time.Sleep(time.Duration(1000*interval) * time.Millisecond) } - return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) + return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxRetryTimes) +} + +func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.Interval + + for i := 0; i < maxRetryTimes; i++ { + if err := findCondition(dExt); err == nil { + // do action after found + return foundAction(dExt) + } + + if err := findAction(dExt); err != nil { + log.Error().Err(err).Msgf("find action failed") + } + + // wait interval between each findAction + time.Sleep(time.Duration(1000*interval) * time.Millisecond) + } + return fmt.Errorf("loop %d times, match find condition failed", maxRetryTimes) +} + +func (dExt *DriverExt) swipeToTapApp(appName string, action MobileAction) error { + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + identifierOption := WithDataIdentifier(action.Identifier) + indexOption := WithDataIndex(action.Index) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + + // default to retry 5 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 5 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) + + var point PointF + findAppAction := func(d *DriverExt) error { + return dExt.SwipeLeft() + } + findAppCondition := func(d *DriverExt) error { + var err error + point, err = d.GetTextXY(appName, scopeOption, indexOption) + return err + } + foundAppAction := func(d *DriverExt) error { + // click app to launch + return d.TapAbsXY(point.X, point.Y-25, identifierOption) + } + + // go to home screen + if err := dExt.Driver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + dExt.SwipeRight() + } + + // swipe next screen until app found + return dExt.LoopUntil(findAppAction, findAppCondition, foundAppAction, maxRetryOption, waitTimeOption) } diff --git a/hrp/pkg/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go index 83502a8f..22950c44 100644 --- a/hrp/pkg/uixt/swipe_test.go +++ b/hrp/pkg/uixt/swipe_test.go @@ -18,7 +18,7 @@ func TestSwipeUntil(t *testing.T) { } foundAppAction := func(d *DriverExt) error { // click app, launch douyin - return d.TapAbsXY(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y) } driverExt.Driver.Homescreen() @@ -29,7 +29,7 @@ func TestSwipeUntil(t *testing.T) { } // swipe until app found - err = driverExt.SwipeUntil("left", findApp, foundAppAction, 10) + err = driverExt.SwipeUntil("left", findApp, foundAppAction, WithDataMaxRetryTimes(10)) checkErr(t, err) findLive := func(d *DriverExt) error { @@ -39,10 +39,10 @@ func TestSwipeUntil(t *testing.T) { } foundLiveAction := func(d *DriverExt) error { // enter live room - return d.TapAbsXY(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y) } // swipe until live room found - err = driverExt.SwipeUntil("up", findLive, foundLiveAction, 20) + err = driverExt.SwipeUntil("up", findLive, foundLiveAction, WithDataMaxRetryTimes(20)) checkErr(t, err) } diff --git a/hrp/pkg/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go index b1bf9ee9..c5365998 100644 --- a/hrp/pkg/uixt/tap_test.go +++ b/hrp/pkg/uixt/tap_test.go @@ -29,7 +29,7 @@ func TestDriverExt_TapXY(t *testing.T) { driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) - err = driverExt.TapXY(0.4, 0.5, "") + err = driverExt.TapXY(0.4, 0.5) checkErr(t, err) } @@ -37,7 +37,7 @@ func TestDriverExt_TapAbsXY(t *testing.T) { driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) - err = driverExt.TapAbsXY(100, 300, "") + err = driverExt.TapAbsXY(100, 300) checkErr(t, err) } @@ -46,6 +46,6 @@ func TestDriverExt_TapWithOCR(t *testing.T) { checkErr(t, err) // 需要点击文字上方的图标 - err = driverExt.TapOffset("抖音", 0.5, -1, "", false) + err = driverExt.TapOffset("抖音", 0.5, -1) checkErr(t, err) } From 792605a1dc0c065a79b9a9cad675e0cfa580c27e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Oct 2022 21:33:15 +0800 Subject: [PATCH 161/169] fix: exit with code --- hrp/cmd/cli/main.go | 3 ++- hrp/cmd/root.go | 11 +++++----- hrp/cmd/run.go | 9 ++------ hrp/internal/code/code.go | 39 +++++++++++++++++++++++----------- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/android_driver.go | 10 ++++++--- hrp/pkg/uixt/ios_device.go | 2 +- hrp/pkg/uixt/ocr_vedem.go | 12 +++++++---- hrp/pkg/uixt/swipe.go | 8 +++++-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 11 files changed, 62 insertions(+), 38 deletions(-) diff --git a/hrp/cmd/cli/main.go b/hrp/cmd/cli/main.go index 63f5c9bd..453891a1 100644 --- a/hrp/cmd/cli/main.go +++ b/hrp/cmd/cli/main.go @@ -1,6 +1,7 @@ package main import ( + "os" "time" "github.com/getsentry/sentry-go" @@ -20,5 +21,5 @@ func main() { } }() - cmd.Execute() + os.Exit(cmd.Execute()) } diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index 616fd0ab..2cc888e8 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp/cmd/adb" "github.com/httprunner/httprunner/v4/hrp/cmd/ios" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -45,7 +46,8 @@ Copyright 2017 debugtalk`, } }, Version: version.VERSION, - TraverseChildren: true, + TraverseChildren: true, // parses flags on all parents before executing child command + SilenceUsage: true, // silence usage when an error occurs } var ( @@ -56,7 +58,7 @@ var ( // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { +func Execute() int { rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "INFO", "set log level") rootCmd.PersistentFlags().BoolVar(&logJSON, "log-json", false, "set log to json format") rootCmd.PersistentFlags().StringVar(&venv, "venv", "", "specify python3 venv path") @@ -64,9 +66,8 @@ func Execute() { ios.Init(rootCmd) adb.Init(rootCmd) - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } + err := rootCmd.Execute() + return code.GetErrorCode(err) } func setLogLevel(level string) { diff --git a/hrp/cmd/run.go b/hrp/cmd/run.go index c628f93a..5a7b4f8e 100644 --- a/hrp/cmd/run.go +++ b/hrp/cmd/run.go @@ -1,8 +1,6 @@ package cmd import ( - "os" - "github.com/spf13/cobra" "github.com/httprunner/httprunner/v4/hrp" @@ -20,17 +18,14 @@ var runCmd = &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { var paths []hrp.ITestCase for _, arg := range args { path := hrp.TestCasePath(arg) paths = append(paths, &path) } runner := makeHRPRunner() - err := runner.Run(paths...) - if err != nil { - os.Exit(1) - } + return runner.Run(paths...) }, } diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index ef009358..37d8d1ce 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -21,30 +21,45 @@ var ( // runner: [60, 100) -// ios related: [100, 120) +// ios related: [100, 130) var ( - IOSScreenShotError = errors.New("ios screenshot error") // 100 + IOSScreenShotError = errors.New("ios screenshot error") // 110 ) -// android related: [120, 140) - -// OCR related: [140, 160) +// android related: [130, 160) var ( - OCREnvMissedError = errors.New("veDEM OCR env missed error") // 140 - OCRRequestError = errors.New("vedem ocr prepare request error") // 141 - OCRServiceConnectionError = errors.New("vedem ocr service connect error") // 142 - OCRResponseStatusCodeNot200 = errors.New("vedem ocr response status code is not 200") // 143 - OCRResponseError = errors.New("vedem ocr parse response error") // 143 - OCRTextNotFoundError = errors.New("vedem ocr text not found") // 144 + AndroidScreenShotError = errors.New("android screenshot error") // 150 ) -// CV related: [160, 180) +// OCR related: [160, 180) +var ( + OCREnvMissedError = errors.New("veDEM OCR env missed error") // 160 + OCRRequestError = errors.New("vedem ocr prepare request error") // 161 + OCRServiceConnectionError = errors.New("vedem ocr service connect error") // 162 + OCRResponseError = errors.New("vedem ocr parse response error") // 163 + OCRTextNotFoundError = errors.New("vedem ocr text not found") // 164 +) + +// CV related: [180, 200) // report related: [200, 220) var errorsMap = map[error]int{ LoadJSONError: 10, LoadYAMLError: 11, + + // ios related + IOSScreenShotError: 110, + + // android related + AndroidScreenShotError: 130, + + // OCR related + OCREnvMissedError: 160, + OCRRequestError: 161, + OCRServiceConnectionError: 162, + OCRResponseError: 163, + OCRTextNotFoundError: 164, } func GetErrorCode(err error) int { diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index a28eeaba..bd879d79 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10161439 \ No newline at end of file +v4.3.0-beta-10172144 \ No newline at end of file diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index 3f1bdea0..526bfc32 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base64" "encoding/json" - "errors" "fmt" "net" "net/url" @@ -13,7 +12,10 @@ import ( "time" "github.com/electricbubble/gadb" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/code" ) var errDriverNotImplemented = errors.New("driver method not implemented") @@ -860,7 +862,8 @@ func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) var rawResp rawResponse if rawResp, err = ud.httpGET("/session", ud.sessionId, "screenshot"); err != nil { - return nil, err + return nil, errors.Wrap(code.AndroidScreenShotError, + fmt.Sprintf("get UIA screenshot data failed: %v", err)) } reply := new(struct{ Value string }) if err = json.Unmarshal(rawResp, reply); err != nil { @@ -869,7 +872,8 @@ func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { var decodeStr []byte if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { - return nil, err + return nil, errors.Wrap(code.AndroidScreenShotError, + fmt.Sprintf("decode UIA screenshot data failed: %v", err)) } raw = bytes.NewBuffer(decodeStr) diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index b02a34db..0b2a4d50 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -34,7 +34,7 @@ const ( // It may help to prevent out of memory or timeout errors while getting the elements source tree, // but it might restrict the depth of source tree. // A part of elements source tree might be lost if the value was too small. Defaults to 50 - snapshotMaxDepth = 16 + snapshotMaxDepth = 10 // Allows to customize accept/dismiss alert button selector. // It helps you to handle an arbitrary element as accept button in accept alert command. // The selector should be a valid class chain expression, where the search root is the alert element itself. diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 1a2dc00f..1bae01ad 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -96,7 +96,11 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { if err == nil { break } - logID := getLogID(resp.Header) + + var logID string + if resp != nil { + logID = getLogID(resp.Header) + } log.Error().Err(err). Str("logID", logID). Int("imageBufSize", size). @@ -116,7 +120,7 @@ func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) { } if resp.StatusCode != http.StatusOK { - return nil, errors.Wrap(code.OCRResponseStatusCodeNot200, + return nil, errors.Wrap(code.OCRResponseError, fmt.Sprintf("unexpected response status code: %d, results: %v", resp.StatusCode, string(results))) } @@ -258,8 +262,8 @@ func (s *veDEMOCRService) FindTexts(texts []string, imageBuf []byte, options ... } if !success { - return rects, - fmt.Errorf("texts %s not found in %v", texts, ocrTexts) + return rects, errors.Wrap(code.OCRTextNotFoundError, + fmt.Sprintf("texts %s not found in %v", texts, ocrTexts)) } return rects, nil diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index bd1aedf9..a7187045 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -8,6 +8,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/code" ) func assertRelative(p float64) bool { @@ -97,7 +98,8 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, f // wait for swipe action to completed and content to load completely time.Sleep(time.Duration(1000*interval) * time.Millisecond) } - return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxRetryTimes) + return errors.Wrap(code.OCRTextNotFoundError, + fmt.Sprintf("swipe %s %d times, match condition failed", direction, maxRetryTimes)) } func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error { @@ -118,7 +120,9 @@ func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, // wait interval between each findAction time.Sleep(time.Duration(1000*interval) * time.Millisecond) } - return fmt.Errorf("loop %d times, match find condition failed", maxRetryTimes) + + return errors.Wrap(code.OCRTextNotFoundError, + fmt.Sprintf("loop %d times, match find condition failed", maxRetryTimes)) } func (dExt *DriverExt) swipeToTapApp(appName string, action MobileAction) error { diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 4592b08c..c0be7a21 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10161439" +__version__ = "v4.3.0-beta-10172144" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index c3339ecf..747bbbfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10161439" +version = "v4.3.0-beta-10172144" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From c23053ca16e86db9c92106fb94f780299dda0812 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Oct 2022 15:51:56 +0800 Subject: [PATCH 162/169] refactor: update summary in controller --- hrp/internal/code/code.go | 8 ++--- hrp/loader.go | 69 +++++++++++++++++++++++++++++++++++++++ hrp/parser.go | 6 ++-- hrp/parser_test.go | 8 ++--- hrp/runner.go | 7 ++-- hrp/session.go | 40 ++++++++++++----------- hrp/step_api.go | 3 -- hrp/step_mobile_ui.go | 17 ++-------- hrp/step_request.go | 14 ++------ hrp/step_testcase.go | 6 ---- hrp/step_websocket.go | 14 ++------ hrp/testcase.go | 60 ---------------------------------- 12 files changed, 112 insertions(+), 140 deletions(-) create mode 100644 hrp/loader.go diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index 37d8d1ce..5d483978 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -6,8 +6,8 @@ import ( // general: [0, 20) const ( - SUCCESS = 0 - FAIL = 1 + Success = 0 + GeneralFail = 1 ) // loader: [20, 40) @@ -64,7 +64,7 @@ var errorsMap = map[error]int{ func GetErrorCode(err error) int { if err == nil { - return SUCCESS + return Success } e := errors.Cause(err) @@ -72,5 +72,5 @@ func GetErrorCode(err error) int { return code } - return FAIL + return GeneralFail } diff --git a/hrp/loader.go b/hrp/loader.go new file mode 100644 index 00000000..0f75ef95 --- /dev/null +++ b/hrp/loader.go @@ -0,0 +1,69 @@ +package hrp + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { + testCases := make([]*TestCase, 0) + + for _, iTestCase := range iTestCases { + if _, ok := iTestCase.(*TestCase); ok { + testcase, err := iTestCase.ToTestCase() + if err != nil { + log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct") + return nil, err + } + testCases = append(testCases, testcase) + continue + } + + // iTestCase should be a TestCasePath, file path or folder path + tcPath, ok := iTestCase.(*TestCasePath) + if !ok { + return nil, errors.New("invalid iTestCase type") + } + + casePath := tcPath.GetPath() + err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error { + if dir == nil { + // casePath is a file other than a dir + path = casePath + } else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") { + // skip hidden folders + return fs.SkipDir + } else { + // casePath is a dir + path = filepath.Join(casePath, path) + } + + // ignore non-testcase files + ext := filepath.Ext(path) + if ext != ".yml" && ext != ".yaml" && ext != ".json" { + return nil + } + + // filtered testcases + testCasePath := TestCasePath(path) + tc, err := testCasePath.ToTestCase() + if err != nil { + log.Warn().Err(err).Str("path", path).Msg("load testcase failed") + return nil + } + testCases = append(testCases, tc) + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "read dir failed") + } + } + + log.Info().Int("count", len(testCases)).Msg("load testcases successfully") + return testCases, nil +} diff --git a/hrp/parser.go b/hrp/parser.go index 99e4359b..a177c82c 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -190,7 +190,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} return raw, err } - result, err := p.CallFunc(funcName, parsedArgs.([]interface{})...) + result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) if err != nil { log.Error().Str("funcName", funcName).Interface("arguments", arguments). Err(err).Msg("call function failed") @@ -251,9 +251,9 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} return parsedString, nil } -// CallFunc calls function with arguments +// callFunc calls function with arguments // only support return at most one result value -func (p *Parser) CallFunc(funcName string, arguments ...interface{}) (interface{}, error) { +func (p *Parser) callFunc(funcName string, arguments ...interface{}) (interface{}, error) { // call with plugin function if p.plugin != nil { if p.plugin.Has(funcName) { diff --git a/hrp/parser_test.go b/hrp/parser_test.go index 5f27010b..79756c43 100644 --- a/hrp/parser_test.go +++ b/hrp/parser_test.go @@ -478,14 +478,14 @@ func TestCallBuiltinFunction(t *testing.T) { parser := newParser() // call function without arguments - _, err := parser.CallFunc("get_timestamp") + _, err := parser.callFunc("get_timestamp") if !assert.NoError(t, err) { t.Fatal() } // call function with one argument timeStart := time.Now() - _, err = parser.CallFunc("sleep", 1) + _, err = parser.callFunc("sleep", 1) if !assert.NoError(t, err) { t.Fatal() } @@ -494,7 +494,7 @@ func TestCallBuiltinFunction(t *testing.T) { } // call function with one argument - result, err := parser.CallFunc("gen_random_string", 10) + result, err := parser.callFunc("gen_random_string", 10) if !assert.NoError(t, err) { t.Fatal() } @@ -503,7 +503,7 @@ func TestCallBuiltinFunction(t *testing.T) { } // call function with two argument - result, err = parser.CallFunc("max", float64(10), 9.99) + result, err = parser.callFunc("max", float64(10), 9.99) if !assert.NoError(t, err) { t.Fatal() } diff --git a/hrp/runner.go b/hrp/runner.go index 5fd4a102..58559e85 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -317,9 +317,10 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { } type testCaseRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + parsedConfig *TConfig parametersIterator *ParametersIterator rootDir string // project root dir diff --git a/hrp/session.go b/hrp/session.go index 0a3b7fb5..e11a2958 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -35,14 +35,6 @@ func (r *SessionRunner) resetSession() { r.closeResponseChan = make(chan *wsCloseRespObject, 1) } -func (r *SessionRunner) GetParser() *Parser { - return r.parser -} - -func (r *SessionRunner) GetConfig() *TConfig { - return r.parsedConfig -} - func (r *SessionRunner) HTTPStatOn() bool { return r.hrpRunner.httpStatOn } @@ -74,31 +66,41 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { log.Info().Str("step", stepName). Str("type", string(step.Type())).Msg("run step start") + // run step stepResult, err := step.Run(r) stepResult.Name = stepName - if err != nil { + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + r.summary.Stat.Total += 1 + if stepResult.Success { + r.summary.Stat.Successes += 1 + log.Info(). + Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", true). + Interface("exportVars", stepResult.ExportVars). + Msg("run step end") + } else { + r.summary.Stat.Failures += 1 + // update summary result to failed + r.summary.Success = false log.Error(). Str("step", stepResult.Name). Str("type", string(stepResult.StepType)). Bool("success", false). Msg("run step end") + } - if r.hrpRunner.failfast { - return errors.Wrap(err, "abort running due to failfast setting") - } + // check if failfast + if err != nil && r.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") } // update extracted variables for k, v := range stepResult.ExportVars { r.sessionVariables[k] = v } - - log.Info(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", stepResult.Success). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") } // close websocket connection after all steps done diff --git a/hrp/step_api.go b/hrp/step_api.go index f31e96bd..f6640332 100644 --- a/hrp/step_api.go +++ b/hrp/step_api.go @@ -100,9 +100,6 @@ func (s *StepAPIWithOptionalArgs) Struct() *TStep { func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepResult, err error) { defer func() { - if err != nil { - r.summary.Success = false - } stepResult.StepType = stepTypeAPI }() // extend request with referenced API diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 8b16a9ce..585d5fa2 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -4,9 +4,10 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) // ios setting options @@ -559,7 +560,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err if err != nil { return } - parser := s.GetParser() var osType string var mobileStep *MobileStep @@ -590,17 +590,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err screenshots = append(screenshots, uiDriver.ScreenShots...) attachments["screenshots"] = screenshots stepResult.Attachments = attachments - - // update summary - s.summary.Records = append(s.summary.Records, stepResult) - s.summary.Stat.Total += 1 - if stepResult.Success { - s.summary.Stat.Successes += 1 - } else { - s.summary.Stat.Failures += 1 - // update summary result to failed - s.summary.Success = false - } }() // prepare actions @@ -618,7 +607,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { - if action.Params, err = parser.Parse(action.Params, stepVariables); err != nil { + if action.Params, err = s.parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } if err := uiDriver.DoAction(action); err != nil { diff --git a/hrp/step_request.go b/hrp/step_request.go index e5adf26c..7aed3f13 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -297,16 +297,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err if err != nil { stepResult.Attachments = err.Error() } - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - } }() // override step variables @@ -321,8 +311,8 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } sessionData := newSessionData() - parser := r.GetParser() - config := r.GetConfig() + parser := r.parser + config := r.parsedConfig rb := newRequestBuilder(parser, config, step.Request) rb.req.Method = string(step.Request.Method) diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index 72f2c340..e0c7a346 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -109,12 +109,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe // export testcase export variables stepResult.ExportVars = summary.InOut.ExportVars - // merge testcase summary - r.summary.Records = append(r.summary.Records, summary.Records...) - r.summary.Stat.Total += summary.Stat.Total - r.summary.Stat.Successes += summary.Stat.Successes - r.summary.Stat.Failures += summary.Stat.Failures - if err == nil { stepResult.Success = true } diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 5da949f6..c953a1ce 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -260,16 +260,6 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er if err != nil { stepResult.Attachments = err.Error() } - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - } }() // override step variables @@ -279,8 +269,8 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } sessionData := newSessionData() - parser := r.GetParser() - config := r.GetConfig() + parser := r.parser + config := r.parsedConfig dummyReq := &Request{ URL: step.WebSocket.URL, diff --git a/hrp/testcase.go b/hrp/testcase.go index 401b179b..b9733b57 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -2,8 +2,6 @@ package hrp import ( "fmt" - "io/fs" - "os" "path/filepath" "strings" @@ -359,61 +357,3 @@ func convertJmespathExpr(checkExpr string) string { } return strings.Join(checkItems, ".") } - -func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { - testCases := make([]*TestCase, 0) - - for _, iTestCase := range iTestCases { - if _, ok := iTestCase.(*TestCase); ok { - testcase, err := iTestCase.ToTestCase() - if err != nil { - log.Error().Err(err).Msg("failed to convert ITestCase interface to TestCase struct") - return nil, err - } - testCases = append(testCases, testcase) - continue - } - - // iTestCase should be a TestCasePath, file path or folder path - tcPath, ok := iTestCase.(*TestCasePath) - if !ok { - return nil, errors.New("invalid iTestCase type") - } - - casePath := tcPath.GetPath() - err := fs.WalkDir(os.DirFS(casePath), ".", func(path string, dir fs.DirEntry, e error) error { - if dir == nil { - // casePath is a file other than a dir - path = casePath - } else if dir.IsDir() && path != "." && strings.HasPrefix(path, ".") { - // skip hidden folders - return fs.SkipDir - } else { - // casePath is a dir - path = filepath.Join(casePath, path) - } - - // ignore non-testcase files - ext := filepath.Ext(path) - if ext != ".yml" && ext != ".yaml" && ext != ".json" { - return nil - } - - // filtered testcases - testCasePath := TestCasePath(path) - tc, err := testCasePath.ToTestCase() - if err != nil { - log.Warn().Err(err).Str("path", path).Msg("load testcase failed") - return nil - } - testCases = append(testCases, tc) - return nil - }) - if err != nil { - return nil, errors.Wrap(err, "read dir failed") - } - } - - log.Info().Int("count", len(testCases)).Msg("load testcases successfully") - return testCases, nil -} From 0916665adefde5e813b60d62cb14212b901e547b Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Oct 2022 20:56:46 +0800 Subject: [PATCH 163/169] refactor: merge step variables with session variables --- hrp/session.go | 11 +++++++++-- hrp/step_mobile_ui.go | 7 +------ hrp/step_request.go | 7 +------ hrp/step_testcase.go | 6 +----- hrp/step_websocket.go | 7 +------ 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/hrp/session.go b/hrp/session.go index e11a2958..b752ccea 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -57,6 +57,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { // run step in sequential order for _, step := range r.testCase.TestSteps { + // TODO: parse step // parse step name parsedName, err := r.parser.ParseString(step.Name(), r.sessionVariables) if err != nil { @@ -66,6 +67,12 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { log.Info().Str("step", stepName). Str("type", string(step.Type())).Msg("run step start") + // merge step variables with session variables + step.Struct().Variables, err = r.mergeStepVariables(step.Struct().Variables) + if err != nil { + return errors.Wrap(err, "merge step variables with session variables failed") + } + // run step stepResult, err := step.Run(r) stepResult.Name = stepName @@ -120,8 +127,8 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { return nil } -// MergeStepVariables merges step variables with config variables and session variables -func (r *SessionRunner) MergeStepVariables(vars map[string]interface{}) (map[string]interface{}, error) { +// mergeStepVariables merges step variables with config variables and session variables +func (r *SessionRunner) mergeStepVariables(vars map[string]interface{}) (map[string]interface{}, error) { // override variables // step variables > session variables (extracted variables from previous steps) overrideVars := mergeVariables(vars, r.sessionVariables) diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 585d5fa2..cb44af82 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -554,12 +554,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err ContentSize: 0, } screenshots := make([]string, 0) - - // override step variables - stepVariables, err := s.MergeStepVariables(step.Variables) - if err != nil { - return - } + stepVariables := step.Variables var osType string var mobileStep *MobileStep diff --git a/hrp/step_request.go b/hrp/step_request.go index 7aed3f13..b86ddabd 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -291,6 +291,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err Success: false, ContentSize: 0, } + stepVariables := step.Variables defer func() { // update testcase summary @@ -299,12 +300,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } }() - // override step variables - stepVariables, err := r.MergeStepVariables(step.Variables) - if err != nil { - return - } - err = prepareUpload(r.parser, step, stepVariables) if err != nil { return diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index e0c7a346..a581a289 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -51,6 +51,7 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe StepType: stepTypeTestCase, Success: false, } + stepVariables := s.step.Variables defer func() { // update testcase summary @@ -59,11 +60,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe } }() - stepVariables, err := r.MergeStepVariables(s.step.Variables) - if err != nil { - return stepResult, err - } - stepTestCase := s.step.TestCase.(*TestCase) // copy testcase to avoid data racing diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index c953a1ce..0beb2147 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -254,6 +254,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er Success: false, ContentSize: 0, } + stepVariables := step.Variables defer func() { // update testcase summary @@ -262,12 +263,6 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } }() - // override step variables - stepVariables, err := r.MergeStepVariables(step.Variables) - if err != nil { - return - } - sessionData := newSessionData() parser := r.parser config := r.parsedConfig From a2f1f7ca0e900ba9f536ee82036d290bd9b55f17 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 18 Oct 2022 22:02:29 +0800 Subject: [PATCH 164/169] refactor: CaseRunner and SessionRunner --- hrp/README.md | 65 ++++- hrp/boomer.go | 13 +- .../templates/plugin/debugtalk_gen.go | 2 +- hrp/runner.go | 235 +++++++++++++++--- hrp/session.go | 191 -------------- hrp/step_mobile_ui.go | 12 +- hrp/step_rendezvous.go | 2 +- hrp/step_request.go | 28 ++- hrp/step_request_test.go | 20 +- hrp/step_testcase.go | 13 +- hrp/step_thinktime.go | 2 +- hrp/step_websocket.go | 69 ++--- 12 files changed, 335 insertions(+), 317 deletions(-) delete mode 100644 hrp/session.go diff --git a/hrp/README.md b/hrp/README.md index 38a41391..683a0e1a 100644 --- a/hrp/README.md +++ b/hrp/README.md @@ -51,28 +51,50 @@ type HRPRunner struct { } func (r *HRPRunner) Run(testcases ...ITestCase) error -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) *SessionRunner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) ``` 重点关注两个方法: - Run:测试执行的主入口,支持运行一个或多个测试用例 -- NewSessionRunner:针对给定的测试用例初始化一个 SessionRunner +- NewCaseRunner:针对给定的测试用例初始化一个 CaseRunner -### 用例执行器 SessionRunner +### 用例执行器 CaseRunner -测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 +针对每个测试用例,采用 CaseRunner 存储其公共信息,包括 plugin/parser + +```go +type CaseRunner struct { + testCase *TestCase + hrpRunner *HRPRunner + parser *Parser + + parsedConfig *TConfig + parametersIterator *ParametersIterator + rootDir string // project root dir +} + +func (r *CaseRunner) NewSession() *SessionRunner { +``` + +重点关注一个方法: + +- NewSession:测试用例的每一次执行对应一个 SessionRunner + +### SessionRunner + +测试用例的具体执行都由 `SessionRunner` 完成,每个 session 实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 ```go type SessionRunner struct { - testCase *TestCase - hrpRunner *HRPRunner - parser *Parser + caseRunner *CaseRunner sessionVariables map[string]interface{} - transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary + transactions map[string]map[transactionType]time.Time + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary } + +func (r *SessionRunner) Start(givenVars map[string]interface{}) error ``` 重点关注一个方法: @@ -80,12 +102,29 @@ type SessionRunner struct { - Start:启动执行用例,依次执行所有测试步骤 ```go -func (r *SessionRunner) Start() error { +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { ... + r.resetSession() + + r.InitWithParameters(givenVars) + // run step in sequential order for _, step := range r.testCase.TestSteps { - _, err := step.Run(r) - if err != nil && r.hrpRunner.failfast { + // parse step + + // run step + stepResult, err := step.Run(r) + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + + // update extracted variables + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } + + // check if failfast + if err != nil && r.caseRunner.hrpRunner.failfast { return errors.Wrap(err, "abort running due to failfast setting") } } diff --git a/hrp/boomer.go b/hrp/boomer.go index a1d36aac..c3db1e76 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -136,7 +136,7 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TCase { var parsedTestCases []*TCase for _, tc := range testCases { - caseRunner, err := b.hrpRunner.newCaseRunner(tc) + caseRunner, err := b.hrpRunner.NewCaseRunner(tc) if err != nil { log.Error().Err(err).Msg("failed to create runner") os.Exit(1) @@ -313,9 +313,9 @@ func (b *HRPBoomer) PollTestCases(ctx context.Context) { } func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rendezvous) *boomer.Task { - // init runner for testcase + // init case runner for testcase // this runner is shared by multiple session runners - caseRunner, err := b.hrpRunner.newCaseRunner(testcase) + caseRunner, err := b.hrpRunner.NewCaseRunner(testcase) if err != nil { log.Error().Err(err).Msg("failed to create runner") os.Exit(1) @@ -352,18 +352,19 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend transactionSuccess := true // flag current transaction result // init session runner - sessionRunner := caseRunner.newSession() + sessionRunner := caseRunner.NewSession() mutex.Lock() if parametersIterator.HasNext() { - sessionRunner.updateSessionVariables(parametersIterator.Next()) + sessionRunner.InitWithParameters(parametersIterator.Next()) } mutex.Unlock() startTime := time.Now() for _, step := range testcase.TestSteps { + // TODO: parse step struct // parse step name - parsedName, err := sessionRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) + parsedName, err := caseRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables) if err != nil { parsedName = step.Name() } diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go index fd479e84..db5b8409 100644 --- a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go +++ b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go @@ -1,4 +1,4 @@ -// NOTE: Generated By hrp v4.2.0, DO NOT EDIT! +// NOTE: Generated By hrp v4.3.0-beta-10172144, DO NOT EDIT! package main import ( diff --git a/hrp/runner.go b/hrp/runner.go index 58559e85..1b164226 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -2,6 +2,7 @@ package hrp import ( "crypto/tls" + _ "embed" "net" "net/http" "net/http/cookiejar" @@ -206,19 +207,24 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { var runErr error // run testcase one by one for _, testcase := range testCases { - sessionRunner, err := r.NewSessionRunner(testcase) + // each testcase has its own case runner + caseRunner, err := r.NewCaseRunner(testcase) if err != nil { - log.Error().Err(err).Msg("[Run] init session runner failed") + log.Error().Err(err).Msg("[Run] init case runner failed") return err } + // release UI driver session defer func() { - for _, client := range sessionRunner.hrpRunner.uiClients { + for _, client := range r.uiClients { client.Driver.DeleteSession() } }() - for it := sessionRunner.parametersIterator; it.HasNext(); { + for it := caseRunner.parametersIterator; it.HasNext(); { + // case runner can run multiple times with different parameters + // each run has its own session runner + sessionRunner := caseRunner.NewSession() err1 := sessionRunner.Start(it.Next()) caseSummary, err2 := sessionRunner.GetSummary() s.appendCaseSummary(caseSummary) @@ -250,23 +256,10 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { return runErr } -// NewSessionRunner creates a new session runner for testcase. -// each testcase has its own session runner -func (r *HRPRunner) NewSessionRunner(testcase *TestCase) (*SessionRunner, error) { - runner, err := r.newCaseRunner(testcase) - if err != nil { - return nil, err - } - - sessionRunner := &SessionRunner{ - testCaseRunner: runner, - } - sessionRunner.resetSession() - return sessionRunner, nil -} - -func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { - runner := &testCaseRunner{ +// NewCaseRunner creates a new case runner for testcase. +// each testcase has its own case runner +func (r *HRPRunner) NewCaseRunner(testcase *TestCase) (*CaseRunner, error) { + caseRunner := &CaseRunner{ testCase: testcase, hrpRunner: r, parser: newParser(), @@ -278,34 +271,31 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { return nil, errors.Wrap(err, "init plugin failed") } if plugin != nil { - runner.parser.plugin = plugin - runner.rootDir = filepath.Dir(plugin.Path()) + caseRunner.parser.plugin = plugin + caseRunner.rootDir = filepath.Dir(plugin.Path()) } // parse testcase config - if err := runner.parseConfig(); err != nil { + if err := caseRunner.parseConfig(); err != nil { return nil, errors.Wrap(err, "parse testcase config failed") } - // init websocket params - initWebSocket(testcase) - // set testcase timeout in seconds - if runner.testCase.Config.Timeout != 0 { - timeout := time.Duration(runner.testCase.Config.Timeout*1000) * time.Millisecond - runner.hrpRunner.SetTimeout(timeout) + if testcase.Config.Timeout != 0 { + timeout := time.Duration(testcase.Config.Timeout*1000) * time.Millisecond + r.SetTimeout(timeout) } // load plugin info to testcase config if plugin != nil { pluginPath, _ := locatePlugin(testcase.Config.Path) - if runner.parsedConfig.PluginSetting == nil { + if caseRunner.parsedConfig.PluginSetting == nil { pluginContent, err := builtin.ReadFile(pluginPath) if err != nil { return nil, err } tp := strings.Split(plugin.Path(), ".") - runner.parsedConfig.PluginSetting = &PluginConfig{ + caseRunner.parsedConfig.PluginSetting = &PluginConfig{ Path: pluginPath, Content: pluginContent, Type: tp[len(tp)-1], @@ -313,10 +303,10 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { } } - return runner, nil + return caseRunner, nil } -type testCaseRunner struct { +type CaseRunner struct { testCase *TestCase hrpRunner *HRPRunner parser *Parser @@ -327,7 +317,7 @@ type testCaseRunner struct { } // parseConfig parses testcase config, stores to parsedConfig. -func (r *testCaseRunner) parseConfig() error { +func (r *CaseRunner) parseConfig() error { cfg := r.testCase.Config r.parsedConfig = &TConfig{} @@ -441,10 +431,181 @@ func (r *testCaseRunner) parseConfig() error { // each boomer task initiates a new session // in order to avoid data racing -func (r *testCaseRunner) newSession() *SessionRunner { +func (r *CaseRunner) NewSession() *SessionRunner { sessionRunner := &SessionRunner{ - testCaseRunner: r, + caseRunner: r, } sessionRunner.resetSession() return sessionRunner } + +// SessionRunner is used to run testcase and its steps. +// each testcase has its own SessionRunner instance and share session variables. +type SessionRunner struct { + caseRunner *CaseRunner + sessionVariables map[string]interface{} + // transactions stores transaction timing info. + // key is transaction name, value is map of transaction type and time, e.g. start time and end time. + transactions map[string]map[transactionType]time.Time + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary + wsConnMap map[string]*websocket.Conn // save all websocket connections + pongResponseChan chan string // channel used to receive pong response message + closeResponseChan chan *wsCloseRespObject // channel used to receive close response message +} + +func (r *SessionRunner) resetSession() { + log.Info().Msg("reset session runner") + r.sessionVariables = make(map[string]interface{}) + r.transactions = make(map[string]map[transactionType]time.Time) + r.startTime = time.Now() + r.summary = newSummary() + r.wsConnMap = make(map[string]*websocket.Conn) + r.pongResponseChan = make(chan string, 1) + r.closeResponseChan = make(chan *wsCloseRespObject, 1) +} + +// Start runs the test steps in sequential order. +// givenVars is used for data driven +func (r *SessionRunner) Start(givenVars map[string]interface{}) error { + config := r.caseRunner.testCase.Config + log.Info().Str("testcase", config.Name).Msg("run testcase start") + + // reset session runner + r.resetSession() + + // update config variables with given variables + r.InitWithParameters(givenVars) + + // run step in sequential order + for _, step := range r.caseRunner.testCase.TestSteps { + // TODO: parse step struct + // parse step name + parsedName, err := r.caseRunner.parser.ParseString(step.Name(), r.sessionVariables) + if err != nil { + parsedName = step.Name() + } + stepName := convertString(parsedName) + log.Info().Str("step", stepName). + Str("type", string(step.Type())).Msg("run step start") + + // run step + stepResult, err := step.Run(r) + stepResult.Name = stepName + + // update summary + r.summary.Records = append(r.summary.Records, stepResult) + r.summary.Stat.Total += 1 + if stepResult.Success { + r.summary.Stat.Successes += 1 + } else { + r.summary.Stat.Failures += 1 + // update summary result to failed + r.summary.Success = false + } + + // update extracted variables + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } + + if err == nil { + log.Info().Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", true). + Interface("exportVars", stepResult.ExportVars). + Msg("run step end") + continue + } + + // failed + log.Error().Err(err).Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", false). + Msg("run step end") + + // check if failfast + if r.caseRunner.hrpRunner.failfast { + return errors.Wrap(err, "abort running due to failfast setting") + } + } + + // close websocket connection after all steps done + defer func() { + for _, wsConn := range r.wsConnMap { + if wsConn != nil { + log.Info().Str("testcase", config.Name).Msg("websocket disconnected") + err := wsConn.Close() + if err != nil { + log.Error().Err(err).Msg("websocket disconnection failed") + } + } + } + }() + + log.Info().Str("testcase", config.Name).Msg("run testcase end") + return nil +} + +// ParseStepVariables merges step variables with config variables and session variables +func (r *SessionRunner) ParseStepVariables(stepVariables map[string]interface{}) (map[string]interface{}, error) { + // override variables + // step variables > session variables (extracted variables from previous steps) + overrideVars := mergeVariables(stepVariables, r.sessionVariables) + // step variables > testcase config variables + overrideVars = mergeVariables(overrideVars, r.caseRunner.parsedConfig.Variables) + + // parse step variables + parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars) + if err != nil { + log.Error().Interface("variables", r.caseRunner.parsedConfig.Variables). + Err(err).Msg("parse step variables failed") + return nil, err + } + return parsedVariables, nil +} + +// InitWithParameters updates session variables with given parameters. +// this is used for data driven +func (r *SessionRunner) InitWithParameters(parameters map[string]interface{}) { + if len(parameters) == 0 { + return + } + + log.Info().Interface("parameters", parameters).Msg("update session variables") + for k, v := range parameters { + r.sessionVariables[k] = v + } +} + +func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { + caseSummary := r.summary + caseSummary.Name = r.caseRunner.parsedConfig.Name + caseSummary.Time.StartAt = r.startTime + caseSummary.Time.Duration = time.Since(r.startTime).Seconds() + exportVars := make(map[string]interface{}) + for _, value := range r.caseRunner.parsedConfig.Export { + exportVars[value] = r.sessionVariables[value] + } + caseSummary.InOut.ExportVars = exportVars + caseSummary.InOut.ConfigVars = r.caseRunner.parsedConfig.Variables + + for uuid, client := range r.caseRunner.hrpRunner.uiClients { + // add WDA/UIA logs to summary + log, err := client.Driver.StopCaptureLog() + if err != nil { + return caseSummary, err + } + logs := map[string]interface{}{ + "uuid": uuid, + "content": log, + } + + // stop performance monitor + logs["performance"] = client.GetPerfData() + + caseSummary.Logs = append(caseSummary.Logs, logs) + } + + return caseSummary, nil +} diff --git a/hrp/session.go b/hrp/session.go deleted file mode 100644 index b752ccea..00000000 --- a/hrp/session.go +++ /dev/null @@ -1,191 +0,0 @@ -package hrp - -import ( - _ "embed" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" -) - -// SessionRunner is used to run testcase and its steps. -// each testcase has its own SessionRunner instance and share session variables. -type SessionRunner struct { - *testCaseRunner - sessionVariables map[string]interface{} - // transactions stores transaction timing info. - // key is transaction name, value is map of transaction type and time, e.g. start time and end time. - transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary - wsConnMap map[string]*websocket.Conn // save all websocket connections - pongResponseChan chan string // channel used to receive pong response message - closeResponseChan chan *wsCloseRespObject // channel used to receive close response message -} - -func (r *SessionRunner) resetSession() { - log.Info().Msg("reset session runner") - r.sessionVariables = make(map[string]interface{}) - r.transactions = make(map[string]map[transactionType]time.Time) - r.startTime = time.Now() - r.summary = newSummary() - r.wsConnMap = make(map[string]*websocket.Conn) - r.pongResponseChan = make(chan string, 1) - r.closeResponseChan = make(chan *wsCloseRespObject, 1) -} - -func (r *SessionRunner) HTTPStatOn() bool { - return r.hrpRunner.httpStatOn -} - -func (r *SessionRunner) LogOn() bool { - return r.hrpRunner.requestsLogOn -} - -// Start runs the test steps in sequential order. -// givenVars is used for data driven -func (r *SessionRunner) Start(givenVars map[string]interface{}) error { - config := r.testCase.Config - log.Info().Str("testcase", config.Name).Msg("run testcase start") - - // reset session runner - r.resetSession() - - // update config variables with given variables - r.updateSessionVariables(givenVars) - - // run step in sequential order - for _, step := range r.testCase.TestSteps { - // TODO: parse step - // parse step name - parsedName, err := r.parser.ParseString(step.Name(), r.sessionVariables) - if err != nil { - parsedName = step.Name() - } - stepName := convertString(parsedName) - log.Info().Str("step", stepName). - Str("type", string(step.Type())).Msg("run step start") - - // merge step variables with session variables - step.Struct().Variables, err = r.mergeStepVariables(step.Struct().Variables) - if err != nil { - return errors.Wrap(err, "merge step variables with session variables failed") - } - - // run step - stepResult, err := step.Run(r) - stepResult.Name = stepName - - // update summary - r.summary.Records = append(r.summary.Records, stepResult) - r.summary.Stat.Total += 1 - if stepResult.Success { - r.summary.Stat.Successes += 1 - log.Info(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", true). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") - } else { - r.summary.Stat.Failures += 1 - // update summary result to failed - r.summary.Success = false - log.Error(). - Str("step", stepResult.Name). - Str("type", string(stepResult.StepType)). - Bool("success", false). - Msg("run step end") - } - - // check if failfast - if err != nil && r.hrpRunner.failfast { - return errors.Wrap(err, "abort running due to failfast setting") - } - - // update extracted variables - for k, v := range stepResult.ExportVars { - r.sessionVariables[k] = v - } - } - - // close websocket connection after all steps done - defer func() { - for _, wsConn := range r.wsConnMap { - if wsConn != nil { - log.Info().Str("testcase", config.Name).Msg("websocket disconnected") - err := wsConn.Close() - if err != nil { - log.Error().Err(err).Msg("websocket disconnection failed") - } - } - } - }() - - log.Info().Str("testcase", config.Name).Msg("run testcase end") - return nil -} - -// mergeStepVariables merges step variables with config variables and session variables -func (r *SessionRunner) mergeStepVariables(vars map[string]interface{}) (map[string]interface{}, error) { - // override variables - // step variables > session variables (extracted variables from previous steps) - overrideVars := mergeVariables(vars, r.sessionVariables) - // step variables > testcase config variables - overrideVars = mergeVariables(overrideVars, r.parsedConfig.Variables) - - // parse step variables - parsedVariables, err := r.parser.ParseVariables(overrideVars) - if err != nil { - log.Error().Interface("variables", r.parsedConfig.Variables). - Err(err).Msg("parse step variables failed") - return nil, err - } - return parsedVariables, nil -} - -// updateSessionVariables updates session variables with given variables. -// this is used for data driven -func (r *SessionRunner) updateSessionVariables(parameters map[string]interface{}) { - if len(parameters) == 0 { - return - } - - log.Info().Interface("parameters", parameters).Msg("update session variables") - for k, v := range parameters { - r.sessionVariables[k] = v - } -} - -func (r *SessionRunner) GetSummary() (*TestCaseSummary, error) { - caseSummary := r.summary - caseSummary.Name = r.parsedConfig.Name - caseSummary.Time.StartAt = r.startTime - caseSummary.Time.Duration = time.Since(r.startTime).Seconds() - exportVars := make(map[string]interface{}) - for _, value := range r.parsedConfig.Export { - exportVars[value] = r.sessionVariables[value] - } - caseSummary.InOut.ExportVars = exportVars - caseSummary.InOut.ConfigVars = r.parsedConfig.Variables - - for uuid, client := range r.hrpRunner.uiClients { - // add WDA/UIA logs to summary - log, err := client.Driver.StopCaptureLog() - if err != nil { - return caseSummary, err - } - logs := map[string]interface{}{ - "uuid": uuid, - "content": log, - } - - // stop performance monitor - logs["performance"] = client.GetPerfData() - - caseSummary.Logs = append(caseSummary.Logs, logs) - } - - return caseSummary, nil -} diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index cb44af82..a6dad716 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -554,7 +554,13 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err ContentSize: 0, } screenshots := make([]string, 0) - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := s.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } var osType string var mobileStep *MobileStep @@ -569,7 +575,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err } // init wda/uia driver - uiDriver, err := s.hrpRunner.initUIClient(mobileStep.Serial, osType) + uiDriver, err := s.caseRunner.hrpRunner.initUIClient(mobileStep.Serial, osType) if err != nil { return } @@ -602,7 +608,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { - if action.Params, err = s.parser.Parse(action.Params, stepVariables); err != nil { + if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil { return stepResult, errors.Wrap(err, "parse action params failed") } if err := uiDriver.DoAction(action); err != nil { diff --git a/hrp/step_rendezvous.go b/hrp/step_rendezvous.go index edd9cf84..a9e5f0e0 100644 --- a/hrp/step_rendezvous.go +++ b/hrp/step_rendezvous.go @@ -44,7 +44,7 @@ func (s *StepRendezvous) Run(r *SessionRunner) (*StepResult, error) { } // pass current rendezvous if already released, activate rendezvous sequentially after spawn done - if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.testCase.ToTCase()) || !rendezvous.isSpawnDone() { + if rendezvous.isReleased() || !isPreRendezvousAllReleased(rendezvous, r.caseRunner.testCase.ToTCase()) || !rendezvous.isSpawnDone() { return stepResult, nil } diff --git a/hrp/step_request.go b/hrp/step_request.go index b86ddabd..89269d6f 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -291,7 +291,13 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err Success: false, ContentSize: 0, } - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -300,14 +306,14 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } }() - err = prepareUpload(r.parser, step, stepVariables) + err = prepareUpload(r.caseRunner.parser, step, stepVariables) if err != nil { return } sessionData := newSessionData() - parser := r.parser - config := r.parsedConfig + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig rb := newRequestBuilder(parser, config, step.Request) rb.req.Method = string(step.Request.Method) @@ -340,7 +346,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } // log & print request - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printRequest(rb.req); err != nil { return stepResult, err } @@ -348,7 +354,7 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // stat HTTP request var httpStat httpstat.Stat - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { ctx := httpstat.WithHTTPStat(rb.req, &httpStat) rb.req = rb.req.WithContext(ctx) } @@ -356,9 +362,9 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err // select HTTP client var client *http.Client if step.Request.HTTP2 { - client = r.hrpRunner.http2Client + client = r.caseRunner.hrpRunner.http2Client } else { - client = r.hrpRunner.httpClient + client = r.caseRunner.hrpRunner.httpClient } // set step timeout @@ -384,21 +390,21 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err defer resp.Body.Close() // log & print response - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { if err := printResponse(resp); err != nil { return stepResult, err } } // new response object - respObj, err := newHttpResponseObject(r.hrpRunner.t, parser, resp) + respObj, err := newHttpResponseObject(r.caseRunner.hrpRunner.t, parser, resp) if err != nil { err = errors.Wrap(err, "init ResponseObject error") return } stepResult.Elapsed = time.Since(start).Milliseconds() - if r.HTTPStatOn() { + if r.caseRunner.hrpRunner.httpStatOn { // resp.Body has been ReadAll httpStat.Finish() stepResult.HttpStat = httpStat.Durations() diff --git a/hrp/step_request_test.go b/hrp/step_request_test.go index d49a9253..3f7e63c5 100644 --- a/hrp/step_request_test.go +++ b/hrp/step_request_test.go @@ -77,29 +77,13 @@ func TestRunRequestPostDataToStruct(t *testing.T) { } } -func TestRunRequestRun(t *testing.T) { - testcase := &TestCase{ - Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), - TestSteps: []IStep{stepGET, stepPOSTData}, - } - runner := NewRunner(t).SetRequestsLogOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) - - if _, err := stepGET.Run(sessionRunner); err != nil { - t.Fatalf("stepGET.Run() error: %v", err) - } - if _, err := stepPOSTData.Run(sessionRunner); err != nil { - t.Fatalf("stepPOSTData.Run() error: %v", err) - } -} - func TestRunRequestStatOn(t *testing.T) { testcase := &TestCase{ Config: NewConfig("test").SetBaseURL("https://postman-echo.com"), TestSteps: []IStep{stepGET, stepPOSTData}, } - runner := NewRunner(t).SetHTTPStatOn() - sessionRunner, _ := runner.NewSessionRunner(testcase) + caseRunner, _ := NewRunner(t).SetHTTPStatOn().NewCaseRunner(testcase) + sessionRunner := caseRunner.NewSession() if err := sessionRunner.Start(nil); err != nil { t.Fatal() } diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index a581a289..1464735e 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -51,7 +51,13 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe StepType: stepTypeTestCase, Success: false, } - stepVariables := s.step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(s.step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -77,11 +83,12 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe // merge & override extractors copiedTestCase.Config.Export = mergeSlices(s.step.Export, copiedTestCase.Config.Export) - sessionRunner, err := r.hrpRunner.NewSessionRunner(copiedTestCase) + caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(copiedTestCase) if err != nil { - log.Error().Err(err).Msg("create session runner failed") + log.Error().Err(err).Msg("create case runner failed") return stepResult, err } + sessionRunner := caseRunner.NewSession() start := time.Now() // run referenced testcase with step variables diff --git a/hrp/step_thinktime.go b/hrp/step_thinktime.go index 6b14b462..7a505cf8 100644 --- a/hrp/step_thinktime.go +++ b/hrp/step_thinktime.go @@ -39,7 +39,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { Success: true, } - cfg := r.parsedConfig.ThinkTimeSetting + cfg := r.caseRunner.parsedConfig.ThinkTimeSetting if cfg == nil { cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0} } diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 0beb2147..fbb5cfd5 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -231,20 +231,19 @@ type WebSocketAction struct { Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` } -func initWebSocket(testcase *TestCase) { - for _, step := range testcase.TestSteps { - if step.Struct().WebSocket == nil { - continue - } - // init websocket action parameters - if step.Struct().WebSocket.Timeout <= 0 { - step.Struct().WebSocket.Timeout = defaultTimeout - } - // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 - if step.Struct().WebSocket.CloseStatusCode < 1000 || step.Struct().WebSocket.CloseStatusCode > 4999 { - step.Struct().WebSocket.CloseStatusCode = defaultCloseStatus - } +func (w *WebSocketAction) GetTimeout() int64 { + if w.Timeout <= 0 { + return defaultTimeout } + return w.Timeout +} + +func (w *WebSocketAction) GetCloseStatusCode() int64 { + // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 + if w.CloseStatusCode < 1000 || w.CloseStatusCode > 4999 { + return defaultCloseStatus + } + return w.CloseStatusCode } func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { @@ -254,7 +253,13 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er Success: false, ContentSize: 0, } - stepVariables := step.Variables + + // merge step variables with session variables + stepVariables, err := r.ParseStepVariables(step.Variables) + if err != nil { + err = errors.Wrap(err, "parse step variables failed") + return + } defer func() { // update testcase summary @@ -264,8 +269,8 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er }() sessionData := newSessionData() - parser := r.parser - config := r.parsedConfig + parser := r.caseRunner.parser + config := r.caseRunner.parsedConfig dummyReq := &Request{ URL: step.WebSocket.URL, @@ -302,12 +307,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er start := time.Now() // do websocket action - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { fmt.Printf("-------------------- websocket action: %v --------------------\n", step.WebSocket.Type.toString()) } switch step.WebSocket.Type { case wsOpen: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("open websocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("open websocket connection") // use the current websocket connection if existed if r.wsConnMap[parsedURL] != nil { break @@ -317,12 +322,12 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "open connection failed") } case wsPing: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("send ping and expect pong") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("send ping and expect pong") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "send ping message failed") } - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) // asynchronous receiving pong message with timeout go func() { select { @@ -335,7 +340,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } }() case wsWriteAndRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("write a message and read response") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("write a message and read response") err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "write message failed") @@ -345,7 +350,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "read message failed") } case wsRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("read only") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("read only") resp, err = readMessageWithTimeout(parsedURL, r, step) if err != nil { return stepResult, errors.Wrap(err, "read message failed") @@ -357,7 +362,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er return stepResult, errors.Wrap(err, "write message failed") } case wsClose: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("close webSocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.GetTimeout()).Str("url", parsedURL).Msg("close webSocket connection") resp, err = closeWithTimeout(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "close connection failed") @@ -365,7 +370,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er default: return stepResult, errors.Errorf("unexpected websocket frame type: %v", step.WebSocket.Type) } - if r.LogOn() { + if r.caseRunner.hrpRunner.requestsLogOn { err = printWebSocketResponse(resp) if err != nil { return stepResult, errors.Wrap(err, "print response failed") @@ -373,7 +378,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } stepResult.Elapsed = time.Since(start).Milliseconds() - respObj, err := getResponseObject(r.hrpRunner.t, r.parser, resp) + respObj, err := getResponseObject(r.caseRunner.hrpRunner.t, r.caseRunner.parser, resp) if err != nil { err = errors.Wrap(err, "get response object error") return @@ -455,7 +460,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan := make(chan *http.Response) errorChan := make(chan error) go func() { - conn, resp, err := r.hrpRunner.wsDialer.Dial(urlStr, requestHeader) + conn, resp, err := r.caseRunner.hrpRunner.wsDialer.Dial(urlStr, requestHeader) if err != nil { errorChan <- errors.Wrap(err, "dial tcp failed") return @@ -481,7 +486,7 @@ func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, openResponseChan <- resp }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -511,7 +516,7 @@ func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*w } } }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() @@ -530,7 +535,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl } // check priority: text message > binary message if step.WebSocket.TextMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.TextMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.TextMessage, stepVariables) if parseErr != nil { return parseErr } @@ -539,7 +544,7 @@ func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariabl return writeErr } } else if step.WebSocket.BinaryMessage != nil { - parsedMessage, parseErr := r.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) + parsedMessage, parseErr := r.caseRunner.parser.Parse(step.WebSocket.BinaryMessage, stepVariables) if parseErr != nil { return parseErr } @@ -582,7 +587,7 @@ func writeWithAction(c *websocket.Conn, step *TStep, messageType int, message [] case wsPing: return c.WriteControl(websocket.PingMessage, message, time.Now().Add(defaultWriteWait)) case wsClose: - closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.CloseStatusCode), string(message)) + closeMessage := websocket.FormatCloseMessage(int(step.WebSocket.GetCloseStatusCode()), string(message)) return c.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(defaultWriteWait)) default: return c.WriteMessage(messageType, message) @@ -622,7 +627,7 @@ func closeWithTimeout(urlString string, r *SessionRunner, step *TStep, stepVaria // r.wsConn.Close() will be called at the end of current session, so no need to Close here log.Info().Str("msg", readErr.Error()).Msg("connection closed") }() - timer := time.NewTimer(time.Duration(step.WebSocket.Timeout) * time.Millisecond) + timer := time.NewTimer(time.Duration(step.WebSocket.GetTimeout()) * time.Millisecond) select { case <-timer.C: timer.Stop() From d8dc3c58c9f959f87cea50acc273061430bede4d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 19 Oct 2022 11:48:40 +0800 Subject: [PATCH 165/169] feat: add status code for mobile UI --- hrp/internal/code/code.go | 38 +++++++++++++++++++++++--- hrp/internal/version/VERSION | 2 +- hrp/parser.go | 19 ++++++++----- hrp/pkg/uixt/android_device.go | 9 ++++-- hrp/pkg/uixt/android_driver.go | 9 +++++- hrp/pkg/uixt/ios_device.go | 50 +++++++++++++++++++++------------- hrp/pkg/uixt/ios_driver.go | 9 ++++-- hrp/runner.go | 2 +- hrp/step_mobile_ui.go | 7 +++-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 11 files changed, 106 insertions(+), 43 deletions(-) diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index 5d483978..b2e6eb60 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -18,17 +18,33 @@ var ( ) // parser: [40, 60) +var ( + ParseError = errors.New("parse error") // 40 + ParseConfigError = errors.New("parse config error") // 41 + ParseStringError = errors.New("parse string failed") // 42 + ParseVariablesError = errors.New("parse variables failed") // 43 +) // runner: [60, 100) // ios related: [100, 130) var ( - IOSScreenShotError = errors.New("ios screenshot error") // 110 + IOSDeviceConnectionError = errors.New("ios device connection error") // 100 + IOSDeviceHTTPDriverError = errors.New("ios device HTTP driver error") // 101 + IOSDeviceUSBDriverError = errors.New("ios device USB driver error") // 102 + IOSScreenShotError = errors.New("ios screenshot error") // 110 + IOSCaptureLogError = errors.New("ios capture log error") // 111 + + MobileUIDriverError = errors.New("mobile UI driver error") // 120 + MobileUIValidationError = errors.New("mobile UI validation error") // 121 ) // android related: [130, 160) var ( - AndroidScreenShotError = errors.New("android screenshot error") // 150 + AndroidDeviceConnectionError = errors.New("android device connection error") // 130 + AndroidDeviceDriverError = errors.New("android device driver error") // 131 + AndroidScreenShotError = errors.New("android screenshot error") // 150 + AndroidCaptureLogError = errors.New("android capture log error") // 151 ) // OCR related: [160, 180) @@ -45,14 +61,28 @@ var ( // report related: [200, 220) var errorsMap = map[error]int{ + // loader LoadJSONError: 10, LoadYAMLError: 11, + // parser + ParseError: 40, + ParseConfigError: 41, + ParseStringError: 42, + ParseVariablesError: 43, + // ios related - IOSScreenShotError: 110, + IOSDeviceConnectionError: 100, + IOSDeviceHTTPDriverError: 101, + IOSDeviceUSBDriverError: 102, + IOSScreenShotError: 110, + IOSCaptureLogError: 111, // android related - AndroidScreenShotError: 130, + AndroidDeviceConnectionError: 130, + AndroidDeviceDriverError: 131, + AndroidScreenShotError: 150, + AndroidCaptureLogError: 151, // OCR related OCREnvMissedError: 160, diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index bd879d79..902617de 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10172144 \ No newline at end of file +v4.3.0-beta-10191154 \ No newline at end of file diff --git a/hrp/parser.go b/hrp/parser.go index a177c82c..290a5f07 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -13,9 +13,11 @@ import ( "github.com/httprunner/funplugin" "github.com/httprunner/funplugin/shared" "github.com/maja42/goval" + "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" ) func newParser() *Parser { @@ -183,18 +185,18 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} argsStr := funcMatched[2] arguments, err := parseFunctionArguments(argsStr) if err != nil { - return raw, err + return raw, errors.Wrap(code.ParseStringError, err.Error()) } parsedArgs, err := p.Parse(arguments, variablesMapping) if err != nil { - return raw, err + return raw, errors.Wrap(code.ParseStringError, err.Error()) } result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) if err != nil { log.Error().Str("funcName", funcName).Interface("arguments", arguments). Err(err).Msg("call function failed") - return raw, err + return raw, errors.Wrap(code.ParseStringError, err.Error()) } log.Info().Str("funcName", funcName).Interface("arguments", arguments). Interface("output", result).Msg("call function success") @@ -226,7 +228,8 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} } varValue, ok := variablesMapping[varName] if !ok { - return raw, fmt.Errorf("variable %s not found", varName) + return raw, errors.Wrap(code.ParseStringError, + fmt.Sprintf("variable %s not found", varName)) } if fmt.Sprintf("${%s}", varName) == raw || fmt.Sprintf("$%s", varName) == raw { @@ -428,7 +431,8 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in // variables = {"key": ["$key", 2]} if _, ok := extractVarsSet[varName]; ok { log.Error().Interface("variables", variables).Msg("[parseVariables] variable self reference error") - return variables, fmt.Errorf("variable self reference: %v", varName) + return variables, errors.Wrap(code.ParseVariablesError, + fmt.Sprintf("variable self reference: %v", varName)) } // check if reference variable not in variables mapping @@ -443,7 +447,8 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in } if len(undefinedVars) > 0 { log.Error().Interface("undefinedVars", undefinedVars).Msg("[parseVariables] variable not defined error") - return variables, fmt.Errorf("variable not defined: %v", undefinedVars) + return variables, errors.Wrap(code.ParseVariablesError, + fmt.Sprintf("variable not defined: %v", undefinedVars)) } parsedValue, err := p.Parse(varValue, parsedVariables) @@ -456,7 +461,7 @@ func (p *Parser) ParseVariables(variables map[string]interface{}) (map[string]in // check if circular reference exists if traverseRounds > len(variables) { log.Error().Msg("[parseVariables] circular reference error, break infinite loop!") - return variables, fmt.Errorf("circular reference") + return variables, errors.Wrap(code.ParseVariablesError, "circular reference") } } diff --git a/hrp/pkg/uixt/android_device.go b/hrp/pkg/uixt/android_device.go index 8fbd713b..492bf765 100644 --- a/hrp/pkg/uixt/android_device.go +++ b/hrp/pkg/uixt/android_device.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/internal/myexec" ) @@ -71,7 +72,8 @@ func GetAndroidDeviceOptions(dev *AndroidDevice) (deviceOptions []AndroidDeviceO func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) { deviceList, err := DeviceList() if err != nil { - return nil, fmt.Errorf("get attached devices failed: %v", err) + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("get attached devices failed: %v", err)) } device = &AndroidDevice{ @@ -95,13 +97,14 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er return device, nil } - return nil, fmt.Errorf("device %s not found", device.SerialNumber) + return nil, errors.Wrap(code.AndroidDeviceConnectionError, + fmt.Sprintf("device %s not found", device.SerialNumber)) } func DeviceList() (devices []gadb.Device, err error) { var adbClient gadb.Client if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { - return nil, err + return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error()) } return adbClient.DeviceList() diff --git a/hrp/pkg/uixt/android_driver.go b/hrp/pkg/uixt/android_driver.go index 526bfc32..904b550f 100644 --- a/hrp/pkg/uixt/android_driver.go +++ b/hrp/pkg/uixt/android_driver.go @@ -963,7 +963,12 @@ func (ud *uiaDriver) Wait(condition Condition) error { func (ud *uiaDriver) StartCaptureLog(identifier ...string) (err error) { log.Info().Msg("start adb log recording") err = ud.logcat.CatchLogcat() - return + if err != nil { + err = errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("start adb log recording failed: %v", err)) + return err + } + return nil } func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) { @@ -971,6 +976,8 @@ func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) { err = ud.logcat.Stop() if err != nil { log.Error().Err(err).Msg("failed to get adb log recording") + err = errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("get adb log recording failed: %v", err)) return "", err } content := ud.logcat.logBuffer.String() diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 0b2a4d50..fb8afdd6 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/env" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -106,11 +107,13 @@ func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption { func IOSDevices(udid ...string) (devices []giDevice.Device, err error) { var usbmux giDevice.Usbmux if usbmux, err = giDevice.NewUsbmux(); err != nil { - return nil, fmt.Errorf("init usbmux failed: %v", err) + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("init usbmux failed: %v", err)) } if devices, err = usbmux.Devices(); err != nil { - return nil, fmt.Errorf("list ios devices failed: %v", err) + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("list ios devices failed: %v", err)) } // filter by udid @@ -185,7 +188,8 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { return device, nil } - return nil, fmt.Errorf("device %s not found", device.UDID) + return nil, errors.Wrap(code.IOSDeviceConnectionError, + fmt.Sprintf("device %s not found", device.UDID)) } type IOSDevice struct { @@ -230,13 +234,15 @@ func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt if dev.ResetHomeOnStartup { log.Info().Msg("go back to home screen") if err = driver.Homescreen(); err != nil { - return nil, errors.Wrap(err, "failed to go back to home screen") + return nil, errors.Wrap(code.MobileUIDriverError, + fmt.Sprintf("go back to home screen failed: %v", err)) } } driverExt, err = Extend(driver) if err != nil { - return nil, errors.Wrap(err, "failed to extend WebDriver") + return nil, errors.Wrap(code.MobileUIDriverError, + fmt.Sprintf("extend WebDriver failed: %v", err)) } settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{ "snapshotMaxDepth": dev.SnapshotMaxDepth, @@ -380,17 +386,21 @@ func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) { func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { localPort, err := getFreePort() if err != nil { - return nil, errors.Wrap(err, "get free port failed") + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) } if err = dev.forward(localPort, dev.Port); err != nil { - return nil, errors.Wrap(err, "forward tcp port failed") + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) } localMjpegPort, err := getFreePort() if err != nil { - return nil, errors.Wrap(err, "get free port failed") + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) } if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil { - return nil, errors.Wrap(err, "forward tcp port failed") + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) } log.Info().Interface("capabilities", capabilities). @@ -402,11 +412,11 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver host := "127.0.0.1" if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil { - return nil, err + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) } var sessionInfo SessionInfo if sessionInfo, err = wd.NewSession(capabilities); err != nil { - return nil, err + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) } wd.sessionId = sessionInfo.SessionId @@ -414,7 +424,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver "tcp", fmt.Sprintf("%s:%d", host, localMjpegPort), ); err != nil { - return nil, err + return nil, errors.Wrap(code.IOSDeviceHTTPDriverError, err.Error()) } wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn) @@ -428,23 +438,25 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, wd := new(wdaDriver) if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil { - return nil, fmt.Errorf("connect port %d failed: %w", - dev.Port, err) + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, + fmt.Sprintf("connect port %d failed: %v", dev.Port, err)) } wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil { - return nil, fmt.Errorf("connect MJPEG port %d failed: %w", - dev.MjpegPort, err) + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, + fmt.Sprintf("connect MJPEG port %d failed: %v", dev.MjpegPort, err)) } wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil { - return nil, err + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, err.Error()) + } + if _, err = wd.NewSession(capabilities); err != nil { + return nil, errors.Wrap(code.IOSDeviceUSBDriverError, err.Error()) } - _, err = wd.NewSession(capabilities) - return wd, err + return wd, nil } func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) { diff --git a/hrp/pkg/uixt/ios_driver.go b/hrp/pkg/uixt/ios_driver.go index b4704f77..72da3928 100644 --- a/hrp/pkg/uixt/ios_driver.go +++ b/hrp/pkg/uixt/ios_driver.go @@ -858,7 +858,8 @@ func (wd *wdaDriver) StartCaptureLog(identifier ...string) error { data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier[0]} _, err := wd.triggerWDALog(data) if err != nil { - return errors.Wrap(err, "failed to start WDA log recording") + return errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("start WDA log recording failed: %v", err)) } return nil @@ -875,12 +876,14 @@ func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { rawResp, err := wd.triggerWDALog(data) if err != nil { log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to get WDA logs") - return "", errors.Wrap(err, "failed to get WDA logs") + return "", errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("get WDA logs failed: %v", err)) } reply := new(wdaResponse) if err = json.Unmarshal(rawResp, reply); err != nil { log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to json.Unmarshal WDA logs") - return reply, err + return reply, errors.Wrap(code.IOSCaptureLogError, + fmt.Sprintf("json.Unmarshal WDA logs failed: %v", err)) } log.Info().Interface("value", reply.Value).Msg("get WDA log response") return reply.Value, nil diff --git a/hrp/runner.go b/hrp/runner.go index 1b164226..63e5b407 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -560,7 +560,7 @@ func (r *SessionRunner) ParseStepVariables(stepVariables map[string]interface{}) if err != nil { log.Error().Interface("variables", r.caseRunner.parsedConfig.Variables). Err(err).Msg("parse step variables failed") - return nil, err + return nil, errors.Wrap(err, "parse step variables failed") } return parsedVariables, nil } diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index a6dad716..90985d9e 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) @@ -609,10 +610,11 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // run actions for _, action := range actions { if action.Params, err = s.caseRunner.parser.Parse(action.Params, stepVariables); err != nil { - return stepResult, errors.Wrap(err, "parse action params failed") + return stepResult, errors.Wrap(code.ParseError, + fmt.Sprintf("parse action params failed: %v", err)) } if err := uiDriver.DoAction(action); err != nil { - return stepResult, err + return stepResult, errors.Wrap(code.MobileUIDriverError, err.Error()) } } @@ -629,6 +631,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // validate validateResults, err := validateUI(uiDriver, step.Validators) if err != nil { + err = errors.Wrap(code.MobileUIValidationError, err.Error()) return } sessionData := newSessionData() diff --git a/httprunner/__init__.py b/httprunner/__init__.py index c0be7a21..249a8d92 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10172144" +__version__ = "v4.3.0-beta-10191154" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 747bbbfb..43999353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10172144" +version = "v4.3.0-beta-10191154" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 278649b8f395fc71d40bc5e029e3f9ac24afbfa4 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 21 Oct 2022 15:17:20 +0800 Subject: [PATCH 166/169] change: update exit code --- examples/uitest/wda_log_data.json | 55 ++++++++------ examples/uitest/wda_log_test.go | 9 ++- hrp/internal/code/code.go | 116 ++++++++++++++++-------------- hrp/internal/version/VERSION | 2 +- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 6 files changed, 102 insertions(+), 84 deletions(-) diff --git a/examples/uitest/wda_log_data.json b/examples/uitest/wda_log_data.json index 06b012ef..0237e46a 100644 --- a/examples/uitest/wda_log_data.json +++ b/examples/uitest/wda_log_data.json @@ -13,29 +13,6 @@ ] }, "teststeps": [ - { - "name": "查看时间戳", - "ios": { - "actions": [ - { - "method": "home" - }, - { - "method": "app_terminate", - "params": "com.apple.mobilesafari" - }, - { - "method": "swipe_to_tap_app", - "params": "timestamp", - "max_retry_times": 5 - }, - { - "method": "sleep", - "params": 3 - } - ] - } - }, { "name": "启动抖音", "ios": { @@ -80,6 +57,38 @@ ] } }, + { + "name": "进入购物页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "购物", + "identifier": "点击购物" + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "进入推荐页", + "ios": { + "actions": [ + { + "method": "tap_ocr", + "params": "推荐", + "identifier": "点击推荐" + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, { "name": "向上滑动 2 次", "ios": { diff --git a/examples/uitest/wda_log_test.go b/examples/uitest/wda_log_test.go index a0e2b231..54e7a027 100644 --- a/examples/uitest/wda_log_test.go +++ b/examples/uitest/wda_log_test.go @@ -16,11 +16,6 @@ func TestWDALog(t *testing.T) { }). SetIOS(hrp.WithLogOn(true), hrp.WithWDAPort(8700), hrp.WithWDAMjpegPort(8800)), TestSteps: []hrp.IStep{ - hrp.NewStep("查看时间戳"). - IOS(). - Home(). - AppTerminate("com.apple.mobilesafari"). - SwipeToTapApp("timestamp", hrp.WithMaxRetryTimes(5)).Sleep(3), hrp.NewStep("启动抖音"). IOS(). Home(). @@ -31,6 +26,10 @@ func TestWDALog(t *testing.T) { hrp.NewStep("处理青少年弹窗"). IOS(). TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)), + hrp.NewStep("进入购物页"). + IOS().TapByOCR("购物", hrp.WithIdentifier("点击购物")).Sleep(5), + hrp.NewStep("进入推荐页"). + IOS().TapByOCR("推荐", hrp.WithIdentifier("点击推荐")).Sleep(5), hrp.NewStep("向上滑动 2 次"). IOS(). SwipeUp(hrp.WithIdentifier("第 1 次上划")).Sleep(2). diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index b2e6eb60..d0c56e3e 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -4,92 +4,102 @@ import ( "github.com/pkg/errors" ) -// general: [0, 20) +// general: [0, 2) const ( Success = 0 GeneralFail = 1 ) -// loader: [20, 40) +// environment: [2, 10) + +// loader: [10, 20) var ( - LoadError = errors.New("load error") // 20 - LoadJSONError = errors.New("load json error") // 21 - LoadYAMLError = errors.New("load yaml error") // 22 + LoadError = errors.New("load error") // 10 + LoadJSONError = errors.New("load json error") // 11 + LoadYAMLError = errors.New("load yaml error") // 12 ) -// parser: [40, 60) +// parser: [20, 30) var ( - ParseError = errors.New("parse error") // 40 - ParseConfigError = errors.New("parse config error") // 41 - ParseStringError = errors.New("parse string failed") // 42 - ParseVariablesError = errors.New("parse variables failed") // 43 + ParseError = errors.New("parse error") // 20 + ParseStringError = errors.New("parse string failed") // 21 + ParseVariablesError = errors.New("parse variables failed") // 22 + ParseConfigError = errors.New("parse config error") // 25 ) -// runner: [60, 100) +// runner: [30, 40) -// ios related: [100, 130) +// summary: [40, 50) + +// ios device related: [50, 60) var ( - IOSDeviceConnectionError = errors.New("ios device connection error") // 100 - IOSDeviceHTTPDriverError = errors.New("ios device HTTP driver error") // 101 - IOSDeviceUSBDriverError = errors.New("ios device USB driver error") // 102 - IOSScreenShotError = errors.New("ios screenshot error") // 110 - IOSCaptureLogError = errors.New("ios capture log error") // 111 - - MobileUIDriverError = errors.New("mobile UI driver error") // 120 - MobileUIValidationError = errors.New("mobile UI validation error") // 121 + IOSDeviceConnectionError = errors.New("ios device connection error") // 50 + IOSDeviceHTTPDriverError = errors.New("ios device HTTP driver error") // 51 + IOSDeviceUSBDriverError = errors.New("ios device USB driver error") // 52 + IOSScreenShotError = errors.New("ios screenshot error") // 55 + IOSCaptureLogError = errors.New("ios capture log error") // 56 ) -// android related: [130, 160) +// android device related: [60, 70) var ( - AndroidDeviceConnectionError = errors.New("android device connection error") // 130 - AndroidDeviceDriverError = errors.New("android device driver error") // 131 - AndroidScreenShotError = errors.New("android screenshot error") // 150 - AndroidCaptureLogError = errors.New("android capture log error") // 151 + AndroidDeviceConnectionError = errors.New("android device connection error") // 60 + AndroidDeviceDriverError = errors.New("android device driver error") // 61 + AndroidScreenShotError = errors.New("android screenshot error") // 65 + AndroidCaptureLogError = errors.New("android capture log error") // 66 ) -// OCR related: [160, 180) +// UI automation related: [70, 80) var ( - OCREnvMissedError = errors.New("veDEM OCR env missed error") // 160 - OCRRequestError = errors.New("vedem ocr prepare request error") // 161 - OCRServiceConnectionError = errors.New("vedem ocr service connect error") // 162 - OCRResponseError = errors.New("vedem ocr parse response error") // 163 - OCRTextNotFoundError = errors.New("vedem ocr text not found") // 164 + MobileUIDriverError = errors.New("mobile UI driver error") // 70 + MobileUIValidationError = errors.New("mobile UI validation error") // 75 ) -// CV related: [180, 200) +// OCR related: [80, 90) +var ( + OCREnvMissedError = errors.New("OCR env missed error") // 80 + OCRRequestError = errors.New("OCR prepare request error") // 81 + OCRServiceConnectionError = errors.New("OCR service connect error") // 82 + OCRResponseError = errors.New("OCR parse response error") // 83 + OCRTextNotFoundError = errors.New("OCR text not found") // 84 +) -// report related: [200, 220) +// CV related: [90, 100) var errorsMap = map[error]int{ // loader - LoadJSONError: 10, - LoadYAMLError: 11, + LoadError: 10, + LoadJSONError: 11, + LoadYAMLError: 12, // parser - ParseError: 40, - ParseConfigError: 41, - ParseStringError: 42, - ParseVariablesError: 43, + ParseError: 20, + ParseStringError: 21, + ParseVariablesError: 22, + ParseConfigError: 25, // ios related - IOSDeviceConnectionError: 100, - IOSDeviceHTTPDriverError: 101, - IOSDeviceUSBDriverError: 102, - IOSScreenShotError: 110, - IOSCaptureLogError: 111, + IOSDeviceConnectionError: 50, + IOSDeviceHTTPDriverError: 51, + IOSDeviceUSBDriverError: 52, + IOSScreenShotError: 55, + IOSCaptureLogError: 56, // android related - AndroidDeviceConnectionError: 130, - AndroidDeviceDriverError: 131, - AndroidScreenShotError: 150, - AndroidCaptureLogError: 151, + AndroidDeviceConnectionError: 60, + AndroidDeviceDriverError: 61, + AndroidScreenShotError: 65, + AndroidCaptureLogError: 66, + + // UI automation related + MobileUIDriverError: 70, + MobileUIValidationError: 75, // OCR related - OCREnvMissedError: 160, - OCRRequestError: 161, - OCRServiceConnectionError: 162, - OCRResponseError: 163, - OCRTextNotFoundError: 164, + OCREnvMissedError: 80, + OCRRequestError: 81, + OCRServiceConnectionError: 82, + OCRResponseError: 83, + OCRTextNotFoundError: 84, } func GetErrorCode(err error) int { diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 902617de..c510ef4d 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10191154 \ No newline at end of file +v4.3.0-beta-10211516 \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 249a8d92..83cf4c2c 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10191154" +__version__ = "v4.3.0-beta-10211516" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 43999353..8699011b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10191154" +version = "v4.3.0-beta-10211516" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 775e42f7fa348f53325ad48ec6ca3e8fb611c22e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 21 Oct 2022 15:41:30 +0800 Subject: [PATCH 167/169] change: print hrp exit code --- hrp/internal/code/code.go | 11 ++++++++--- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/ios_device.go | 4 ++-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index d0c56e3e..e47b729a 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -2,6 +2,7 @@ package code import ( "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) // general: [0, 2) @@ -102,15 +103,19 @@ var errorsMap = map[error]int{ OCRTextNotFoundError: 84, } -func GetErrorCode(err error) int { +func GetErrorCode(err error) (exitCode int) { if err == nil { + log.Info().Int("code", Success).Msg("hrp exit") return Success } e := errors.Cause(err) if code, ok := errorsMap[e]; ok { - return code + exitCode = code + } else { + exitCode = GeneralFail } - return GeneralFail + log.Warn().Int("code", exitCode).Msg("hrp exit") + return } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index c510ef4d..6581f6f8 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10211516 \ No newline at end of file +v4.3.0-beta-10211541 \ No newline at end of file diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index fb8afdd6..6b2cadc1 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -306,8 +306,8 @@ func (dev *IOSDevice) forward(localPort, remotePort int) error { rInnerConn, err := device.NewConnect(remotePort) if err != nil { - log.Error().Err(err).Msg("connect to device failed") - os.Exit(1) + log.Error().Err(err).Msg("connect to ios device failed") + os.Exit(code.GetErrorCode(code.IOSDeviceConnectionError)) } rConn := rInnerConn.RawConn() diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 83cf4c2c..866a4993 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10211516" +__version__ = "v4.3.0-beta-10211541" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 8699011b..61308132 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10211516" +version = "v4.3.0-beta-10211541" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 1f7737d0e7f95c7e14fea25610007e984eb1d44d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 21 Oct 2022 20:35:02 +0800 Subject: [PATCH 168/169] change: add exit code --- hrp/boomer.go | 9 +++--- hrp/build.go | 29 ++++++++++++------ hrp/cmd/boom.go | 12 ++++---- hrp/cmd/curl.go | 14 +++++---- hrp/cmd/scaffold.go | 3 +- hrp/internal/builtin/utils.go | 18 ++++++----- hrp/internal/code/code.go | 56 +++++++++++++++++++++++++++-------- hrp/internal/myexec/cmd.go | 3 +- hrp/loader.go | 2 +- hrp/parser.go | 19 +++++++----- hrp/plugin.go | 5 +++- hrp/runner_test.go | 5 ++-- hrp/step_request.go | 5 ++-- hrp/testcase.go | 28 ++++++++++++------ 14 files changed, 137 insertions(+), 71 deletions(-) diff --git a/hrp/boomer.go b/hrp/boomer.go index c3db1e76..4e3ecef5 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -14,6 +14,7 @@ import ( "golang.org/x/net/context" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/pkg/boomer" @@ -121,7 +122,7 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS testCases, err := LoadTestCases(testcases...) if err != nil { log.Error().Err(err).Msg("failed to load testcases") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } for _, testcase := range testCases { @@ -139,7 +140,7 @@ func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TCase { caseRunner, err := b.hrpRunner.NewCaseRunner(tc) if err != nil { log.Error().Err(err).Msg("failed to create runner") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } caseRunner.parsedConfig.Parameters = caseRunner.parametersIterator.outParameters() parsedTestCases = append(parsedTestCases, &TCase{ @@ -155,7 +156,7 @@ func (b *HRPBoomer) TestCasesToBytes(testcases ...ITestCase) []byte { testCases, err := LoadTestCases(testcases...) if err != nil { log.Error().Err(err).Msg("failed to load testcases") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } tcs := b.ParseTestCases(testCases) testCasesBytes, err := json.Marshal(tcs) @@ -318,7 +319,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend caseRunner, err := b.hrpRunner.NewCaseRunner(testcase) if err != nil { log.Error().Err(err).Msg("failed to create runner") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } if caseRunner.parser.plugin != nil { b.pluginsMutex.Lock() diff --git a/hrp/build.go b/hrp/build.go index bf950fde..491bdb8f 100644 --- a/hrp/build.go +++ b/hrp/build.go @@ -15,6 +15,7 @@ import ( "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/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/version" ) @@ -175,11 +176,11 @@ func buildGo(path string, output string) error { content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") - return errors.Wrap(err, "read file failed") + return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexGoFunctionName.findAllFunctionNames(string(content)) if err != nil { - return err + return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ @@ -187,7 +188,11 @@ func buildGo(path string, output string) error { Version: version.VERSION, FunctionNames: functionNames, } - return templateContent.generateGo(output) + err = templateContent.generateGo(output) + if err != nil { + return errors.Wrap(code.BuildGoPluginFailed, err.Error()) + } + return nil } // buildPy completes funppy information in debugtalk.py @@ -196,17 +201,18 @@ func buildPy(path string, output string) error { // check the syntax of debugtalk.py err := myexec.ExecPython3Command("py_compile", path) if err != nil { - return errors.Wrap(err, "python plugin syntax invalid") + return errors.Wrap(code.InvalidPluginFile, + fmt.Sprintf("python plugin syntax invalid: %s", err.Error())) } content, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("failed to read file") - return errors.Wrap(err, "read file failed") + return errors.Wrap(code.LoadFileError, err.Error()) } functionNames, err := regexPyFunctionName.findAllFunctionNames(string(content)) if err != nil { - return err + return errors.Wrap(code.InvalidPluginFile, err.Error()) } templateContent := &pluginTemplate{ @@ -214,7 +220,11 @@ func buildPy(path string, output string) error { Version: version.VERSION, FunctionNames: functionNames, } - return templateContent.generatePy(output) + err = templateContent.generatePy(output) + if err != nil { + return errors.Wrap(code.BuildPyPluginFailed, err.Error()) + } + return nil } func BuildPlugin(path string, output string) (err error) { @@ -225,11 +235,12 @@ func BuildPlugin(path string, output string) (err error) { case ".go": err = buildGo(path, output) default: - return errors.New("type error, expected .py or .go") + return errors.Wrap(code.UnsupportedFileExtension, + "type error, expected .py or .go") } if err != nil { log.Error().Err(err).Str("path", path).Msg("build plugin failed") - os.Exit(1) + return err } return nil } diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index d5f853f4..08cff659 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -1,7 +1,6 @@ package cmd import ( - "os" "strings" "time" @@ -30,7 +29,7 @@ var boomCmd = &cobra.Command{ } setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { var paths []hrp.ITestCase for _, arg := range args { path := hrp.TestCasePath(arg) @@ -42,7 +41,7 @@ var boomCmd = &cobra.Command{ err := builtin.LoadFile(boomArgs.profile, &boomArgs.Profile) if err != nil { log.Error().Err(err).Msg("failed to load profile") - os.Exit(1) + return err } } @@ -89,6 +88,7 @@ var boomCmd = &cobra.Command{ hrpBoomer.InitBoomer() hrpBoomer.Run(paths...) } + return nil }, } @@ -141,13 +141,13 @@ func init() { boomCmd.Flags().IntVar(&boomArgs.expectWorkersMaxWait, "expect-workers-max-wait", 120, "How many workers master should expect to connect before starting the test (only when --autostart is used") } -func makeHRPBoomer() *hrp.HRPBoomer { +func makeHRPBoomer() (*hrp.HRPBoomer, error) { // if set profile, the priority is higher than the other commands if boomArgs.profile != "" { err := builtin.LoadFile(boomArgs.profile, &boomArgs) if err != nil { log.Error().Err(err).Msg("failed to load profile") - os.Exit(1) + return nil, err } } hrpBoomer := hrp.NewStandaloneBoomer(boomArgs.SpawnCount, boomArgs.SpawnRate) @@ -157,5 +157,5 @@ func makeHRPBoomer() *hrp.HRPBoomer { hrpBoomer.SetProfile(&boomArgs.Profile) hrpBoomer.EnableGracefulQuit(context.Background()) hrpBoomer.InitBoomer() - return hrpBoomer + return hrpBoomer, nil } diff --git a/hrp/cmd/curl.go b/hrp/cmd/curl.go index 02c18a7f..2fdda0cc 100644 --- a/hrp/cmd/curl.go +++ b/hrp/cmd/curl.go @@ -21,11 +21,9 @@ var runCurlCmd = &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { runner := makeHRPRunner() - if runner.Run(makeCurlTestCase(args)) != nil { - os.Exit(1) - } + return runner.Run(makeCurlTestCase(args)) }, } @@ -41,9 +39,13 @@ var boomCurlCmd = &cobra.Command{ } setLogLevel(logLevel) }, - Run: func(cmd *cobra.Command, args []string) { - boomer := makeHRPBoomer() + RunE: func(cmd *cobra.Command, args []string) error { + boomer, err := makeHRPBoomer() + if err != nil { + return err + } boomer.Run(makeCurlTestCase(args)) + return nil }, } diff --git a/hrp/cmd/scaffold.go b/hrp/cmd/scaffold.go index 86830725..b127b0ab 100644 --- a/hrp/cmd/scaffold.go +++ b/hrp/cmd/scaffold.go @@ -2,7 +2,6 @@ package cmd import ( "errors" - "os" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -37,7 +36,7 @@ var scaffoldCmd = &cobra.Command{ err := scaffold.CreateScaffold(args[0], pluginType, venv, force) if err != nil { log.Error().Err(err).Msg("create scaffold project failed") - os.Exit(1) + return err } log.Info().Str("projectName", args[0]).Msg("create scaffold success") return nil diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index c79939c8..bfa9c323 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -22,6 +22,7 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -235,8 +236,6 @@ func InterfaceType(raw interface{}) string { return reflect.TypeOf(raw).String() } -var ErrUnsupportedFileExt = fmt.Errorf("unsupported file extension") - // LoadFile loads file content with file extension and assigns to structObj func LoadFile(path string, structObj interface{}) (err error) { log.Info().Str("path", path).Msg("load file") @@ -252,12 +251,15 @@ func LoadFile(path string, structObj interface{}) (err error) { decoder := json.NewDecoder(bytes.NewReader(file)) decoder.UseNumber() err = decoder.Decode(structObj) + err = errors.Wrap(code.LoadJSONError, err.Error()) case ".yaml", ".yml": err = yaml.Unmarshal(file, structObj) + err = errors.Wrap(code.LoadYAMLError, err.Error()) case ".env": err = parseEnvContent(file, structObj) + err = errors.Wrap(code.LoadEnvError, err.Error()) default: - err = ErrUnsupportedFileExt + err = code.UnsupportedFileExtension } return err } @@ -297,14 +299,14 @@ func loadFromCSV(path string) []map[string]interface{} { file, err := ReadFile(path) if err != nil { log.Error().Err(err).Msg("read csv file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } r := csv.NewReader(strings.NewReader(string(file))) content, err := r.ReadAll() if err != nil { log.Error().Err(err).Msg("parse csv file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } firstLine := content[0] // parameter names var result []map[string]interface{} @@ -323,7 +325,7 @@ func loadMessage(path string) []byte { file, err := ReadFile(path) if err != nil { log.Error().Err(err).Msg("read message file failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return file } @@ -333,13 +335,13 @@ func ReadFile(path string) ([]byte, error) { path, err = filepath.Abs(path) if err != nil { log.Error().Err(err).Str("path", path).Msg("convert absolute path failed") - return nil, err + return nil, errors.Wrap(code.LoadFileError, err.Error()) } file, err := os.ReadFile(path) if err != nil { log.Error().Err(err).Msg("read file failed") - return nil, err + return nil, errors.Wrap(code.LoadFileError, err.Error()) } return file, nil } diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index e47b729a..9bf517b1 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -12,24 +12,40 @@ const ( ) // environment: [2, 10) +var ( + InvalidPython3Venv = errors.New("prepare python3 venv failed") // 9 +) // loader: [10, 20) var ( - LoadError = errors.New("load error") // 10 - LoadJSONError = errors.New("load json error") // 11 - LoadYAMLError = errors.New("load yaml error") // 12 + LoadFileError = errors.New("load file error") // 10 + LoadJSONError = errors.New("load json error") // 11 + LoadYAMLError = errors.New("load yaml error") // 12 + LoadEnvError = errors.New("load .env error") // 13 + LoadCSVError = errors.New("load csv error") // 14 + InvalidCaseFormat = errors.New("invalid case format") // 15 + UnsupportedFileExtension = errors.New("unsupported file extension") // 16 + ReferencedFileNotFound = errors.New("referenced file not found") // 17 + InvalidPluginFile = errors.New("invalid plugin file") // 18 ) // parser: [20, 30) var ( ParseError = errors.New("parse error") // 20 - ParseStringError = errors.New("parse string failed") // 21 - ParseVariablesError = errors.New("parse variables failed") // 22 - ParseConfigError = errors.New("parse config error") // 25 + VariableNotFound = errors.New("variable not found") // 21 + ParseFunctionError = errors.New("parse function failed") // 22 + CallFunctionError = errors.New("call function failed") // 23 + ParseVariablesError = errors.New("parse variables failed") // 24 ) // runner: [30, 40) +var ( + InitPluginFailed = errors.New("init plugin failed") // 31 + BuildGoPluginFailed = errors.New("build go plugin failed") // 32 + BuildPyPluginFailed = errors.New("build py plugin failed") // 33 +) + // summary: [40, 50) // ios device related: [50, 60) @@ -67,16 +83,31 @@ var ( // CV related: [90, 100) var errorsMap = map[error]int{ + // environment + InvalidPython3Venv: 9, + // loader - LoadError: 10, - LoadJSONError: 11, - LoadYAMLError: 12, + LoadFileError: 10, + LoadJSONError: 11, + LoadYAMLError: 12, + LoadEnvError: 13, + LoadCSVError: 14, + InvalidCaseFormat: 15, + UnsupportedFileExtension: 16, + ReferencedFileNotFound: 17, + InvalidPluginFile: 18, // parser ParseError: 20, - ParseStringError: 21, - ParseVariablesError: 22, - ParseConfigError: 25, + VariableNotFound: 21, + ParseFunctionError: 22, + CallFunctionError: 23, + ParseVariablesError: 24, + + // runner + InitPluginFailed: 31, + BuildGoPluginFailed: 32, + BuildPyPluginFailed: 33, // ios related IOSDeviceConnectionError: 50, @@ -105,7 +136,6 @@ var errorsMap = map[error]int{ func GetErrorCode(err error) (exitCode int) { if err == nil { - log.Info().Int("code", Success).Msg("hrp exit") return Success } diff --git a/hrp/internal/myexec/cmd.go b/hrp/internal/myexec/cmd.go index 3876008e..b92e312e 100644 --- a/hrp/internal/myexec/cmd.go +++ b/hrp/internal/myexec/cmd.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/env" ) @@ -39,7 +40,7 @@ func EnsurePython3Venv(venv string, packages ...string) (python3 string, err err } python3, err = ensurePython3Venv(venv, packages...) if err != nil { - return "", errors.Wrap(err, "prepare python3 venv failed") + return "", errors.Wrap(code.InvalidPython3Venv, err.Error()) } python3Executable = python3 log.Info().Str("Python3Executable", python3Executable).Msg("set python3 executable path") diff --git a/hrp/loader.go b/hrp/loader.go index 0f75ef95..9d2ecf8c 100644 --- a/hrp/loader.go +++ b/hrp/loader.go @@ -54,7 +54,7 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { tc, err := testCasePath.ToTestCase() if err != nil { log.Warn().Err(err).Str("path", path).Msg("load testcase failed") - return nil + return err } testCases = append(testCases, tc) return nil diff --git a/hrp/parser.go b/hrp/parser.go index 290a5f07..34fb4f73 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -125,14 +125,19 @@ func (p *Parser) Parse(raw interface{}, variablesMapping map[string]interface{}) } } -func parseJSONNumber(raw builtinJSON.Number) (interface{}, error) { +func parseJSONNumber(raw builtinJSON.Number) (value interface{}, err error) { if strings.Contains(raw.String(), ".") { // float64 - return raw.Float64() + value, err = raw.Float64() } else { // int64 - return raw.Int64() + value, err = raw.Int64() } + if err != nil { + return nil, errors.Wrap(code.ParseError, + fmt.Sprintf("parse json number failed: %v", err)) + } + return value, nil } const ( @@ -185,18 +190,18 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} argsStr := funcMatched[2] arguments, err := parseFunctionArguments(argsStr) if err != nil { - return raw, errors.Wrap(code.ParseStringError, err.Error()) + return raw, errors.Wrap(code.ParseFunctionError, err.Error()) } parsedArgs, err := p.Parse(arguments, variablesMapping) if err != nil { - return raw, errors.Wrap(code.ParseStringError, err.Error()) + return raw, err } result, err := p.callFunc(funcName, parsedArgs.([]interface{})...) if err != nil { log.Error().Str("funcName", funcName).Interface("arguments", arguments). Err(err).Msg("call function failed") - return raw, errors.Wrap(code.ParseStringError, err.Error()) + return raw, errors.Wrap(code.CallFunctionError, err.Error()) } log.Info().Str("funcName", funcName).Interface("arguments", arguments). Interface("output", result).Msg("call function success") @@ -228,7 +233,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} } varValue, ok := variablesMapping[varName] if !ok { - return raw, errors.Wrap(code.ParseStringError, + return raw, errors.Wrap(code.VariableNotFound, fmt.Sprintf("variable %s not found", varName)) } diff --git a/hrp/plugin.go b/hrp/plugin.go index f28558a2..8e8fc875 100644 --- a/hrp/plugin.go +++ b/hrp/plugin.go @@ -9,8 +9,10 @@ import ( "github.com/httprunner/funplugin" "github.com/httprunner/funplugin/fungo" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/internal/myexec" "github.com/httprunner/httprunner/v4/hrp/internal/sdk" ) @@ -52,7 +54,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er err = BuildPlugin(pluginPath, genPyPluginPath) if err != nil { log.Error().Err(err).Str("path", pluginPath).Msg("build plugin failed") - return nil, nil + return nil, err } pluginPath = genPyPluginPath @@ -73,6 +75,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er plugin, err = funplugin.Init(pluginPath, pluginOptions...) if err != nil { log.Error().Err(err).Msgf("init plugin failed: %s", pluginPath) + err = errors.Wrap(code.InitPluginFailed, err.Error()) return } diff --git a/hrp/runner_test.go b/hrp/runner_test.go index 383cae09..c07cf1f1 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func buildHashicorpGoPlugin() { err := BuildPlugin(tmpl("plugin/debugtalk.go"), tmpl("debugtalk.bin")) if err != nil { log.Error().Err(err).Msg("build hashicorp go plugin failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } } @@ -33,7 +34,7 @@ func buildHashicorpPyPlugin() { err := ioutil.WriteFile(tmpl("debugtalk.py"), src, 0o644) if err != nil { log.Error().Err(err).Msg("copy hashicorp python plugin failed") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } } diff --git a/hrp/step_request.go b/hrp/step_request.go index 89269d6f..4a25d02e 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -21,6 +21,7 @@ import ( "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/json" "github.com/httprunner/httprunner/v4/hrp/pkg/httpstat" ) @@ -683,7 +684,7 @@ func (s *StepRequest) CallRefCase(tc ITestCase) *StepTestCaseWithOptionalArgs { s.step.TestCase, err = tc.ToTestCase() if err != nil { log.Error().Err(err).Msg("failed to load testcase") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return &StepTestCaseWithOptionalArgs{ step: s.step, @@ -696,7 +697,7 @@ func (s *StepRequest) CallRefAPI(api IAPI) *StepAPIWithOptionalArgs { s.step.API, err = api.ToAPI() if err != nil { log.Error().Err(err).Msg("failed to load api") - os.Exit(1) + os.Exit(code.GetErrorCode(err)) } return &StepAPIWithOptionalArgs{ step: s.step, diff --git a/hrp/testcase.go b/hrp/testcase.go index b9733b57..67d0d715 100644 --- a/hrp/testcase.go +++ b/hrp/testcase.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/code" ) // ITestCase represents interface for testcases, @@ -117,7 +118,8 @@ func (tc *TCase) MakeCompat() (err error) { func (tc *TCase) ToTestCase(casePath string) (*TestCase, error) { if tc.TestSteps == nil { - return nil, errors.New("invalid testcase format, missing teststeps!") + return nil, errors.Wrap(code.InvalidCaseFormat, + "invalid testcase format, missing teststeps!") } if tc.Config == nil { @@ -169,7 +171,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { if ok { path := filepath.Join(projectRootDir, apiPath) if !builtin.IsFilePathExists(path) { - return nil, errors.New("referenced api file not found: " + path) + return nil, errors.Wrap(code.ReferencedFileNotFound, + fmt.Sprintf("referenced api file not found: %s", path)) } refAPI := APIPath(path) @@ -181,7 +184,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } else { apiMap, ok := step.API.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("referenced api should be map or path(string), got %v", step.API) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("referenced api should be map or path(string), got %v", step.API)) } api := &API{} err = mapstructure.Decode(apiMap, api) @@ -192,7 +196,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } _, ok = step.API.(*API) if !ok { - return nil, fmt.Errorf("failed to handle referenced API, got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("failed to handle referenced API, got %v", step.TestCase)) } testCase.TestSteps = append(testCase.TestSteps, &StepAPIWithOptionalArgs{ step: step, @@ -202,7 +207,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { if ok { path := filepath.Join(projectRootDir, casePath) if !builtin.IsFilePathExists(path) { - return nil, errors.New("referenced testcase file not found: " + path) + return nil, errors.Wrap(code.ReferencedFileNotFound, + fmt.Sprintf("referenced testcase file not found: %s", path)) } refTestCase := TestCasePath(path) @@ -214,7 +220,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } else { testCaseMap, ok := step.TestCase.(map[string]interface{}) if !ok { - return nil, fmt.Errorf("referenced testcase should be map or path(string), got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("referenced testcase should be map or path(string), got %v", step.TestCase)) } tCase := &TCase{} err = mapstructure.Decode(testCaseMap, tCase) @@ -229,7 +236,8 @@ func (tc *TCase) toTestCase() (*TestCase, error) { } _, ok = step.TestCase.(*TestCase) if !ok { - return nil, fmt.Errorf("failed to handle referenced testcase, got %v", step.TestCase) + return nil, errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("failed to handle referenced testcase, got %v", step.TestCase)) } testCase.TestSteps = append(testCase.TestSteps, &StepTestCaseWithOptionalArgs{ step: step, @@ -317,7 +325,8 @@ func convertCompatValidator(Validators []interface{}) (err error) { for assertMethod, iValidatorContent := range validatorMap { validatorContent := iValidatorContent.([]interface{}) if len(validatorContent) > 3 { - return fmt.Errorf("unexpected validator format: %v", validatorMap) + return errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("unexpected validator format: %v", validatorMap)) } validator.Check = validatorContent[0].(string) validator.Assert = assertMethod @@ -330,7 +339,8 @@ func convertCompatValidator(Validators []interface{}) (err error) { Validators[i] = validator continue } - return fmt.Errorf("unexpected validator format: %v", validatorMap) + return errors.Wrap(code.InvalidCaseFormat, + fmt.Sprintf("unexpected validator format: %v", validatorMap)) } return nil } From be523eda0c4f22290e8323f1bcee6614640cd182 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 21 Oct 2022 21:57:54 +0800 Subject: [PATCH 169/169] release v4.3.0 --- docs/CHANGELOG.md | 3 ++- docs/cmd/hrp.md | 2 +- docs/cmd/hrp_boom.md | 3 ++- docs/cmd/hrp_boom_curl.md | 2 +- docs/cmd/hrp_build.md | 2 +- docs/cmd/hrp_convert.md | 2 +- docs/cmd/hrp_convert_curl.md | 2 +- docs/cmd/hrp_curl.md | 2 +- docs/cmd/hrp_dns.md | 2 +- docs/cmd/hrp_ping.md | 2 +- docs/cmd/hrp_pytest.md | 2 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_run_curl.md | 2 +- docs/cmd/hrp_startproject.md | 2 +- docs/cmd/hrp_traceroute.md | 2 +- docs/cmd/hrp_wiki.md | 2 +- examples/demo-empty-project/proj.json | 4 ++-- examples/demo-with-go-plugin/proj.json | 2 +- examples/demo-with-py-plugin/proj.json | 2 +- examples/demo-without-plugin/proj.json | 4 ++-- hrp/internal/builtin/utils.go | 12 +++++++++--- hrp/internal/code/code.go | 5 +++++ .../scaffold/templates/plugin/.debugtalk_gen.py | 3 ++- .../scaffold/templates/plugin/debugtalk_gen.go | 2 +- hrp/internal/version/VERSION | 2 +- hrp/loader.go | 3 +-- hrp/pkg/uixt/swipe.go | 2 +- hrp/step_mobile_ui.go | 9 +++++++-- httprunner/__init__.py | 2 +- pyproject.toml | 2 +- 30 files changed, 53 insertions(+), 35 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 432d07d7..f3aaf35c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,10 +1,11 @@ # Release History -## v4.3.0 (2022-10-16) +## v4.3.0 (2022-10-21) - feat: support iOS UI automation with [WebDriverAgent] - feat support Android UI automation with [uiautomator2] - feat: integrage ios device management with [gidevice] +- feat: add specified exit code for different exceptions - refactor: make boomer/uixt/httpstat as sub package ## v4.2.1 (2022-09-01) diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index fae4a004..e60bfc4c 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -41,4 +41,4 @@ Copyright 2017 debugtalk * [hrp traceroute](hrp_traceroute.md) - run integrated traceroute command * [hrp wiki](hrp_wiki.md) - visit https://httprunner.com -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index 20adfde0..c5c92782 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -44,6 +44,7 @@ hrp boom [flags] --profile string profile for load testing --prometheus-gateway string Prometheus Pushgateway url. --request-increase-rate string Request increase rate, disabled by default. (default "-1") + --run-time int Stop after the specified amount of time(s), Only used --autostart. Defaults to run forever. --spawn-count int The number of users to spawn for load testing (default 1) --spawn-rate float The rate for spawning users (default 1) --worker worker of distributed testing @@ -54,4 +55,4 @@ hrp boom [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp boom curl](hrp_boom_curl.md) - run load test with curl command -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_boom_curl.md b/docs/cmd/hrp_boom_curl.md index 87a0a66f..f8c7dcc5 100644 --- a/docs/cmd/hrp_boom_curl.md +++ b/docs/cmd/hrp_boom_curl.md @@ -16,4 +16,4 @@ hrp boom curl URLs [flags] * [hrp boom](hrp_boom.md) - run load test with boomer -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md index 0be74f06..69f024ff 100644 --- a/docs/cmd/hrp_build.md +++ b/docs/cmd/hrp_build.md @@ -28,4 +28,4 @@ hrp build $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 24b7ecd9..5f47069a 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -23,4 +23,4 @@ hrp convert $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp convert curl](hrp_convert_curl.md) - convert curl command to httprunner testcase -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_convert_curl.md b/docs/cmd/hrp_convert_curl.md index 9880780a..c6c2dde3 100644 --- a/docs/cmd/hrp_convert_curl.md +++ b/docs/cmd/hrp_convert_curl.md @@ -16,4 +16,4 @@ hrp convert curl URLs [flags] * [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_curl.md b/docs/cmd/hrp_curl.md index 82fe7511..5ef6e71d 100644 --- a/docs/cmd/hrp_curl.md +++ b/docs/cmd/hrp_curl.md @@ -16,4 +16,4 @@ hrp curl $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_dns.md b/docs/cmd/hrp_dns.md index 57f051d3..d2aaef60 100644 --- a/docs/cmd/hrp_dns.md +++ b/docs/cmd/hrp_dns.md @@ -26,4 +26,4 @@ hrp dns $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_ping.md b/docs/cmd/hrp_ping.md index 164652ad..0475d93b 100644 --- a/docs/cmd/hrp_ping.md +++ b/docs/cmd/hrp_ping.md @@ -20,4 +20,4 @@ hrp ping $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index b5eced08..3512a8ea 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -16,4 +16,4 @@ hrp pytest $path ... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 7079fc39..a0f70751 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -36,4 +36,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. * [hrp run curl](hrp_run_curl.md) - run API test with curl command -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_run_curl.md b/docs/cmd/hrp_run_curl.md index cdc3f54c..672a26c2 100644 --- a/docs/cmd/hrp_run_curl.md +++ b/docs/cmd/hrp_run_curl.md @@ -16,4 +16,4 @@ hrp run curl URLs [flags] * [hrp run](hrp_run.md) - run API test with go engine -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index d64f16dc..888c7457 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -21,4 +21,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_traceroute.md b/docs/cmd/hrp_traceroute.md index f959d880..a31d1dd1 100644 --- a/docs/cmd/hrp_traceroute.md +++ b/docs/cmd/hrp_traceroute.md @@ -19,4 +19,4 @@ hrp traceroute $url [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md index 56669a98..97986ea5 100644 --- a/docs/cmd/hrp_wiki.md +++ b/docs/cmd/hrp_wiki.md @@ -16,4 +16,4 @@ hrp wiki [flags] * [hrp](hrp.md) - Next-Generation API Testing Solution. -###### Auto generated by spf13/cobra on 21-Aug-2022 +###### Auto generated by spf13/cobra on 21-Oct-2022 diff --git a/examples/demo-empty-project/proj.json b/examples/demo-empty-project/proj.json index b2b376f6..edf45464 100644 --- a/examples/demo-empty-project/proj.json +++ b/examples/demo-empty-project/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-empty-project", - "create_time": "2022-07-11T11:45:29.942532+08:00", - "hrp_version": "v4.1.6" + "create_time": "2022-10-21T21:54:56.252853+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index 3225b92e..867531eb 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-go-plugin", - "create_time": "2022-09-28T16:40:14.674398+08:00", + "create_time": "2022-10-21T21:52:38.979867+08:00", "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index a2b30841..c0aeb776 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-py-plugin", - "create_time": "2022-09-28T16:40:15.283869+08:00", + "create_time": "2022-10-21T21:52:39.555851+08:00", "hrp_version": "v4.3.0" } diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json index 50c06186..593129a3 100644 --- a/examples/demo-without-plugin/proj.json +++ b/examples/demo-without-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-without-plugin", - "create_time": "2022-07-11T11:45:29.800018+08:00", - "hrp_version": "v4.1.6" + "create_time": "2022-10-21T21:54:56.136458+08:00", + "hrp_version": "v4.3.0" } diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index bfa9c323..2f109a5b 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -251,13 +251,19 @@ func LoadFile(path string, structObj interface{}) (err error) { decoder := json.NewDecoder(bytes.NewReader(file)) decoder.UseNumber() err = decoder.Decode(structObj) - err = errors.Wrap(code.LoadJSONError, err.Error()) + if err != nil { + err = errors.Wrap(code.LoadJSONError, err.Error()) + } case ".yaml", ".yml": err = yaml.Unmarshal(file, structObj) - err = errors.Wrap(code.LoadYAMLError, err.Error()) + if err != nil { + err = errors.Wrap(code.LoadYAMLError, err.Error()) + } case ".env": err = parseEnvContent(file, structObj) - err = errors.Wrap(code.LoadEnvError, err.Error()) + if err != nil { + err = errors.Wrap(code.LoadEnvError, err.Error()) + } default: err = code.UnsupportedFileExtension } diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index 9bf517b1..48557401 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -134,6 +134,11 @@ var errorsMap = map[error]int{ OCRTextNotFoundError: 84, } +func IsErrorPredefined(err error) bool { + _, ok := errorsMap[err] + return ok +} + func GetErrorCode(err error) (exitCode int) { if err == nil { return Success diff --git a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py index 708b20f9..d5c51015 100644 --- a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py +++ b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py @@ -1,4 +1,4 @@ -# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! +# NOTE: Generated By hrp v4.3.0, DO NOT EDIT! import sys import os @@ -10,6 +10,7 @@ from debugtalk import * if __name__ == "__main__": import funppy + funppy.register("get_user_agent", get_user_agent) funppy.register("sleep", sleep) funppy.register("sum", sum) diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go index db5b8409..9d08c9a0 100644 --- a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go +++ b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go @@ -1,4 +1,4 @@ -// NOTE: Generated By hrp v4.3.0-beta-10172144, DO NOT EDIT! +// NOTE: Generated By hrp v4.3.0, DO NOT EDIT! package main import ( diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index 6581f6f8..1ddc0f60 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.0-beta-10211541 \ No newline at end of file +v4.3.0 \ No newline at end of file diff --git a/hrp/loader.go b/hrp/loader.go index 9d2ecf8c..ff737c6d 100644 --- a/hrp/loader.go +++ b/hrp/loader.go @@ -53,8 +53,7 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) { testCasePath := TestCasePath(path) tc, err := testCasePath.ToTestCase() if err != nil { - log.Warn().Err(err).Str("path", path).Msg("load testcase failed") - return err + return nil } testCases = append(testCases, tc) return nil diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index a7187045..9d0089ff 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -84,7 +84,7 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, f } } else if d, ok := direction.([]float64); ok { if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3]); err != nil { - log.Error().Err(err).Msgf("swipe %s failed", d) + log.Error().Err(err).Msgf("swipe %v failed", d) } } else if d, ok := direction.([]interface{}); ok { sx, _ := builtin.Interface2Float64(d[0]) diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 90985d9e..3a0cd143 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -614,7 +614,10 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err fmt.Sprintf("parse action params failed: %v", err)) } if err := uiDriver.DoAction(action); err != nil { - return stepResult, errors.Wrap(code.MobileUIDriverError, err.Error()) + if !code.IsErrorPredefined(err) { + err = errors.Wrap(code.MobileUIDriverError, err.Error()) + } + return stepResult, err } } @@ -631,7 +634,9 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // validate validateResults, err := validateUI(uiDriver, step.Validators) if err != nil { - err = errors.Wrap(code.MobileUIValidationError, err.Error()) + if !code.IsErrorPredefined(err) { + err = errors.Wrap(code.MobileUIValidationError, err.Error()) + } return } sessionData := newSessionData() diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 866a4993..1f96dea8 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "v4.3.0-beta-10211541" +__version__ = "v4.3.0" __description__ = "One-stop solution for HTTP(S) testing." diff --git a/pyproject.toml b/pyproject.toml index 61308132..33947c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "v4.3.0-beta-10211541" +version = "v4.3.0" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md"