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..58a00913 --- /dev/null +++ b/hrp/pkg/uixt/live_e2e.go @@ -0,0 +1,140 @@ +package uixt + +import ( + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "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 + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Interval int `json:"interval"` // seconds + Duration int `json:"duration"` // seconds + Timelines []timeLog `json:"timelines"` +} + +func (dExt *DriverExt) CollectEndToEndDelay(options ...ActionOption) { + dataOptions := NewActionOptions(options...) + startTime := time.Now() + + if dataOptions.Interval == 0 { + dataOptions.Interval = 5 + } + if dataOptions.Timeout == 0 { + dataOptions.Timeout = 60 + } + + endToEndDelay := &EndToEndDelay{ + driver: dExt, + Duration: int(dataOptions.Timeout), + Interval: int(dataOptions.Interval), + StartTime: startTime.Format("2006-01-02 15:04:05"), + } + + 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 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 + 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") + 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") + 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: + 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() + if utcTime.Unix()%int64(ete.Interval) == 0 { + _ = ete.getCurrentLiveTime(utcTime) + } else { + time.Sleep(500 * time.Millisecond) + } + } + } +} 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,