From 81d1fd7da0f3961abcbe5d3b509815c512389683 Mon Sep 17 00:00:00 2001 From: buyuxiang Date: Wed, 18 Oct 2023 21:31:32 +0800 Subject: [PATCH 1/3] feat: support live end to end delay collection --- examples/uitest/android_e2e_delay_test.go | 59 +++++++ examples/uitest/android_e2e_delay_test.json | 146 +++++++++++++++++ hrp/internal/version/VERSION | 2 +- hrp/pkg/uixt/action.go | 4 + hrp/pkg/uixt/ext.go | 3 + hrp/pkg/uixt/live_e2e.go | 173 ++++++++++++++++++++ hrp/step_mobile_ui.go | 9 + 7 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 examples/uitest/android_e2e_delay_test.go create mode 100644 examples/uitest/android_e2e_delay_test.json create mode 100644 hrp/pkg/uixt/live_e2e.go diff --git a/examples/uitest/android_e2e_delay_test.go b/examples/uitest/android_e2e_delay_test.go new file mode 100644 index 00000000..be5f9b35 --- /dev/null +++ b/examples/uitest/android_e2e_delay_test.go @@ -0,0 +1,59 @@ +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" + "github.com/httprunner/httprunner/v4/hrp/pkg/uixt" +) + +func TestAndroidDouyinE2E(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("直播_抖音_端到端时延_android"). + WithVariables(map[string]interface{}{ + "device": "${ENV(SerialNumber)}", + "ups": "${ENV(LIVEUPLIST)}", + }). + SetAndroid(uixt.WithSerialNumber("$device"), uixt.WithAdbLogOn(true)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + Android(). + AppTerminate("com.ss.android.ugc.aweme"). + AppLaunch("com.ss.android.ugc.aweme"). + Home(). + SwipeToTapApp( + "抖音", + uixt.WithMaxRetryTimes(5), + uixt.WithTapOffset(0, -50), + ). + Sleep(20). + Validate(). + AssertOCRExists("推荐", "进入抖音失败"), + hrp.NewStep("点击放大镜"). + Android(). + TapXY(0.9, 0.08). + Sleep(5), + hrp.NewStep("输入账号名称"). + Android(). + Input("$ups"). + Sleep(5), + hrp.NewStep("点击搜索"). + Android(). + TapByOCR("搜索"). + Sleep(5), + hrp.NewStep("端到端采集").Loop(5). + Android(). + TapByOCR( + "直播中", + uixt.WithIgnoreNotFoundError(true), + uixt.WithIndex(-1), + ). + EndToEndDelay(uixt.WithInterval(5), uixt.WithTimeout(120)). + TapByUITypes(uixt.WithScreenShotUITypes("close")), + }, + } + + if err := testCase.Dump2JSON("android_e2e_delay_test.json"); err != nil { + t.Fatal(err) + } +} diff --git a/examples/uitest/android_e2e_delay_test.json b/examples/uitest/android_e2e_delay_test.json new file mode 100644 index 00000000..d3707119 --- /dev/null +++ b/examples/uitest/android_e2e_delay_test.json @@ -0,0 +1,146 @@ +{ + "config": { + "name": "直播_抖音_端到端时延_android", + "variables": { + "device": "${ENV(SerialNumber)}", + "ups": "${ENV(LIVEUPLIST)}" + }, + "android": [ + { + "serial": "$device", + "log_on": true + } + ] + }, + "teststeps": [ + { + "name": "启动抖音", + "android": { + "actions": [ + { + "method": "app_terminate", + "params": "com.ss.android.ugc.aweme" + }, + { + "method": "app_launch", + "params": "com.ss.android.ugc.aweme" + }, + { + "method": "home" + }, + { + "method": "swipe_to_tap_app", + "params": "抖音", + "options": { + "max_retry_times": 5, + "offset": [ + 0, + -50 + ] + } + }, + { + "method": "sleep", + "params": 20 + } + ] + }, + "validate": [ + { + "check": "ui_ocr", + "assert": "exists", + "expect": "推荐", + "msg": "进入抖音失败" + } + ] + }, + { + "name": "点击放大镜", + "android": { + "actions": [ + { + "method": "tap_xy", + "params": [ + 0.9, + 0.08 + ], + "options": { + + } + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "输入账号名称", + "android": { + "actions": [ + { + "method": "input", + "params": "$ups", + "options": { + + } + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "点击搜索", + "android": { + "actions": [ + { + "method": "tap_ocr", + "params": "搜索", + "options": { + + } + }, + { + "method": "sleep", + "params": 5 + } + ] + } + }, + { + "name": "端到端采集", + "android": { + "actions": [ + { + "method": "tap_ocr", + "params": "直播中", + "options": { + "ignore_NotFoundError": true, + "index": -1 + } + }, + { + "method": "live_e2e", + "options": { + "interval": 5, + "timeout": 120 + } + }, + { + "method": "tap_cv", + "options": { + "screenshot_with_ui_types": [ + "close" + ] + } + } + ] + }, + "loops": 5 + } + ] +} diff --git a/hrp/internal/version/VERSION b/hrp/internal/version/VERSION index e764e622..96a9a9b4 100644 --- a/hrp/internal/version/VERSION +++ b/hrp/internal/version/VERSION @@ -1 +1 @@ -v4.3.6.2310111458 \ No newline at end of file +v4.3.6.2310201510 diff --git a/hrp/pkg/uixt/action.go b/hrp/pkg/uixt/action.go index cf28c4b8..59e903b8 100644 --- a/hrp/pkg/uixt/action.go +++ b/hrp/pkg/uixt/action.go @@ -59,6 +59,7 @@ const ( ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap ACTION_VideoCrawler ActionMethod = "video_crawler" ACTION_ClosePopups ActionMethod = "close_popups" + ACTION_EndToEndDelay ActionMethod = "live_e2e" ) type MobileAction struct { @@ -685,6 +686,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) { return dExt.VideoCrawler(configs) case ACTION_ClosePopups: return dExt.ClosePopups(action.GetOptions()...) + case ACTION_EndToEndDelay: + dExt.CollectEndToEndDelay(action.GetOptions()...) + return nil } return nil } diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 54009453..c242f341 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -128,12 +128,14 @@ type cacheStepData struct { screenResults ScreenResultMap // cache feed/live video stat videoCrawler *VideoCrawler + e2eDelay []timeLog } func (d *cacheStepData) reset() { d.screenShots = make([]string, 0) d.screenResults = make(map[string]*ScreenResult) d.videoCrawler = nil + d.e2eDelay = nil } type DriverExt struct { @@ -275,6 +277,7 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} { cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls() dExt.cacheStepData.screenResults.updatePopupCloseStatus() cacheData["screen_results"] = dExt.cacheStepData.screenResults + cacheData["e2e_results"] = dExt.cacheStepData.e2eDelay // clear cache dExt.cacheStepData.reset() diff --git a/hrp/pkg/uixt/live_e2e.go b/hrp/pkg/uixt/live_e2e.go new file mode 100644 index 00000000..51c238cb --- /dev/null +++ b/hrp/pkg/uixt/live_e2e.go @@ -0,0 +1,173 @@ +package uixt + +import ( + "fmt" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/httprunner/funplugin/myexec" + "github.com/rs/zerolog/log" +) + +type timeLog struct { + UTCTimeStr string `json:"utc_time_str"` + UTCTime int64 `json:"utc_time"` + LiveTimeStr string `json:"live_time_str"` + LiveTime int64 `json:"live_time"` + Delay float64 `json:"delay"` +} + +type EndToEndDelay struct { + driver *DriverExt + file *os.File + resultDir string + UUID string `json:"uuid"` + AppName string `json:"appName"` + BundleID string `json:"bundleID"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Interval int `json:"interval"` // seconds + Duration int `json:"duration"` // seconds + Timelines []timeLog `json:"timelines"` + PerfFile string `json:"perf"` +} + +func (dExt *DriverExt) CollectEndToEndDelay(options ...ActionOption) { + dataOptions := NewActionOptions(options...) + var err error + startTime := time.Now() + resultDir := filepath.Join("endtoenddelay", startTime.Format("2006-01-02 15:04:05")) + + if err = os.MkdirAll(filepath.Join(resultDir, "screenshot"), 0o755); err != nil { + log.Fatal().Err(err).Msg("failed to create result dir") + } + + filename := filepath.Join(resultDir, "log.txt") + f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) + if err != nil { + log.Fatal().Err(err).Msg("failed to open file") + } + // write title + f.WriteString("utc_time\tutc_timestamp\tlive_time\tlive_seconds\n") + + if dataOptions.Interval == 0 { + dataOptions.Interval = 5 + } + if dataOptions.Timeout == 0 { + dataOptions.Timeout = 60 + } + + endToEndDelay := &EndToEndDelay{ + driver: dExt, + file: f, + resultDir: resultDir, + Duration: int(dataOptions.Timeout), + Interval: int(dataOptions.Interval), + StartTime: startTime.Format("2006-01-02 15:04:05"), + } + + SntpCheckTime() + + endToEndDelay.Start() + + dExt.cacheStepData.e2eDelay = endToEndDelay.Timelines +} + +func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { + utcTimeStr := utcTime.Format("2006-01-02 15:04:05") + ocrTexts, err := ete.driver.GetScreenTexts() + if err != nil { + log.Error().Err(err).Msg("get ocr texts failed") + return err + } + + // filter ocr texts with time format + var liveTimeTexts []string + for _, ocrText := range ocrTexts { + if strings.HasPrefix(ocrText.Text, "16") && + len(ocrText.Text) > 8 && + !strings.Contains(ocrText.Text, ":") { + liveTimeTexts = append(liveTimeTexts, ocrText.Text) + } + } + + var liveTimeText string + if len(liveTimeTexts) != 0 { + liveTimeText = liveTimeTexts[0] + } else { + log.Warn().Msg("no time text found") + return nil + } + + if len(liveTimeText) < 13 { + for (13 - len(liveTimeText)) > 0 { + liveTimeText += "0" + } + } + liveTimeInt, err := strconv.Atoi(liveTimeText) + if err != nil { + liveTimeInt = 0 + } + liveTimeSInt, err := strconv.Atoi(liveTimeText[:10]) + if err != nil { + liveTimeSInt = 0 + } + liveTimeNSInt, err := strconv.Atoi(liveTimeText[10:13]) + if err != nil { + liveTimeNSInt = 0 + } + liveTimeStr := time.Unix(int64(liveTimeSInt), int64(liveTimeNSInt*1000*1000)).Format("2006-01-02 15:04:05") + line := fmt.Sprintf("%s\t%d\t%s\t%s\n", + utcTimeStr, utcTime.UnixMicro(), liveTimeStr, liveTimeText) + log.Info(). + Str("utcTime", utcTimeStr). + Int64("utcTimeInt", utcTime.UnixMilli()). + Str("liveTime", liveTimeStr). + Int64("liveTimeInt", int64(liveTimeInt)). + Float64("delay", float64(utcTime.UnixMilli()-int64(liveTimeInt))/1000). + Msg("log live time") + if _, err := ete.file.WriteString(line); err != nil { + log.Error().Err(err).Str("line", line).Msg("write timeseries failed") + } + ete.Timelines = append(ete.Timelines, timeLog{ + UTCTimeStr: utcTimeStr, + UTCTime: utcTime.UnixMilli(), + LiveTimeStr: liveTimeStr, + LiveTime: int64(liveTimeInt), + Delay: float64(utcTime.UnixMilli()-int64(liveTimeInt)) / 1000, + }) + return nil +} + +func (ete *EndToEndDelay) Start() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + timer := time.NewTimer(time.Duration(ete.Duration) * time.Second) + for { + select { + case <-timer.C: + return + case <-c: + return + default: + utcTime := time.Now() + if utcTime.Unix()%int64(ete.Interval) == 0 { + _ = ete.getCurrentLiveTime(utcTime) + } else { + time.Sleep(500 * time.Millisecond) + } + } + } +} + +func SntpCheckTime() { + err := myexec.RunCommand("sudo", "sntp", "-sS", "time.asia.apple.com") + if err != nil { + log.Error().Err(err).Msg("failed to synchronize time using sntp") + } +} diff --git a/hrp/step_mobile_ui.go b/hrp/step_mobile_ui.go index 09a3f194..6cb28be3 100644 --- a/hrp/step_mobile_ui.go +++ b/hrp/step_mobile_ui.go @@ -300,6 +300,15 @@ func (s *StepMobile) VideoCrawler(params map[string]interface{}) *StepMobile { return &StepMobile{step: s.step} } +func (s *StepMobile) EndToEndDelay(options ...uixt.ActionOption) *StepMobile { + s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ + Method: uixt.ACTION_EndToEndDelay, + Params: nil, + Options: uixt.NewActionOptions(options...), + }) + return &StepMobile{step: s.step} +} + func (s *StepMobile) ScreenShot(options ...uixt.ActionOption) *StepMobile { s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{ Method: uixt.ACTION_ScreenShot, From b83c2a38534fd6f872db3f474b75032641acf2ff Mon Sep 17 00:00:00 2001 From: buyuxiang Date: Tue, 14 Nov 2023 20:10:42 +0800 Subject: [PATCH 2/3] remove unused endtoenddelay directory --- hrp/pkg/uixt/live_e2e.go | 41 ++-------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/hrp/pkg/uixt/live_e2e.go b/hrp/pkg/uixt/live_e2e.go index 51c238cb..6ce4bdb1 100644 --- a/hrp/pkg/uixt/live_e2e.go +++ b/hrp/pkg/uixt/live_e2e.go @@ -1,16 +1,13 @@ package uixt import ( - "fmt" "os" "os/signal" - "path/filepath" "strconv" "strings" "syscall" "time" - "github.com/httprunner/funplugin/myexec" "github.com/rs/zerolog/log" ) @@ -24,36 +21,16 @@ type timeLog struct { type EndToEndDelay struct { driver *DriverExt - file *os.File - resultDir string - UUID string `json:"uuid"` - AppName string `json:"appName"` - BundleID string `json:"bundleID"` StartTime string `json:"startTime"` EndTime string `json:"endTime"` Interval int `json:"interval"` // seconds Duration int `json:"duration"` // seconds Timelines []timeLog `json:"timelines"` - PerfFile string `json:"perf"` } func (dExt *DriverExt) CollectEndToEndDelay(options ...ActionOption) { dataOptions := NewActionOptions(options...) - var err error startTime := time.Now() - resultDir := filepath.Join("endtoenddelay", startTime.Format("2006-01-02 15:04:05")) - - if err = os.MkdirAll(filepath.Join(resultDir, "screenshot"), 0o755); err != nil { - log.Fatal().Err(err).Msg("failed to create result dir") - } - - filename := filepath.Join(resultDir, "log.txt") - f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) - if err != nil { - log.Fatal().Err(err).Msg("failed to open file") - } - // write title - f.WriteString("utc_time\tutc_timestamp\tlive_time\tlive_seconds\n") if dataOptions.Interval == 0 { dataOptions.Interval = 5 @@ -64,15 +41,11 @@ func (dExt *DriverExt) CollectEndToEndDelay(options ...ActionOption) { endToEndDelay := &EndToEndDelay{ driver: dExt, - file: f, - resultDir: resultDir, Duration: int(dataOptions.Timeout), Interval: int(dataOptions.Interval), StartTime: startTime.Format("2006-01-02 15:04:05"), } - SntpCheckTime() - endToEndDelay.Start() dExt.cacheStepData.e2eDelay = endToEndDelay.Timelines @@ -122,8 +95,6 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { liveTimeNSInt = 0 } liveTimeStr := time.Unix(int64(liveTimeSInt), int64(liveTimeNSInt*1000*1000)).Format("2006-01-02 15:04:05") - line := fmt.Sprintf("%s\t%d\t%s\t%s\n", - utcTimeStr, utcTime.UnixMicro(), liveTimeStr, liveTimeText) log.Info(). Str("utcTime", utcTimeStr). Int64("utcTimeInt", utcTime.UnixMilli()). @@ -131,9 +102,6 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { Int64("liveTimeInt", int64(liveTimeInt)). Float64("delay", float64(utcTime.UnixMilli()-int64(liveTimeInt))/1000). Msg("log live time") - if _, err := ete.file.WriteString(line); err != nil { - log.Error().Err(err).Str("line", line).Msg("write timeseries failed") - } ete.Timelines = append(ete.Timelines, timeLog{ UTCTimeStr: utcTimeStr, UTCTime: utcTime.UnixMilli(), @@ -151,8 +119,10 @@ func (ete *EndToEndDelay) Start() { for { select { case <-timer.C: + ete.EndTime = time.Now().Format("2006-01-02 15:04:05") return case <-c: + ete.EndTime = time.Now().Format("2006-01-02 15:04:05") return default: utcTime := time.Now() @@ -164,10 +134,3 @@ func (ete *EndToEndDelay) Start() { } } } - -func SntpCheckTime() { - err := myexec.RunCommand("sudo", "sntp", "-sS", "time.asia.apple.com") - if err != nil { - log.Error().Err(err).Msg("failed to synchronize time using sntp") - } -} From 2b24c83f5fbfc988841c820420bf15d3ca973f32 Mon Sep 17 00:00:00 2001 From: buyuxiang Date: Tue, 14 Nov 2023 20:25:36 +0800 Subject: [PATCH 3/3] fix timestamp recognition error --- hrp/pkg/uixt/live_e2e.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hrp/pkg/uixt/live_e2e.go b/hrp/pkg/uixt/live_e2e.go index 6ce4bdb1..58a00913 100644 --- a/hrp/pkg/uixt/live_e2e.go +++ b/hrp/pkg/uixt/live_e2e.go @@ -62,11 +62,15 @@ func (ete *EndToEndDelay) getCurrentLiveTime(utcTime time.Time) error { // filter ocr texts with time format var liveTimeTexts []string for _, ocrText := range ocrTexts { - if strings.HasPrefix(ocrText.Text, "16") && - len(ocrText.Text) > 8 && - !strings.Contains(ocrText.Text, ":") { - liveTimeTexts = append(liveTimeTexts, ocrText.Text) + if len(ocrText.Text) < 10 || strings.Contains(ocrText.Text, ":") { + continue } + // exclude digit(s) recognized as letter(s) + _, errParseInt := strconv.ParseInt(ocrText.Text[:10], 10, 64) + if errParseInt != nil { + continue + } + liveTimeTexts = append(liveTimeTexts, ocrText.Text) } var liveTimeText string