mirror of
https://github.com/httprunner/httprunner.git
synced 2026-08-28 19:47:14 +08:00
Merge branch 'feat/buyuxiang/live-e2e' into 'video-release'
feat: support live end to end delay collection See merge request iesqa/httprunner!16
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
v4.3.6.2310111458
|
v4.3.6.2310201510
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const (
|
|||||||
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
ACTION_SwipeToTapTexts ActionMethod = "swipe_to_tap_texts" // swipe up & down to find text and tap
|
||||||
ACTION_VideoCrawler ActionMethod = "video_crawler"
|
ACTION_VideoCrawler ActionMethod = "video_crawler"
|
||||||
ACTION_ClosePopups ActionMethod = "close_popups"
|
ACTION_ClosePopups ActionMethod = "close_popups"
|
||||||
|
ACTION_EndToEndDelay ActionMethod = "live_e2e"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MobileAction struct {
|
type MobileAction struct {
|
||||||
@@ -685,6 +686,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
|
|||||||
return dExt.VideoCrawler(configs)
|
return dExt.VideoCrawler(configs)
|
||||||
case ACTION_ClosePopups:
|
case ACTION_ClosePopups:
|
||||||
return dExt.ClosePopups(action.GetOptions()...)
|
return dExt.ClosePopups(action.GetOptions()...)
|
||||||
|
case ACTION_EndToEndDelay:
|
||||||
|
dExt.CollectEndToEndDelay(action.GetOptions()...)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,14 @@ type cacheStepData struct {
|
|||||||
screenResults ScreenResultMap
|
screenResults ScreenResultMap
|
||||||
// cache feed/live video stat
|
// cache feed/live video stat
|
||||||
videoCrawler *VideoCrawler
|
videoCrawler *VideoCrawler
|
||||||
|
e2eDelay []timeLog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *cacheStepData) reset() {
|
func (d *cacheStepData) reset() {
|
||||||
d.screenShots = make([]string, 0)
|
d.screenShots = make([]string, 0)
|
||||||
d.screenResults = make(map[string]*ScreenResult)
|
d.screenResults = make(map[string]*ScreenResult)
|
||||||
d.videoCrawler = nil
|
d.videoCrawler = nil
|
||||||
|
d.e2eDelay = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DriverExt struct {
|
type DriverExt struct {
|
||||||
@@ -275,6 +277,7 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
|
|||||||
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
||||||
dExt.cacheStepData.screenResults.updatePopupCloseStatus()
|
dExt.cacheStepData.screenResults.updatePopupCloseStatus()
|
||||||
cacheData["screen_results"] = dExt.cacheStepData.screenResults
|
cacheData["screen_results"] = dExt.cacheStepData.screenResults
|
||||||
|
cacheData["e2e_results"] = dExt.cacheStepData.e2eDelay
|
||||||
|
|
||||||
// clear cache
|
// clear cache
|
||||||
dExt.cacheStepData.reset()
|
dExt.cacheStepData.reset()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -300,6 +300,15 @@ func (s *StepMobile) VideoCrawler(params map[string]interface{}) *StepMobile {
|
|||||||
return &StepMobile{step: s.step}
|
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 {
|
func (s *StepMobile) ScreenShot(options ...uixt.ActionOption) *StepMobile {
|
||||||
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
|
s.mobileStep().Actions = append(s.mobileStep().Actions, uixt.MobileAction{
|
||||||
Method: uixt.ACTION_ScreenShot,
|
Method: uixt.ACTION_ScreenShot,
|
||||||
|
|||||||
Reference in New Issue
Block a user