From 7a9349ea32dbcc141233fcacbe984a2b86fe4729 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 16 Apr 2023 12:33:13 +0800 Subject: [PATCH 1/9] fix: MobileUIAppNotInForegroundError exit code --- hrp/internal/code/code.go | 5 +++-- hrp/internal/version/VERSION | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hrp/internal/code/code.go b/hrp/internal/code/code.go index 354ff302..3479c8b6 100644 --- a/hrp/internal/code/code.go +++ b/hrp/internal/code/code.go @@ -124,8 +124,9 @@ var errorsMap = map[error]int{ AndroidCaptureLogError: 66, // UI automation related - MobileUIDriverError: 70, - MobileUIValidationError: 75, + MobileUIDriverError: 70, + MobileUIValidationError: 75, + MobileUIAppNotInForegroundError: 76, // OCR related OCREnvMissedError: 80, diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index f907911d..ff51d667 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.3.2304142356 \ No newline at end of file +v4.3.3.2304161232 \ No newline at end of file From a6ec63322a9c356e6234d791221229ffe8a1f882 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 16 Apr 2023 18:00:36 +0800 Subject: [PATCH 2/9] change: update go mod --- go.mod | 6 +++--- go.sum | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 593fc47d..f6975946 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.13.0 - github.com/rs/zerolog v1.28.0 + github.com/rs/zerolog v1.29.1 github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.5.0 @@ -56,7 +56,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.18 // 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 @@ -75,7 +75,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index c40a64ef..979a82b0 100644 --- a/go.sum +++ b/go.sum @@ -89,7 +89,7 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH 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/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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 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= @@ -301,8 +301,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd 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/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -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-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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= @@ -370,8 +371,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR 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.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= -github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 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= @@ -626,8 +627,9 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 3770ce12bdb2480079b7d2e29bff2288ddac7d85 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 16 Apr 2023 18:50:21 +0800 Subject: [PATCH 3/9] feat: add validator AssertAppInForeground and AssertAppNotInForeground --- docs/CHANGELOG.md | 3 +- examples/uitest/demo_feed_random_slide.json | 29 ++++++++++++++- .../uitest/demo_feed_random_slide_test.go | 9 ++++- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/android_adb_driver.go | 1 + hrp/pkg/uixt/ext.go | 37 ++++++++++++++----- hrp/step_mobile_ui.go | 30 +++++++++++++++ 7 files changed, 97 insertions(+), 14 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 35632507..723ca0d4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## v4.3.3 (2023-04-14) +## v4.3.3 (2023-04-16) **go version** @@ -9,6 +9,7 @@ - feat: add adb `screencap` sub command - feat: add `IsAppInForeground` to check if the given package is in foreground - feat: check if app is in foreground when step failed +- feat: add validator AssertAppInForeground and AssertAppNotInForeground - fix: adb driver for TapFloat - fix: stop logcat only when enabled - fix: do not fail case when kill logcat error diff --git a/examples/uitest/demo_feed_random_slide.json b/examples/uitest/demo_feed_random_slide.json index 5a89c1cc..d74f45ae 100644 --- a/examples/uitest/demo_feed_random_slide.json +++ b/examples/uitest/demo_feed_random_slide.json @@ -28,7 +28,15 @@ "params": 10 } ] - } + }, + "validate": [ + { + "check": "ui_foreground_app", + "assert": "equal", + "expect": "com.ss.android.ugc.aweme", + "msg": "app [com.ss.android.ugc.aweme] should be in foreground" + } + ] }, { "name": "处理青少年弹窗", @@ -102,6 +110,25 @@ ] }, "loops": 10 + }, + { + "name": "exit", + "android": { + "actions": [ + { + "method": "app_terminate", + "params": "com.ss.android.ugc.aweme" + } + ] + }, + "validate": [ + { + "check": "ui_foreground_app", + "assert": "not_equal", + "expect": "com.ss.android.ugc.aweme", + "msg": "app [com.ss.android.ugc.aweme] should not be in foreground" + } + ] } ] } diff --git a/examples/uitest/demo_feed_random_slide_test.go b/examples/uitest/demo_feed_random_slide_test.go index ad892b87..98311c69 100644 --- a/examples/uitest/demo_feed_random_slide_test.go +++ b/examples/uitest/demo_feed_random_slide_test.go @@ -21,7 +21,9 @@ func TestAndroidDouyinFeedTest(t *testing.T) { Android(). AppTerminate("com.ss.android.ugc.aweme"). AppLaunch("com.ss.android.ugc.aweme"). - Sleep(10), + Sleep(10). + Validate(). + AssertAppInForeground("com.ss.android.ugc.aweme"), hrp.NewStep("处理青少年弹窗"). Android(). TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)), @@ -40,6 +42,11 @@ func TestAndroidDouyinFeedTest(t *testing.T) { Android(). SwipeUp(). SleepRandom(0, 5, 0.7, 5, 10, 0.3), + hrp.NewStep("exit"). + Android(). + AppTerminate("com.ss.android.ugc.aweme"). + Validate(). + AssertAppNotInForeground("com.ss.android.ugc.aweme"), }, } diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index ff51d667..e73aa79d 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.3.2304161232 \ No newline at end of file +v4.3.3.2304161855 \ No newline at end of file diff --git a/hrp/pkg/uixt/android_adb_driver.go b/hrp/pkg/uixt/android_adb_driver.go index f373f924..68f8c28b 100644 --- a/hrp/pkg/uixt/android_adb_driver.go +++ b/hrp/pkg/uixt/android_adb_driver.go @@ -368,6 +368,7 @@ func (ad *adbDriver) IsAppInForeground(packageName string) (bool, error) { // adb shell dumpsys activity activities | grep mResumedActivity output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities") if err != nil { + log.Error().Err(err).Msg("failed to dumpsys activities") return false, err } diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index f110e107..093cb776 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -41,10 +41,15 @@ const ( RecordStop MobileMethod = "record_stop" // UI validation - SelectorName string = "ui_name" - SelectorLabel string = "ui_label" - SelectorOCR string = "ui_ocr" - SelectorImage string = "ui_image" + // selectors + SelectorName string = "ui_name" + SelectorLabel string = "ui_label" + SelectorOCR string = "ui_ocr" + SelectorImage string = "ui_image" + SelectorForegroundApp string = "ui_foreground_app" + // assertions + AssertionEqual string = "equal" + AssertionNotEqual string = "not_equal" AssertionExists string = "exists" AssertionNotExists string = "not_exists" @@ -349,6 +354,16 @@ func (dExt *DriverExt) IsImageExist(text string) bool { return err == nil } +func (dExt *DriverExt) IsAppInForeground(packageName string) bool { + // check if app is in foreground + yes, err := dExt.Driver.IsAppInForeground(packageName) + if !yes || err != nil { + log.Info().Str("packageName", packageName).Msg("app is not in foreground") + return false + } + return true +} + var errActionNotImplemented = errors.New("UI action not implemented") func convertToFloat64(val interface{}) (float64, error) { @@ -700,18 +715,20 @@ func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) } func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool { - var exists bool - if assert == AssertionExists { - exists = true + var exp bool + if assert == AssertionExists || assert == AssertionEqual { + exp = true } else { - exists = false + exp = false } var result bool switch check { case SelectorOCR: - result = (dExt.IsOCRExist(expected) == exists) + result = (dExt.IsOCRExist(expected) == exp) case SelectorImage: - result = (dExt.IsImageExist(expected) == exists) + result = (dExt.IsImageExist(expected) == exp) + case SelectorForegroundApp: + result = (dExt.IsAppInForeground(expected) == exp) } if !result { diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 99014a4c..f5483488 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -468,6 +468,36 @@ func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string, return s } +func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorForegroundApp, + Assert: uixt.AssertionEqual, + Expect: packageName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("app [%s] should be in foreground", packageName) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation { + v := Validator{ + Check: uixt.SelectorForegroundApp, + Assert: uixt.AssertionNotEqual, + Expect: packageName, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("app [%s] should not be in foreground", packageName) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepMobileUIValidation) Name() string { return s.step.Name } From 9e73328448e71d57bb801b5817d7becbd2f5ed57 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Sun, 16 Apr 2023 22:11:35 +0800 Subject: [PATCH 4/9] refactor: TakeScreenShot saves all screenshots --- examples/worldcup/main.go | 6 +--- hrp/cmd/adb/screencap.go | 5 ++-- hrp/internal/builtin/utils.go | 4 +++ hrp/pkg/gadb/device.go | 4 ++- hrp/pkg/uixt/ext.go | 53 ++++++++++++++++++----------------- hrp/pkg/uixt/interface.go | 12 -------- hrp/pkg/uixt/ocr_vedem.go | 17 ++--------- hrp/pkg/uixt/opencv.go | 6 ++-- hrp/step_mobile_ui.go | 15 ++++------ 9 files changed, 50 insertions(+), 72 deletions(-) diff --git a/examples/worldcup/main.go b/examples/worldcup/main.go index e67f024c..697bcd57 100644 --- a/examples/worldcup/main.go +++ b/examples/worldcup/main.go @@ -150,11 +150,7 @@ func NewWorldCupLive(device uixt.Device, matchName, bundleID string, duration, i func (wc *WorldCupLive) getCurrentLiveTime(utcTime time.Time) error { utcTimeStr := utcTime.Format("15:04:05") - fileName := filepath.Join( - wc.resultDir, "screenshot", utcTimeStr) - ocrTexts, err := wc.driver.GetTextsByOCR( - uixt.WithScreenShot(fileName), - ) + ocrTexts, err := wc.driver.GetTextsByOCR() if err != nil { log.Error().Err(err).Msg("get ocr texts failed") return err diff --git a/hrp/cmd/adb/screencap.go b/hrp/cmd/adb/screencap.go index d9166191..bb1ddf6c 100644 --- a/hrp/cmd/adb/screencap.go +++ b/hrp/cmd/adb/screencap.go @@ -3,9 +3,10 @@ package adb import ( "fmt" "io/ioutil" - "time" "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) var screencapAndroidDevicesCmd = &cobra.Command{ @@ -22,7 +23,7 @@ var screencapAndroidDevicesCmd = &cobra.Command{ return err } - filepath := fmt.Sprintf("screencap_%d.png", time.Now().Unix()) + filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_")) if err = ioutil.WriteFile(filepath, res, 0o644); err != nil { return err } diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 63dbfd0f..a27ac366 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -443,3 +443,7 @@ func Sign(ver string, ak string, sk string, body []byte) string { signResult := sha256HMAC(signKey, body) return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult)) } + +func GenNameWithTimestamp(prefix string) string { + return fmt.Sprintf("%s%d", prefix, time.Now().Unix()) +} diff --git a/hrp/pkg/gadb/device.go b/hrp/pkg/gadb/device.go index 181609b0..dfac9042 100644 --- a/hrp/pkg/gadb/device.go +++ b/hrp/pkg/gadb/device.go @@ -11,6 +11,8 @@ import ( "time" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) type DeviceFileInfo struct { @@ -538,7 +540,7 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) { return string(raw), err } - remote := fmt.Sprintf("/data/local/tmp/gadb_remote_%d.apk", time.Now().Unix()) + remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_")) err := d.Push(apk, remote, time.Now()) if err != nil { return "", fmt.Errorf("error pushing: %v", err) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 093cb776..6b495370 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -217,7 +217,7 @@ type DriverExt struct { doneMjpegStream chan bool scale float64 ocrService OCRService // used to get text from image - ScreenShots []string // save screenshots path + screenShots []string // cache screenshot paths CVArgs } @@ -253,7 +253,9 @@ func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error) return dExt, nil } -func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) { +// TakeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder +// if fileName is empty, it will not save image file and only return raw image data +func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, err error) { // wait for action done time.Sleep(500 * time.Millisecond) @@ -263,14 +265,27 @@ 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).Msg("takeScreenShot failed") + log.Error().Err(err).Msg("capture screenshot data failed") return nil, err } + + // save screenshot to file + if len(fileName) > 0 && fileName[0] != "" { + path := filepath.Join(env.ScreenShotsPath, fileName[0]) + path, err := dExt.saveScreenShot(raw, path) + if err != nil { + log.Error().Err(err).Msg("save screenshot file failed") + return nil, err + } + dExt.screenShots = append(dExt.screenShots, path) + log.Info().Str("path", path).Msg("save screenshot file success") + } + return raw, nil } // saveScreenShot saves image file with file name -func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { +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") @@ -302,19 +317,11 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { 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 failed") - } - - fileName = filepath.Join(env.ScreenShotsPath, fileName) - path, err := saveScreenShot(raw, fileName) - if err != nil { - return "", errors.Wrap(err, "save screenshot failed") - } - return path, nil +func (dExt *DriverExt) GetScreenShots() []string { + defer func() { + dExt.screenShots = nil + }() + return dExt.screenShots } // isPathExists returns true if path exists, whether path is file or dir @@ -689,15 +696,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } } case CtlScreenShot: - // take snapshot - log.Info().Msg("take snapshot for current screen") - screenshotPath, err := dExt.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") - dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath) + // take screenshot + log.Info().Msg("take screenshot for current screen") + _, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("screenshot_")) return err case CtlStartCamera: return dExt.Driver.StartCamera() diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index 501e6862..7d6145b3 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -2,7 +2,6 @@ package uixt import ( "bytes" - "fmt" "math" "strings" "time" @@ -437,7 +436,6 @@ type DataOptions struct { 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 - ScreenShotFilename string // turn on screenshot and specify file name } type DataOption func(data *DataOptions) @@ -514,16 +512,6 @@ func WithDataWaitTime(sec float64) DataOption { } } -func WithScreenShot(fileName ...string) DataOption { - return func(data *DataOptions) { - if len(fileName) > 0 { - data.ScreenShotFilename = fileName[0] - } else { - data.ScreenShotFilename = fmt.Sprintf("screenshot_%d", time.Now().Unix()) - } - } -} - func NewDataOptions(options ...DataOption) *DataOptions { dataOptions := &DataOptions{ Data: make(map[string]interface{}), diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index f68964b6..48bb804a 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -175,14 +175,6 @@ func (s *veDEMOCRService) GetTexts(imageBuf *bytes.Buffer, options ...DataOption dataOptions := NewDataOptions(options...) - if dataOptions.ScreenShotFilename != "" { - path, err := saveScreenShot(imageBuf, dataOptions.ScreenShotFilename) - if err != nil { - return nil, errors.Wrap(err, "save screenshot failed") - } - log.Debug().Str("path", path).Msg("save screenshot") - } - for _, ocrResult := range ocrResults { rect := image.Rectangle{ // ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下 @@ -313,8 +305,7 @@ type OCRService interface { func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err error) { var bufSource *bytes.Buffer - if bufSource, err = dExt.takeScreenShot(); err != nil { - err = fmt.Errorf("takeScreenShot error: %v", err) + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { return } @@ -329,8 +320,7 @@ func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err 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) + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { return } @@ -348,8 +338,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, 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) + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { return } diff --git a/hrp/pkg/uixt/opencv.go b/hrp/pkg/uixt/opencv.go index 6a5b3bb8..95e10783 100644 --- a/hrp/pkg/uixt/opencv.go +++ b/hrp/pkg/uixt/opencv.go @@ -14,6 +14,8 @@ import ( "github.com/pkg/errors" "gocv.io/x/gocv" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) const ( @@ -101,7 +103,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, if bufSearch, err = getBufFromDisk(search); err != nil { return nil, err } - if bufSource, err = dExt.takeScreenShot(); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("cv_")); err != nil { return nil, err } @@ -116,7 +118,7 @@ func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOpt if bufSearch, err = getBufFromDisk(imagePath); err != nil { return 0, 0, 0, 0, err } - if bufSource, err = dExt.takeScreenShot(); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("cv_")); err != nil { return 0, 0, 0, 0, err } diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index f5483488..6cfb90d1 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -2,11 +2,11 @@ package hrp import ( "fmt" - "time" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/code" "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" ) @@ -572,7 +572,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err Success: false, ContentSize: 0, } - screenshots := make([]string, 0) // merge step variables with session variables stepVariables, err := s.ParseStepVariables(step.Variables) @@ -602,18 +601,14 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err } // take screenshot after each step - screenshotPath, err := uiDriver.ScreenShot( - fmt.Sprintf("step_%d", time.Now().Unix())) + _, err := uiDriver.TakeScreenShot( + builtin.GenNameWithTimestamp("step_") + "_" + step.Name) if err != nil { - log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed") - } else { - log.Info().Str("path", screenshotPath).Msg("take screenshot on step finished") - screenshots = append(screenshots, screenshotPath) + log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed on step finished") } // save attachments - screenshots = append(screenshots, uiDriver.ScreenShots...) - attachments["screenshots"] = screenshots + attachments["screenshots"] = uiDriver.GetScreenShots() stepResult.Attachments = attachments }() From a286e0f37f8381a70e85c3a55444eba144b7694e Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 17 Apr 2023 14:38:07 +0800 Subject: [PATCH 5/9] change: update changelog --- docs/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 723ca0d4..01e44971 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,11 +10,12 @@ - feat: add `IsAppInForeground` to check if the given package is in foreground - feat: check if app is in foreground when step failed - feat: add validator AssertAppInForeground and AssertAppNotInForeground +- feat: save screenshots of all steps including ocr and cv recognition process data - fix: adb driver for TapFloat - fix: stop logcat only when enabled - fix: do not fail case when kill logcat error - fix: take screenshot after each step -- fix: screencap compatibility for shell v1 and v2 +- fix: screencap compatibility for shell v1 and v2 protocol ## v4.3.2 (2022-12-26) From 804e69037aa6df66b3b0e11112061a1d62179e9b Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 17 Apr 2023 22:45:58 +0800 Subject: [PATCH 6/9] change: GenNameWithTimestamp --- .../demo_android_feed_random_swipe.json | 134 ++++++++++++++++++ ...=> demo_android_feed_random_swipe_test.go} | 2 +- examples/uitest/demo_feed_random_slide.json | 2 +- ...yin_live.json => demo_ios_live_swipe.json} | 0 ...in_test.go => demo_ios_live_swipe_test.go} | 5 +- hrp/cmd/adb/screencap.go | 2 +- hrp/internal/builtin/utils.go | 7 +- hrp/pkg/gadb/device.go | 2 +- hrp/pkg/uixt/ext.go | 2 +- hrp/pkg/uixt/ocr_vedem.go | 6 +- hrp/pkg/uixt/opencv.go | 4 +- hrp/step_mobile_ui.go | 2 +- 12 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 examples/uitest/demo_android_feed_random_swipe.json rename examples/uitest/{demo_feed_random_slide_test.go => demo_android_feed_random_swipe_test.go} (94%) rename examples/uitest/{demo_douyin_live.json => demo_ios_live_swipe.json} (100%) rename examples/uitest/{demo_douyin_test.go => demo_ios_live_swipe_test.go} (91%) diff --git a/examples/uitest/demo_android_feed_random_swipe.json b/examples/uitest/demo_android_feed_random_swipe.json new file mode 100644 index 00000000..d74f45ae --- /dev/null +++ b/examples/uitest/demo_android_feed_random_swipe.json @@ -0,0 +1,134 @@ +{ + "config": { + "name": "点播_抖音_滑动场景_随机间隔_android", + "variables": { + "device": "${ENV(SerialNumber)}" + }, + "android": [ + { + "serial": "$device" + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "android": { + "actions": [ + { + "method": "app_terminate", + "params": "com.ss.android.ugc.aweme" + }, + { + "method": "app_launch", + "params": "com.ss.android.ugc.aweme" + }, + { + "method": "sleep", + "params": 10 + } + ] + }, + "validate": [ + { + "check": "ui_foreground_app", + "assert": "equal", + "expect": "com.ss.android.ugc.aweme", + "msg": "app [com.ss.android.ugc.aweme] should be in foreground" + } + ] + }, + { + "name": "处理青少年弹窗", + "android": { + "actions": [ + { + "method": "tap_ocr", + "params": "我知道了", + "ignore_NotFoundError": true + } + ] + } + }, + { + "name": "滑动 Feed 3 次,随机间隔 0-5s", + "android": { + "actions": [ + { + "method": "swipe", + "params": "up" + }, + { + "method": "sleep_random", + "params": [ + 0, + 5 + ] + } + ] + }, + "loops": 3 + }, + { + "name": "滑动 Feed 1 次,随机间隔 5-10s", + "android": { + "actions": [ + { + "method": "swipe", + "params": "up" + }, + { + "method": "sleep_random", + "params": [ + 5, + 10 + ] + } + ] + }, + "loops": 1 + }, + { + "name": "滑动 Feed 10 次,70% 随机间隔 0-5s,30% 随机间隔 5-10s", + "android": { + "actions": [ + { + "method": "swipe", + "params": "up" + }, + { + "method": "sleep_random", + "params": [ + 0, + 5, + 0.7, + 5, + 10, + 0.3 + ] + } + ] + }, + "loops": 10 + }, + { + "name": "exit", + "android": { + "actions": [ + { + "method": "app_terminate", + "params": "com.ss.android.ugc.aweme" + } + ] + }, + "validate": [ + { + "check": "ui_foreground_app", + "assert": "not_equal", + "expect": "com.ss.android.ugc.aweme", + "msg": "app [com.ss.android.ugc.aweme] should not be in foreground" + } + ] + } + ] +} diff --git a/examples/uitest/demo_feed_random_slide_test.go b/examples/uitest/demo_android_feed_random_swipe_test.go similarity index 94% rename from examples/uitest/demo_feed_random_slide_test.go rename to examples/uitest/demo_android_feed_random_swipe_test.go index 98311c69..a8d7392e 100644 --- a/examples/uitest/demo_feed_random_slide_test.go +++ b/examples/uitest/demo_android_feed_random_swipe_test.go @@ -50,7 +50,7 @@ func TestAndroidDouyinFeedTest(t *testing.T) { }, } - if err := testCase.Dump2JSON("demo_feed_random_slide.json"); err != nil { + if err := testCase.Dump2JSON("demo_android_feed_random_swipe.json"); err != nil { t.Fatal(err) } diff --git a/examples/uitest/demo_feed_random_slide.json b/examples/uitest/demo_feed_random_slide.json index d74f45ae..f256eaf4 100644 --- a/examples/uitest/demo_feed_random_slide.json +++ b/examples/uitest/demo_feed_random_slide.json @@ -109,7 +109,7 @@ } ] }, - "loops": 10 + "loops": 2 }, { "name": "exit", diff --git a/examples/uitest/demo_douyin_live.json b/examples/uitest/demo_ios_live_swipe.json similarity index 100% rename from examples/uitest/demo_douyin_live.json rename to examples/uitest/demo_ios_live_swipe.json diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_ios_live_swipe_test.go similarity index 91% rename from examples/uitest/demo_douyin_test.go rename to examples/uitest/demo_ios_live_swipe_test.go index def2e9d3..575edc58 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_ios_live_swipe_test.go @@ -45,10 +45,7 @@ func TestIOSDouyinLive(t *testing.T) { }, } - if err := testCase.Dump2JSON("demo_douyin_live.json"); err != nil { - t.Fatal(err) - } - if err := testCase.Dump2YAML("demo_douyin_live.yaml"); err != nil { + if err := testCase.Dump2JSON("demo_ios_live_swipe.json"); err != nil { t.Fatal(err) } diff --git a/hrp/cmd/adb/screencap.go b/hrp/cmd/adb/screencap.go index bb1ddf6c..8147e0c4 100644 --- a/hrp/cmd/adb/screencap.go +++ b/hrp/cmd/adb/screencap.go @@ -23,7 +23,7 @@ var screencapAndroidDevicesCmd = &cobra.Command{ return err } - filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_")) + filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_%d")) if err = ioutil.WriteFile(filepath, res, 0o644); err != nil { return err } diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index a27ac366..b88f7902 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -444,6 +444,9 @@ func Sign(ver string, ak string, sk string, body []byte) string { return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult)) } -func GenNameWithTimestamp(prefix string) string { - return fmt.Sprintf("%s%d", prefix, time.Now().Unix()) +func GenNameWithTimestamp(tmpl string) string { + if !strings.Contains(tmpl, "%d") { + tmpl = tmpl + "_%d" + } + return fmt.Sprintf(tmpl, time.Now().Unix()) } diff --git a/hrp/pkg/gadb/device.go b/hrp/pkg/gadb/device.go index dfac9042..2067755a 100644 --- a/hrp/pkg/gadb/device.go +++ b/hrp/pkg/gadb/device.go @@ -540,7 +540,7 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) { return string(raw), err } - remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_")) + remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_%d")) err := d.Push(apk, remote, time.Now()) if err != nil { return "", fmt.Errorf("error pushing: %v", err) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 6b495370..0d296577 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -698,7 +698,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { case CtlScreenShot: // take screenshot log.Info().Msg("take screenshot for current screen") - _, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("screenshot_")) + _, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("screenshot_%d")) return err case CtlStartCamera: return dExt.Driver.StartCamera() diff --git a/hrp/pkg/uixt/ocr_vedem.go b/hrp/pkg/uixt/ocr_vedem.go index 48bb804a..15db742d 100644 --- a/hrp/pkg/uixt/ocr_vedem.go +++ b/hrp/pkg/uixt/ocr_vedem.go @@ -305,7 +305,7 @@ type OCRService interface { func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err error) { var bufSource *bytes.Buffer - if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil { return } @@ -320,7 +320,7 @@ func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, y, width, height float64, err error) { var bufSource *bytes.Buffer - if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil { return } @@ -338,7 +338,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption) (points [][]float64, err error) { var bufSource *bytes.Buffer - if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("ocr_")); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil { return } diff --git a/hrp/pkg/uixt/opencv.go b/hrp/pkg/uixt/opencv.go index 95e10783..98f7286b 100644 --- a/hrp/pkg/uixt/opencv.go +++ b/hrp/pkg/uixt/opencv.go @@ -103,7 +103,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, if bufSearch, err = getBufFromDisk(search); err != nil { return nil, err } - if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("cv_")); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil { return nil, err } @@ -118,7 +118,7 @@ func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOpt if bufSearch, err = getBufFromDisk(imagePath); err != nil { return 0, 0, 0, 0, err } - if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("cv_")); err != nil { + if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil { return 0, 0, 0, 0, err } diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 6cfb90d1..ff8b3a2c 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -602,7 +602,7 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err // take screenshot after each step _, err := uiDriver.TakeScreenShot( - builtin.GenNameWithTimestamp("step_") + "_" + step.Name) + builtin.GenNameWithTimestamp("step_%d_") + step.Name) if err != nil { log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed on step finished") } From 865c8c6786292016b7b477b1a35e63b93ca4d7de Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Tue, 18 Apr 2023 19:33:15 +0800 Subject: [PATCH 7/9] change: update examples --- examples/uitest/demo_android_douyin_test.go | 44 ----------- ...wipe.json => demo_android_feed_swipe.json} | 0 ...est.go => demo_android_feed_swipe_test.go} | 2 +- ...lide.json => demo_android_live_swipe.json} | 78 ++++++++++--------- .../uitest/demo_android_live_swipe_test.go | 59 ++++++++++++++ 5 files changed, 102 insertions(+), 81 deletions(-) delete mode 100644 examples/uitest/demo_android_douyin_test.go rename examples/uitest/{demo_android_feed_random_swipe.json => demo_android_feed_swipe.json} (100%) rename examples/uitest/{demo_android_feed_random_swipe_test.go => demo_android_feed_swipe_test.go} (94%) rename examples/uitest/{demo_feed_random_slide.json => demo_android_live_swipe.json} (73%) create mode 100644 examples/uitest/demo_android_live_swipe_test.go diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go deleted file mode 100644 index 7d7c94f4..00000000 --- a/examples/uitest/demo_android_douyin_test.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build localtest - -package uitest - -import ( - "testing" - - "github.com/httprunner/httprunner/v4/hrp" - "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" -) - -func TestAndroidDouYinLive(t *testing.T) { - testCase := &hrp.TestCase{ - Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). - SetAndroid(uixt.WithUIA2(false), uixt.WithAdbLogOn(true)), - TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音"). - Android(). - Home(). - AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 - SwipeToTapApp("抖音", uixt.WithMaxRetryTimes(5)). - Sleep(10), - hrp.NewStep("处理青少年弹窗"). - Android(). - Tap("推荐"). - TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)). - Validate(). - AssertOCRExists("首页", "抖音启动失败,「首页」不存在"), - hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」"). - Android(). - SwipeToTapText("直播", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")), - hrp.NewStep("向上滑动,等待 10s"). - Android(). - SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 - SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 - }, - } - - runner := hrp.NewRunner(t).SetSaveTests(true) - err := runner.Run(testCase) - if err != nil { - t.Fatal(err) - } -} diff --git a/examples/uitest/demo_android_feed_random_swipe.json b/examples/uitest/demo_android_feed_swipe.json similarity index 100% rename from examples/uitest/demo_android_feed_random_swipe.json rename to examples/uitest/demo_android_feed_swipe.json diff --git a/examples/uitest/demo_android_feed_random_swipe_test.go b/examples/uitest/demo_android_feed_swipe_test.go similarity index 94% rename from examples/uitest/demo_android_feed_random_swipe_test.go rename to examples/uitest/demo_android_feed_swipe_test.go index a8d7392e..c2a024c3 100644 --- a/examples/uitest/demo_android_feed_random_swipe_test.go +++ b/examples/uitest/demo_android_feed_swipe_test.go @@ -50,7 +50,7 @@ func TestAndroidDouyinFeedTest(t *testing.T) { }, } - if err := testCase.Dump2JSON("demo_android_feed_random_swipe.json"); err != nil { + if err := testCase.Dump2JSON("demo_android_feed_swipe.json"); err != nil { t.Fatal(err) } diff --git a/examples/uitest/demo_feed_random_slide.json b/examples/uitest/demo_android_live_swipe.json similarity index 73% rename from examples/uitest/demo_feed_random_slide.json rename to examples/uitest/demo_android_live_swipe.json index f256eaf4..3912f1ca 100644 --- a/examples/uitest/demo_feed_random_slide.json +++ b/examples/uitest/demo_android_live_swipe.json @@ -25,7 +25,7 @@ }, { "method": "sleep", - "params": 10 + "params": 5 } ] }, @@ -51,45 +51,20 @@ } }, { - "name": "滑动 Feed 3 次,随机间隔 0-5s", + "name": "在推荐页上划,直到出现「点击进入直播间」", "android": { "actions": [ { - "method": "swipe", - "params": "up" - }, - { - "method": "sleep_random", - "params": [ - 0, - 5 - ] + "method": "swipe_to_tap_text", + "params": "点击进入直播间", + "identifier": "进入直播间", + "max_retry_times": 10 } ] - }, - "loops": 3 + } }, { - "name": "滑动 Feed 1 次,随机间隔 5-10s", - "android": { - "actions": [ - { - "method": "swipe", - "params": "up" - }, - { - "method": "sleep_random", - "params": [ - 5, - 10 - ] - } - ] - }, - "loops": 1 - }, - { - "name": "滑动 Feed 10 次,70% 随机间隔 0-5s,30% 随机间隔 5-10s", + "name": "滑动 Feed 5 次,60% 随机间隔 0-5s,40% 随机间隔 5-10s", "android": { "actions": [ { @@ -101,15 +76,46 @@ "params": [ 0, 5, - 0.7, + 0.6, 5, 10, - 0.3 + 0.4 ] } ] }, - "loops": 2 + "loops": 5 + }, + { + "name": "向上滑动,等待 10s", + "android": { + "actions": [ + { + "method": "swipe", + "params": "up", + "identifier": "第一次上划" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + }, + { + "method": "swipe", + "params": "up", + "identifier": "第二次上划" + }, + { + "method": "sleep", + "params": 10 + }, + { + "method": "screenshot" + } + ] + } }, { "name": "exit", diff --git a/examples/uitest/demo_android_live_swipe_test.go b/examples/uitest/demo_android_live_swipe_test.go new file mode 100644 index 00000000..500aff1d --- /dev/null +++ b/examples/uitest/demo_android_live_swipe_test.go @@ -0,0 +1,59 @@ +//go:build localtest + +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func TestAndroidLiveSwipeTest(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("点播_抖音_滑动场景_随机间隔_android"). + WithVariables(map[string]interface{}{ + "device": "${ENV(SerialNumber)}", + }). + SetAndroid(uixt.WithSerialNumber("$device")), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + Android(). + AppTerminate("com.ss.android.ugc.aweme"). + AppLaunch("com.ss.android.ugc.aweme"). + Sleep(5). + Validate(). + AssertAppInForeground("com.ss.android.ugc.aweme"), + hrp.NewStep("处理青少年弹窗"). + Android(). + TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)), + hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」"). + Android(). + SwipeToTapText("点击进入直播间", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")), + hrp.NewStep("滑动 Feed 5 次,60% 随机间隔 0-5s,40% 随机间隔 5-10s"). + Loop(5). + Android(). + SwipeUp(). + SleepRandom(0, 5, 0.6, 5, 10, 0.4), + hrp.NewStep("向上滑动,等待 10s"). + Android(). + SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + hrp.NewStep("exit"). + Android(). + AppTerminate("com.ss.android.ugc.aweme"). + Validate(). + AssertAppNotInForeground("com.ss.android.ugc.aweme"), + }, + } + + if err := testCase.Dump2JSON("demo_android_live_swipe.json"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} From b6cf074bb5047493248f3ab01d4c0a78bf62377d Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Tue, 18 Apr 2023 19:59:52 +0800 Subject: [PATCH 8/9] fix: copy screenshot buffer for OCR recognizing --- docs/CHANGELOG.md | 2 +- hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/ext.go | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 01e44971..905caa26 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## v4.3.3 (2023-04-16) +## v4.3.3 (2023-04-18) **go version** diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index e73aa79d..2167abfb 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.3.2304161855 \ No newline at end of file +v4.3.3.2304181958 \ No newline at end of file diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 0d296577..bff5f541 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -286,7 +286,13 @@ func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, er // saveScreenShot saves image file with file name func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { - img, format, err := image.Decode(raw) + // notice: screenshot data is a stream, so we need to copy it to a new buffer + copiedBuffer := &bytes.Buffer{} + if _, err := copiedBuffer.Write(raw.Bytes()); err != nil { + log.Error().Err(err).Msg("copy screenshot buffer failed") + } + + img, format, err := image.Decode(copiedBuffer) if err != nil { return "", errors.Wrap(err, "decode screenshot image failed") } From d636f0cd3c7b98268714acf79295fbce2ff9bec4 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Tue, 18 Apr 2023 20:14:40 +0800 Subject: [PATCH 9/9] fix: change httpbin.org from http to https --- examples/httpbin/validate.yml | 2 +- examples/httpbin/validate_test.py | 2 +- hrp/boomer_test.go | 2 +- hrp/pkg/convert/from_curl_test.go | 2 +- hrp/runner_test.go | 4 ++-- hrp/step_request_test.go | 8 ++++---- httprunner/client_test.py | 6 +++--- httprunner/ext/uploader/__init__.py | 10 ++++++---- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/httpbin/validate.yml b/examples/httpbin/validate.yml index d5769a7b..f18f14e9 100644 --- a/examples/httpbin/validate.yml +++ b/examples/httpbin/validate.yml @@ -1,6 +1,6 @@ config: name: basic test with httpbin - base_url: http://httpbin.org/ + base_url: https://httpbin.org/ teststeps: - diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index 18f38d2e..44d5a4d2 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -5,7 +5,7 @@ from httprunner import HttpRunner, Config, Step, RunRequest class TestCaseValidate(HttpRunner): - config = Config("basic test with httpbin").base_url("http://httpbin.org/") + config = Config("basic test with httpbin").base_url("https://httpbin.org/") teststeps = [ Step( diff --git a/hrp/boomer_test.go b/hrp/boomer_test.go index 83151b5e..9eadc91a 100644 --- a/hrp/boomer_test.go +++ b/hrp/boomer_test.go @@ -10,7 +10,7 @@ func TestBoomerStandaloneRun(t *testing.T) { defer removeHashicorpGoPlugin() testcase1 := &TestCase{ - Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"), + Config: NewConfig("TestCase1").SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("headers"). GET("/headers"). diff --git a/hrp/pkg/convert/from_curl_test.go b/hrp/pkg/convert/from_curl_test.go index a9b5f4f2..63c036a5 100644 --- a/hrp/pkg/convert/from_curl_test.go +++ b/hrp/pkg/convert/from_curl_test.go @@ -24,7 +24,7 @@ func TestLoadCurlCase(t *testing.T) { if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) { t.Fatal() } - if !assert.Equal(t, "http://httpbin.org", tCase.TestSteps[0].Request.URL) { + if !assert.Equal(t, "https://httpbin.org", tCase.TestSteps[0].Request.URL) { t.Fatal() } diff --git a/hrp/runner_test.go b/hrp/runner_test.go index c07cf1f1..9a0280bb 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -63,7 +63,7 @@ func assertRunTestCases(t *testing.T) { refCase := TestCasePath(demoTestCaseWithPluginJSONPath) testcase1 := &TestCase{ Config: NewConfig("TestCase1"). - SetBaseURL("http://httpbin.org"), + SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("testcase1-step1"). GET("/headers"). @@ -77,7 +77,7 @@ func assertRunTestCases(t *testing.T) { AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"), NewStep("testcase1-step3").CallRefCase( &TestCase{ - Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("http://httpbin.org"), + Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("ip"). GET("/ip"). diff --git a/hrp/step_request_test.go b/hrp/step_request_test.go index 3f7e63c5..04970ea7 100644 --- a/hrp/step_request_test.go +++ b/hrp/step_request_test.go @@ -153,7 +153,7 @@ func TestRunRequestStatOn(t *testing.T) { if !assert.Greater(t, stat["Total"], int64(1)) { t.Fatal() } - if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(3)) { + if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(100)) { t.Fatal() } } @@ -165,7 +165,7 @@ func TestRunCaseWithTimeout(t *testing.T) { testcase1 := &TestCase{ Config: NewConfig("TestCase1"). SetTimeout(2 * time.Second). // set global timeout to 2s - SetBaseURL("http://httpbin.org"), + SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("step1"). GET("/delay/1"). @@ -181,7 +181,7 @@ func TestRunCaseWithTimeout(t *testing.T) { testcase2 := &TestCase{ Config: NewConfig("TestCase2"). SetTimeout(2 * time.Second). // set global timeout to 2s - SetBaseURL("http://httpbin.org"), + SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("step1"). GET("/delay/3"). @@ -198,7 +198,7 @@ func TestRunCaseWithTimeout(t *testing.T) { testcase3 := &TestCase{ Config: NewConfig("TestCase3"). SetTimeout(2 * time.Second). - SetBaseURL("http://httpbin.org"), + SetBaseURL("https://httpbin.org"), TestSteps: []IStep{ NewStep("step2"). GET("/delay/3"). diff --git a/httprunner/client_test.py b/httprunner/client_test.py index 467d4246..3e3175e0 100644 --- a/httprunner/client_test.py +++ b/httprunner/client_test.py @@ -8,7 +8,7 @@ class TestHttpSession(unittest.TestCase): self.session = HttpSession() def test_request_http(self): - self.session.request("get", "http://httpbin.org/get") + self.session.request("get", "https://httpbin.org/get") address = self.session.data.address self.assertGreater(len(address.server_ip), 0) self.assertEqual(address.server_port, 80) @@ -26,7 +26,7 @@ class TestHttpSession(unittest.TestCase): def test_request_http_allow_redirects(self): self.session.request( "get", - "http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", + "https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", allow_redirects=True, ) address = self.session.data.address @@ -50,7 +50,7 @@ class TestHttpSession(unittest.TestCase): def test_request_http_not_allow_redirects(self): self.session.request( "get", - "http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", + "https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com", allow_redirects=False, ) address = self.session.data.address diff --git a/httprunner/ext/uploader/__init__.py b/httprunner/ext/uploader/__init__.py index 6bbd86cf..a89fce79 100644 --- a/httprunner/ext/uploader/__init__.py +++ b/httprunner/ext/uploader/__init__.py @@ -10,7 +10,7 @@ Then you can write upload test script as below: - test: name: upload file request: - url: http://httpbin.org/upload + url: https://httpbin.org/upload method: POST headers: Cookie: session=AAA-BBB-CCC @@ -31,7 +31,7 @@ For compatibility, you can also write upload test script in old way: field2: "value2" m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)} request: - url: http://httpbin.org/upload + url: https://httpbin.org/upload method: POST headers: Content-Type: ${multipart_content_type($m_encoder)} @@ -75,7 +75,9 @@ def ensure_upload_ready(): sys.exit(1) -def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping): +def prepare_upload_step( + step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping +): """preprocess for upload test replace `upload` info with MultipartEncoder @@ -84,7 +86,7 @@ def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions { "variables": {}, "request": { - "url": "http://httpbin.org/upload", + "url": "https://httpbin.org/upload", "method": "POST", "headers": { "Cookie": "session=AAA-BBB-CCC"