mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-03 13:31:26 +08:00
Merge branch 'merge-from-video' into 'master'
Merge from video See merge request iesqa/httprunner!42
This commit is contained in:
@@ -1,8 +1,19 @@
|
|||||||
# Release History
|
# Release History
|
||||||
|
|
||||||
## v4.3.8 (2024-01-18)
|
## v4.3.9 (2024-01-18)
|
||||||
|
|
||||||
- feat: add Shell step type
|
- feat: add Shell step type
|
||||||
|
- fix: OCR calls use compressed image
|
||||||
|
|
||||||
|
## v4.3.8 (2023-09-19)
|
||||||
|
- feat: OCR calls use the high-precision cluster interface, and the default timeout is changed from 3s to 10s
|
||||||
|
- feat: use jpeg to compress screenshots
|
||||||
|
- feat: increase live broadcast end-to-end collection capabilities
|
||||||
|
- fix: add file reading and writing methods as a log management logic
|
||||||
|
- fix: throws an error when the number of use cases is 0
|
||||||
|
- fix: fixed the problem that the new version of adb format fails to parse
|
||||||
|
|
||||||
|
**go version**
|
||||||
|
|
||||||
## v4.3.7 (2023-09-19)
|
## v4.3.7 (2023-09-19)
|
||||||
|
|
||||||
|
|||||||
59
examples/uitest/android_e2e_delay_test.go
Normal file
59
examples/uitest/android_e2e_delay_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
146
examples/uitest/android_e2e_delay_test.json
Normal file
146
examples/uitest/android_e2e_delay_test.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
66
examples/uitest/android_follow_live_test.json
Normal file
66
examples/uitest/android_follow_live_test.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "直播_抖音_关注天窗_android",
|
||||||
|
"variables": {
|
||||||
|
"device": "${ENV(SerialNumber)}",
|
||||||
|
"ups": "${ENV(LIVEUPLIST)}"
|
||||||
|
},
|
||||||
|
"android": [
|
||||||
|
{
|
||||||
|
"serial": "$device",
|
||||||
|
"log_on": true,
|
||||||
|
"close_popup": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "启动抖音",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.ss.android.ugc.aweme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_launch",
|
||||||
|
"params": "com.ss.android.ugc.aweme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "处理青少年弹窗",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "tap_ocr",
|
||||||
|
"params": "我知道了",
|
||||||
|
"ignore_NotFoundError": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "ui_ocr",
|
||||||
|
"assert": "exists",
|
||||||
|
"expect": "推荐",
|
||||||
|
"msg": "进入抖音失败"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "点击关注",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "tap_ocr",
|
||||||
|
"params": "关注",
|
||||||
|
"index": -1,
|
||||||
|
"identifier": "click_live_new"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
examples/uitest/bili/ios/bili_ios.json
Normal file
18
examples/uitest/bili/ios/bili_ios.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "run ui test on bili ios",
|
||||||
|
"variables": {
|
||||||
|
"RunTimes": 3,
|
||||||
|
"UDID": "${ENV(UDID)}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "run bili ios",
|
||||||
|
"shell": {
|
||||||
|
"string": "bili_ios",
|
||||||
|
"expect_exit_code": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
137
examples/uitest/bili/ios/cli.go
Normal file
137
examples/uitest/bili/ios/cli.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
serial string
|
||||||
|
runTimes int
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
serial = os.Getenv("UDID")
|
||||||
|
numStr := os.Getenv("RunTimes")
|
||||||
|
defaultNum := 20
|
||||||
|
|
||||||
|
var err error
|
||||||
|
runTimes, err = strconv.Atoi(numStr)
|
||||||
|
if err != nil {
|
||||||
|
runTimes = defaultNum
|
||||||
|
}
|
||||||
|
fmt.Printf("=== start running cases, serial=%s, runTimes=%d ===\n", serial, runTimes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func launchAppDriver(pkgName string) (driver *uixt.DriverExt, err error) {
|
||||||
|
device, _ := uixt.NewIOSDevice(uixt.WithUDID(serial))
|
||||||
|
driver, err = device.NewDriver()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//_, err = driver.Driver.AppTerminate(pkgName)
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//err = driver.Driver.Homescreen()
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//err = driver.Driver.AppLaunch(pkgName)
|
||||||
|
//if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
|
||||||
|
// 处理弹窗
|
||||||
|
err = driver.ClosePopupsHandler()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入推荐页
|
||||||
|
err = driver.TapByOCR("推荐", uixt.WithScope(0, 0, 1, 0.3))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return driver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchVideo(driver *uixt.DriverExt) (err error) {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
err = driver.SwipeRelative(0.7, 0.7, 0.7, 0.2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// 点击进入某视频
|
||||||
|
err = driver.TapXY(0.3, 0.5)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
// 点击播放区域,展现横屏图标
|
||||||
|
err = driver.TapXY(0.5, 0.1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// 切换横屏
|
||||||
|
err = driver.TapByUIDetection(
|
||||||
|
uixt.WithScreenShotUITypes("fullScreen"))
|
||||||
|
if err != nil {
|
||||||
|
// 未找到横屏图标,该页面可能不是横版视频(直播|广告|Feed)
|
||||||
|
// 退出回到推荐页
|
||||||
|
driver.Driver.PressBack()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 观播 10s
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
// 返回视频页面
|
||||||
|
err = driver.Driver.PressBack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// 返回推荐页
|
||||||
|
err = driver.Driver.PressBack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// build shell command
|
||||||
|
// go build -o bili_android examples/uitest/bilibili/cli.go
|
||||||
|
func main() {
|
||||||
|
driver, err := launchAppDriver("tv.danmaku.bilianime")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重复采集 XX 次
|
||||||
|
for i := 0; i < runTimes; i++ {
|
||||||
|
err = watchVideo(driver)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,223 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"name": "点播_抖音_滑动场景_随机间隔_android",
|
"name": "直播_抖极_feed卡片_android",
|
||||||
"variables": {
|
"variables": {
|
||||||
"device": "${ENV(SerialNumber)}"
|
"device": "${ENV(SerialNumber)}"
|
||||||
},
|
},
|
||||||
"android": [
|
"android": [
|
||||||
{
|
{
|
||||||
"serial": "$device"
|
"serial": "$device",
|
||||||
|
"log_on": true,
|
||||||
|
"close_popup": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"teststeps": [
|
"teststeps": [
|
||||||
{
|
{
|
||||||
"name": "启动抖音",
|
"name": "清理android无关进程",
|
||||||
"android": {
|
"android": {
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "app_terminate",
|
"method": "app_terminate",
|
||||||
"params": "com.ss.android.ugc.aweme"
|
"params": "com.ss.android.ugc.aweme"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.ss.android.ugc.aweme.lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.smile.gifmaker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.kuaishou.nebula"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.tencent.mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.duowan.kiwi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "air.tv.douyu.android"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.xingin.xhs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.taobao.taobao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "tv.danmaku.bili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.cmcc.cmvideo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.xunmeng.pinduoduo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.cctv.yangshipin.app.androidp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "启动抖音极速版",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.ss.android.ugc.aweme.lite"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"method": "app_launch",
|
"method": "app_launch",
|
||||||
"params": "com.ss.android.ugc.aweme"
|
"params": "com.ss.android.ugc.aweme.lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "sleep",
|
||||||
|
"params": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "close_popups",
|
||||||
|
"options": {
|
||||||
|
"max_retry_times": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "ui_foreground_app",
|
||||||
|
"assert": "equal",
|
||||||
|
"expect": "com.ss.android.ugc.aweme.lite",
|
||||||
|
"msg": "app [com.ss.android.ugc.aweme.lite] should be in foreground"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "处理通讯录弹窗",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "tap_ocr",
|
||||||
|
"params": "拒绝",
|
||||||
|
"ignore_NotFoundError": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "处理青少年弹窗",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "sleep",
|
||||||
|
"params": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "tap_ocr",
|
||||||
|
"params": "我知道了",
|
||||||
|
"ignore_NotFoundError": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "点击直播标签,进入直播间",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "swipe_to_tap_texts",
|
||||||
|
"params": [
|
||||||
|
"看直播开宝箱",
|
||||||
|
"最高领",
|
||||||
|
"点击进入直播间"
|
||||||
|
],
|
||||||
|
"identifier": "click_live_new",
|
||||||
|
"max_retry_times": 40,
|
||||||
|
"wait_time": 2,
|
||||||
|
"direction": [
|
||||||
|
0.5,
|
||||||
|
0.8,
|
||||||
|
0.5,
|
||||||
|
0.2
|
||||||
|
],
|
||||||
|
"scope": [
|
||||||
|
0.1,
|
||||||
|
0.5,
|
||||||
|
0.9,
|
||||||
|
0.9
|
||||||
|
],
|
||||||
|
"offset": [
|
||||||
|
0,
|
||||||
|
-100
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "等待30s",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "sleep",
|
||||||
|
"params": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "下滑进入下一个直播间",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "swipe",
|
||||||
|
"params": [
|
||||||
|
0.5,
|
||||||
|
0.7,
|
||||||
|
0.5,
|
||||||
|
0.1
|
||||||
|
],
|
||||||
|
"identifier": "slide_in_live_new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "sleep",
|
||||||
|
"params": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "返回主界面,并打开本地时间戳",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "app_terminate",
|
||||||
|
"params": "com.ss.android.ugc.aweme.lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "swipe_to_tap_app",
|
||||||
|
"params": "local",
|
||||||
|
"max_retry_times": 5,
|
||||||
|
"offset": [
|
||||||
|
0,
|
||||||
|
-50
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -31,107 +227,10 @@
|
|||||||
},
|
},
|
||||||
"validate": [
|
"validate": [
|
||||||
{
|
{
|
||||||
"check": "ui_foreground_app",
|
"check": "ui_ocr",
|
||||||
"assert": "equal",
|
"assert": "exists",
|
||||||
"expect": "com.ss.android.ugc.aweme",
|
"expect": "17",
|
||||||
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
|
"msg": "打开本地时间戳失败"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "处理青少年弹窗",
|
|
||||||
"android": {
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"method": "tap_ocr",
|
|
||||||
"params": "我知道了",
|
|
||||||
"options": {
|
|
||||||
"ignore_NotFoundError": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "滑动 Feed 3 次,随机间隔 0-5s",
|
|
||||||
"android": {
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"method": "swipe",
|
|
||||||
"params": "up",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "sleep_random",
|
|
||||||
"params": [
|
|
||||||
0,
|
|
||||||
5
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"loops": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "滑动 Feed 1 次,随机间隔 5-10s",
|
|
||||||
"android": {
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"method": "swipe",
|
|
||||||
"params": "up",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "sleep_random",
|
|
||||||
"params": [
|
|
||||||
5,
|
|
||||||
10
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"loops": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "滑动 Feed 10 次,70% 随机间隔 0-5s,30% 随机间隔 5-10s",
|
|
||||||
"android": {
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"method": "swipe",
|
|
||||||
"params": "up",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -26,8 +26,9 @@ require (
|
|||||||
github.com/spf13/cobra v1.5.0
|
github.com/spf13/cobra v1.5.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
gocv.io/x/gocv v0.32.1
|
gocv.io/x/gocv v0.32.1
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.20.0
|
||||||
golang.org/x/oauth2 v0.8.0
|
golang.org/x/oauth2 v0.8.0
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
google.golang.org/grpc v1.57.0
|
google.golang.org/grpc v1.57.0
|
||||||
google.golang.org/protobuf v1.32.0
|
google.golang.org/protobuf v1.32.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -47,8 +48,9 @@ require (
|
|||||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
github.com/go-openapi/swag v0.22.3 // indirect
|
github.com/go-openapi/swag v0.22.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/hashicorp/go-plugin v1.6.0 // indirect
|
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||||
|
github.com/hashicorp/go-plugin v1.4.10 // indirect
|
||||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
@@ -67,17 +69,16 @@ require (
|
|||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||||
github.com/tklauser/numcpus v0.5.0 // indirect
|
github.com/tklauser/numcpus v0.5.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/httprunner/funplugin => ../funplugin
|
replace github.com/httprunner/funplugin v0.5.4 => /Users/bytedance/go/src/github.com/httprunner/funplugin
|
||||||
|
|||||||
42
go.sum
42
go.sum
@@ -48,7 +48,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
@@ -146,7 +145,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.1/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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 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.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
@@ -164,23 +164,21 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
|||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk=
|
||||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
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/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
github.com/httprunner/funplugin v0.5.4 h1:hlfNGcYw2Rv2Mdp1l2S1R6ufwzKgpB9lheFvAxI0LfM=
|
|
||||||
github.com/httprunner/funplugin v0.5.4/go.mod h1:YZzBBSOSdLZEpHZz0P2E5SOQ+o1+Fbn30oWS4RGHBz0=
|
|
||||||
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
|
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-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||||
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
||||||
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
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 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
@@ -289,8 +287,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||||
@@ -325,7 +323,6 @@ github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKo
|
|||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
@@ -341,7 +338,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
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=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -372,7 +368,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20180826012351-8a410e7b638d/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -401,13 +396,11 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
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-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -426,7 +419,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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-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-20180905080454-ebe1bf3edb33/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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -471,8 +463,6 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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-20220128215802-99c3d69c2c27/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-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-20220722155257-8c9f86f7a55f/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
@@ -486,7 +476,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -532,7 +521,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
|||||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
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-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -559,8 +547,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
@@ -590,8 +578,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
|||||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ var listAndroidDevicesCmd = &cobra.Command{
|
|||||||
if isDetail {
|
if isDetail {
|
||||||
fmt.Println(format(d.DeviceInfo()))
|
fmt.Println(format(d.DeviceInfo()))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(d.Serial(), d.Usb())
|
if usb, err := d.Usb(); err != nil {
|
||||||
|
fmt.Println(d.Serial())
|
||||||
|
} else {
|
||||||
|
fmt.Println(d.Serial(), usb)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ var listAppsCmd = &cobra.Command{
|
|||||||
applicationType = gidevice.ApplicationTypeAny
|
applicationType = gidevice.ApplicationTypeAny
|
||||||
}
|
}
|
||||||
|
|
||||||
result, errList := device.InstallationProxyBrowse(
|
result, err := device.InstallationProxyBrowse(
|
||||||
gidevice.WithApplicationType(applicationType),
|
gidevice.WithApplicationType(applicationType),
|
||||||
gidevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier"))
|
gidevice.WithReturnAttributes("CFBundleVersion", "CFBundleDisplayName", "CFBundleIdentifier"))
|
||||||
if errList != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get app list failed")
|
return fmt.Errorf("get app list failed %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, app := range result {
|
for _, app := range result {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -483,3 +484,28 @@ func ConvertToStringSlice(val interface{}) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("invalid type for conversion to []string")
|
return nil, fmt.Errorf("invalid type for conversion to []string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFreePort() (int, error) {
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "resolve tcp addr failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.ListenTCP("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "listen tcp addr failed")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = l.Close(); err != nil {
|
||||||
|
log.Error().Err(err).Msg(fmt.Sprintf("close addr %s error", l.Addr().String()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return l.Addr().(*net.TCPAddr).Port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentDay() string {
|
||||||
|
now := time.Now()
|
||||||
|
// 格式化日期为 yyyyMMdd
|
||||||
|
formattedDate := now.Format("20060102")
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
|
|||||||
17
hrp/internal/env/env.go
vendored
17
hrp/internal/env/env.go
vendored
@@ -22,15 +22,18 @@ var (
|
|||||||
const (
|
const (
|
||||||
ResultsDirName = "results"
|
ResultsDirName = "results"
|
||||||
ScreenshotsDirName = "screenshots"
|
ScreenshotsDirName = "screenshots"
|
||||||
|
ActionLogDireName = "action_log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
RootDir string
|
RootDir string
|
||||||
ResultsDir string
|
ResultsDir string
|
||||||
ResultsPath string
|
ResultsPath string
|
||||||
ScreenShotsPath string
|
ScreenShotsPath string
|
||||||
StartTime = time.Now()
|
StartTime = time.Now()
|
||||||
StartTimeStr = StartTime.Format("20060102150405")
|
StartTimeStr = StartTime.Format("20060102150405")
|
||||||
|
ActionLogFilePath string
|
||||||
|
DeviceActionLogFilePath string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -43,4 +46,6 @@ func init() {
|
|||||||
ResultsDir = filepath.Join(ResultsDirName, StartTimeStr)
|
ResultsDir = filepath.Join(ResultsDirName, StartTimeStr)
|
||||||
ResultsPath = filepath.Join(RootDir, ResultsDir)
|
ResultsPath = filepath.Join(RootDir, ResultsDir)
|
||||||
ScreenShotsPath = filepath.Join(ResultsPath, ScreenshotsDirName)
|
ScreenShotsPath = filepath.Join(ResultsPath, ScreenshotsDirName)
|
||||||
|
ActionLogFilePath = filepath.Join(ResultsDir, ActionLogDireName)
|
||||||
|
DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v4.3.8-202401091556
|
v4.3.9
|
||||||
@@ -53,6 +53,7 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
|
|||||||
testCasePath := TestCasePath(path)
|
testCasePath := TestCasePath(path)
|
||||||
tc, err := testCasePath.ToTestCase()
|
tc, err := testCasePath.ToTestCase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("fail to parse test:")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
testCases = append(testCases, tc)
|
testCases = append(testCases, tc)
|
||||||
@@ -63,6 +64,10 @@ func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(testCases) < 1 {
|
||||||
|
return nil, errors.New("test case count less than 1 or parse error")
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
|
log.Info().Int("count", len(testCases)).Msg("load testcases successfully")
|
||||||
return testCases, nil
|
return testCases, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func (c Client) DeviceList() (devices []*Device, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) < 5 || len(fields[0]) == 0 {
|
if len(fields) < 4 || len(fields[0]) == 0 {
|
||||||
log.Error().Str("line", line).Msg("get unexpected line")
|
log.Error().Str("line", line).Msg("get unexpected line")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -117,6 +117,9 @@ func (c Client) DeviceList() (devices []*Device, err error) {
|
|||||||
mapAttrs := map[string]string{}
|
mapAttrs := map[string]string{}
|
||||||
for _, field := range sliceAttrs {
|
for _, field := range sliceAttrs {
|
||||||
split := strings.Split(field, ":")
|
split := strings.Split(field, ":")
|
||||||
|
if len(split) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
key, val := split[0], split[1]
|
key, val := split[0], split[1]
|
||||||
mapAttrs[key] = val
|
mapAttrs[key] = val
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -107,20 +108,50 @@ func (d *Device) features() (features Features, err error) {
|
|||||||
return features, nil
|
return features, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) Product() string {
|
func (d *Device) HasAttribute(key string) bool {
|
||||||
return d.attrs["product"]
|
_, ok := d.attrs[key]
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) Model() string {
|
func (d *Device) Product() (string, error) {
|
||||||
return d.attrs["model"]
|
if d.HasAttribute("product") {
|
||||||
|
return d.attrs["product"], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("does not have attribute: product")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) Usb() string {
|
func (d *Device) Model() (string, error) {
|
||||||
return d.attrs["usb"]
|
if d.HasAttribute("model") {
|
||||||
|
return d.attrs["model"], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("does not have attribute: model")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) transportId() string {
|
func (d *Device) Brand() (string, error) {
|
||||||
return d.attrs["transport_id"]
|
if d.HasAttribute("brand") {
|
||||||
|
return d.attrs["brand"], nil
|
||||||
|
}
|
||||||
|
brand, err := d.RunShellCommand("getprop", "ro.product.brand")
|
||||||
|
brand = strings.TrimSpace(brand)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("does not have attribute: brand")
|
||||||
|
}
|
||||||
|
d.attrs["brand"] = brand
|
||||||
|
return brand, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) Usb() (string, error) {
|
||||||
|
if d.HasAttribute("usb") {
|
||||||
|
return d.attrs["usb"], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("does not have attribute: usb")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) transportId() (string, error) {
|
||||||
|
if d.HasAttribute("transport_id") {
|
||||||
|
return d.attrs["transport_id"], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("does not have attribute: transport_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) DeviceInfo() map[string]string {
|
func (d *Device) DeviceInfo() map[string]string {
|
||||||
@@ -132,8 +163,13 @@ func (d *Device) Serial() string {
|
|||||||
return d.serial
|
return d.serial
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) IsUsb() bool {
|
func (d *Device) IsUsb() (bool, error) {
|
||||||
return d.Usb() != ""
|
usb, err := d.Usb()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return usb != "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) State() (DeviceState, error) {
|
func (d *Device) State() (DeviceState, error) {
|
||||||
@@ -146,10 +182,8 @@ func (d *Device) DevicePath() (string, error) {
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) Forward(localPort int, remoteInterface interface{}, noRebind ...bool) (err error) {
|
func (d *Device) Forward(remoteInterface interface{}, noRebind ...bool) (port int, err error) {
|
||||||
command := ""
|
|
||||||
var remote string
|
var remote string
|
||||||
local := fmt.Sprintf("tcp:%d", localPort)
|
|
||||||
switch r := remoteInterface.(type) {
|
switch r := remoteInterface.(type) {
|
||||||
// for unix sockets
|
// for unix sockets
|
||||||
case string:
|
case string:
|
||||||
@@ -158,14 +192,28 @@ func (d *Device) Forward(localPort int, remoteInterface interface{}, noRebind ..
|
|||||||
remote = fmt.Sprintf("tcp:%d", r)
|
remote = fmt.Sprintf("tcp:%d", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forwardList, err := d.ForwardList()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, forwardItem := range forwardList {
|
||||||
|
if forwardItem.Remote == remote {
|
||||||
|
return strconv.Atoi(forwardItem.Local[4:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localPort, err := builtin.GetFreePort()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
local := fmt.Sprintf("tcp:%d", localPort)
|
||||||
|
|
||||||
|
command := fmt.Sprintf("host-serial:%s:forward:%s;%s", d.serial, local, remote)
|
||||||
if len(noRebind) != 0 && noRebind[0] {
|
if len(noRebind) != 0 && noRebind[0] {
|
||||||
command = fmt.Sprintf("host-serial:%s:forward:norebind:%s;%s", d.serial, local, remote)
|
command = fmt.Sprintf("host-serial:%s:forward:norebind:%s;%s", d.serial, local, remote)
|
||||||
} else {
|
|
||||||
command = fmt.Sprintf("host-serial:%s:forward:%s;%s", d.serial, local, remote)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = d.adbClient.executeCommand(command, true)
|
_, err = d.adbClient.executeCommand(command, true)
|
||||||
return
|
return localPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
|
func (d *Device) ForwardList() (deviceForwardList []DeviceForward, err error) {
|
||||||
@@ -502,7 +550,7 @@ func (d *Device) Pull(remotePath string, dest io.Writer) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
func (d *Device) installViaABBExec(apk io.ReadSeeker, args ...string) (raw []byte, err error) {
|
||||||
var (
|
var (
|
||||||
tp transport
|
tp transport
|
||||||
filesize int64
|
filesize int64
|
||||||
@@ -515,8 +563,11 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() { _ = tp.Close() }()
|
defer func() { _ = tp.Close() }()
|
||||||
|
cmd := "abb_exec:package\x00install\x00-t"
|
||||||
cmd := fmt.Sprintf("abb_exec:package\x00install\x00-t\x00-S\x00%d", filesize)
|
for _, arg := range args {
|
||||||
|
cmd += "\x00" + arg
|
||||||
|
}
|
||||||
|
cmd += fmt.Sprintf("\x00-S\x00%d", filesize)
|
||||||
if err = tp.SendWithCheck(cmd); err != nil {
|
if err = tp.SendWithCheck(cmd); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -533,7 +584,7 @@ func (d *Device) installViaABBExec(apk io.ReadSeeker) (raw []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
func (d *Device) InstallAPK(apk io.ReadSeeker, args ...string) (string, error) {
|
||||||
haserr := func(ret string) bool {
|
haserr := func(ret string) bool {
|
||||||
return strings.Contains(ret, "Failure")
|
return strings.Contains(ret, "Failure")
|
||||||
}
|
}
|
||||||
@@ -553,8 +604,9 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error pushing: %v", err)
|
return "", fmt.Errorf("error pushing: %v", err)
|
||||||
}
|
}
|
||||||
|
args = append([]string{"install"}, args...)
|
||||||
res, err := d.RunShellCommand("pm", "install", "-f", remote)
|
args = append(args, "-f", remote)
|
||||||
|
res, err := d.RunShellCommand("pm", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "install apk failed")
|
return "", errors.Wrap(err, "install apk failed")
|
||||||
}
|
}
|
||||||
@@ -569,7 +621,7 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
|
|||||||
if len(keepData) == 0 {
|
if len(keepData) == 0 {
|
||||||
keepData = []bool{false}
|
keepData = []bool{false}
|
||||||
}
|
}
|
||||||
packageName = strings.ReplaceAll(packageName, " ", "")
|
packageName = strings.TrimSpace(packageName)
|
||||||
if len(packageName) == 0 {
|
if len(packageName) == 0 {
|
||||||
return "", fmt.Errorf("invalid package name")
|
return "", fmt.Errorf("invalid package name")
|
||||||
}
|
}
|
||||||
@@ -581,6 +633,33 @@ func (d *Device) Uninstall(packageName string, keepData ...bool) (string, error)
|
|||||||
return d.RunShellCommand("pm", args...)
|
return d.RunShellCommand("pm", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Device) ListPackages() ([]string, error) {
|
||||||
|
args := []string{"list", "packages"}
|
||||||
|
resRaw, err := d.RunShellCommand("pm", args...)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(resRaw, "\n")
|
||||||
|
var packages []string
|
||||||
|
for _, line := range lines {
|
||||||
|
packageName := strings.TrimPrefix(line, "package:")
|
||||||
|
packages = append(packages, packageName)
|
||||||
|
}
|
||||||
|
return packages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) IsPackagesInstalled(packageName string) bool {
|
||||||
|
packages, err := d.ListPackages()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
packageName = strings.TrimSpace(packageName)
|
||||||
|
if len(packageName) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return builtin.Contains(packages, packageName)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Device) ScreenCap() ([]byte, error) {
|
func (d *Device) ScreenCap() ([]byte, error) {
|
||||||
if d.HasFeature(FeatShellV2) {
|
if d.HasFeature(FeatShellV2) {
|
||||||
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
return d.RunShellCommandV2WithBytes("screencap", "-p")
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ func TestDevice_Product(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
product := dev.Product()
|
product, err := dev.Product()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
t.Log(dev.Serial(), product)
|
t.Log(dev.Serial(), product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,24 @@ func TestDevice_Model(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
t.Log(dev.Serial(), dev.Model())
|
model, err := dev.Model()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevice_Brand(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
|
||||||
|
for i := range devices {
|
||||||
|
dev := devices[i]
|
||||||
|
brand, err := dev.Brand()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), brand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +98,15 @@ func TestDevice_Usb(t *testing.T) {
|
|||||||
|
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
dev := devices[i]
|
dev := devices[i]
|
||||||
t.Log(dev.Serial(), dev.Usb(), dev.IsUsb())
|
usb, err := dev.Usb()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
isUsb, err := dev.IsUsb()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(dev.Serial(), usb, isUsb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +123,7 @@ func TestDevice_Forward(t *testing.T) {
|
|||||||
setupDevices(t)
|
setupDevices(t)
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
localPort := 61000
|
localPort, err := device.Forward(6790)
|
||||||
err := device.Forward(localPort, 6790)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -314,6 +341,22 @@ func TestDevice_InstallAPK(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDevice_ListPackages(t *testing.T) {
|
||||||
|
setupDevices(t)
|
||||||
|
for _, dev := range devices {
|
||||||
|
res, err := dev.ListPackages()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(res)
|
||||||
|
installed := dev.IsPackagesInstalled("io.appium.uiautomator2.server")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(installed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDevice_HasFeature(t *testing.T) {
|
func TestDevice_HasFeature(t *testing.T) {
|
||||||
setupDevices(t)
|
setupDevices(t)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -116,11 +117,13 @@ type ActionOptions struct {
|
|||||||
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
|
Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
|
||||||
|
|
||||||
// screenshot related
|
// screenshot related
|
||||||
ScreenShotWithOCR bool `json:"screenshot_with_ocr,omitempty" yaml:"screenshot_with_ocr,omitempty"`
|
ScreenShotWithOCR bool `json:"screenshot_with_ocr,omitempty" yaml:"screenshot_with_ocr,omitempty"`
|
||||||
ScreenShotWithUpload bool `json:"screenshot_with_upload,omitempty" yaml:"screenshot_with_upload,omitempty"`
|
ScreenShotWithUpload bool `json:"screenshot_with_upload,omitempty" yaml:"screenshot_with_upload,omitempty"`
|
||||||
ScreenShotWithLiveType bool `json:"screenshot_with_live_type,omitempty" yaml:"screenshot_with_live_type,omitempty"`
|
ScreenShotWithLiveType bool `json:"screenshot_with_live_type,omitempty" yaml:"screenshot_with_live_type,omitempty"`
|
||||||
ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" yaml:"screenshot_with_ui_types,omitempty"`
|
ScreenShotWithLivePopularity bool `json:"screenshot_with_live_popularity,omitempty" yaml:"screenshot_with_live_popularity,omitempty"`
|
||||||
ScreenShotWithClosePopups bool `json:"screenshot_with_close_popups,omitempty" yaml:"screenshot_with_close_popups,omitempty"`
|
ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" yaml:"screenshot_with_ui_types,omitempty"`
|
||||||
|
ScreenShotWithClosePopups bool `json:"screenshot_with_close_popups,omitempty" yaml:"screenshot_with_close_popups,omitempty"`
|
||||||
|
ScreenShotWithOCRCluster string `json:"screenshot_with_ocr_cluster,omitempty" yaml:"screenshot_with_ocr_cluster,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ActionOptions) Options() []ActionOption {
|
func (o *ActionOptions) Options() []ActionOption {
|
||||||
@@ -180,6 +183,9 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
if len(o.AbsScope) == 4 {
|
if len(o.AbsScope) == 4 {
|
||||||
options = append(options, WithAbsScope(
|
options = append(options, WithAbsScope(
|
||||||
o.AbsScope[0], o.AbsScope[1], o.AbsScope[2], o.AbsScope[3]))
|
o.AbsScope[0], o.AbsScope[1], o.AbsScope[2], o.AbsScope[3]))
|
||||||
|
} else if len(o.Scope) == 4 {
|
||||||
|
options = append(options, WithScope(
|
||||||
|
o.Scope[0], o.Scope[1], o.Scope[2], o.Scope[3]))
|
||||||
}
|
}
|
||||||
if len(o.Offset) == 2 {
|
if len(o.Offset) == 2 {
|
||||||
// for tap [x,y] offset
|
// for tap [x,y] offset
|
||||||
@@ -221,9 +227,18 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
if o.ScreenShotWithLiveType {
|
if o.ScreenShotWithLiveType {
|
||||||
options = append(options, WithScreenShotLiveType(true))
|
options = append(options, WithScreenShotLiveType(true))
|
||||||
}
|
}
|
||||||
|
if o.ScreenShotWithLivePopularity {
|
||||||
|
options = append(options, WithScreenShotLivePopularity(true))
|
||||||
|
}
|
||||||
if len(o.ScreenShotWithUITypes) > 0 {
|
if len(o.ScreenShotWithUITypes) > 0 {
|
||||||
options = append(options, WithScreenShotUITypes(o.ScreenShotWithUITypes...))
|
options = append(options, WithScreenShotUITypes(o.ScreenShotWithUITypes...))
|
||||||
}
|
}
|
||||||
|
if o.ScreenShotWithClosePopups {
|
||||||
|
options = append(options, WithScreenShotClosePopups(true))
|
||||||
|
}
|
||||||
|
if o.ScreenShotWithOCRCluster != "" {
|
||||||
|
options = append(options, WithScreenOCRCluster(o.ScreenShotWithOCRCluster))
|
||||||
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
@@ -239,6 +254,9 @@ func (o *ActionOptions) screenshotActions() []string {
|
|||||||
if o.ScreenShotWithLiveType {
|
if o.ScreenShotWithLiveType {
|
||||||
actions = append(actions, "liveType")
|
actions = append(actions, "liveType")
|
||||||
}
|
}
|
||||||
|
if o.ScreenShotWithLivePopularity {
|
||||||
|
actions = append(actions, "livePopularity")
|
||||||
|
}
|
||||||
// UI detection
|
// UI detection
|
||||||
if len(o.ScreenShotWithUITypes) > 0 {
|
if len(o.ScreenShotWithUITypes) > 0 {
|
||||||
actions = append(actions, "ui")
|
actions = append(actions, "ui")
|
||||||
@@ -286,11 +304,11 @@ func (o *ActionOptions) updateData(data map[string]interface{}) {
|
|||||||
data["frequency"] = o.Frequency
|
data["frequency"] = o.Frequency
|
||||||
}
|
}
|
||||||
if _, ok := data["frequency"]; !ok {
|
if _, ok := data["frequency"]; !ok {
|
||||||
data["frequency"] = 60 // default frequency
|
data["frequency"] = 10 // default frequency
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := data["isReplace"]; !ok {
|
if _, ok := data["replace"]; !ok {
|
||||||
data["isReplace"] = true // default true
|
data["replace"] = true // default true
|
||||||
}
|
}
|
||||||
|
|
||||||
// custom options
|
// custom options
|
||||||
@@ -309,6 +327,11 @@ func NewActionOptions(options ...ActionOption) *ActionOptions {
|
|||||||
return actionOptions
|
return actionOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TapTextAction struct {
|
||||||
|
Text string
|
||||||
|
Options []ActionOption
|
||||||
|
}
|
||||||
|
|
||||||
type ActionOption func(o *ActionOptions)
|
type ActionOption func(o *ActionOptions)
|
||||||
|
|
||||||
func WithCustomOption(key string, value interface{}) ActionOption {
|
func WithCustomOption(key string, value interface{}) ActionOption {
|
||||||
@@ -460,6 +483,12 @@ func WithScreenShotLiveType(liveTypeOn bool) ActionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithScreenShotLivePopularity(livePopularityOn bool) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenShotWithLivePopularity = livePopularityOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithScreenShotUITypes(uiTypes ...string) ActionOption {
|
func WithScreenShotUITypes(uiTypes ...string) ActionOption {
|
||||||
return func(o *ActionOptions) {
|
return func(o *ActionOptions) {
|
||||||
o.ScreenShotWithUITypes = uiTypes
|
o.ScreenShotWithUITypes = uiTypes
|
||||||
@@ -472,6 +501,12 @@ func WithScreenShotClosePopups(closeOn bool) ActionOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithScreenOCRCluster(ocrCluster string) ActionOption {
|
||||||
|
return func(o *ActionOptions) {
|
||||||
|
o.ScreenShotWithOCRCluster = ocrCluster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (dExt *DriverExt) ParseActionOptions(options ...ActionOption) []ActionOption {
|
func (dExt *DriverExt) ParseActionOptions(options ...ActionOption) []ActionOption {
|
||||||
actionOptions := NewActionOptions(options...)
|
actionOptions := NewActionOptions(options...)
|
||||||
|
|
||||||
@@ -669,6 +704,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.ClosePopupsHandler()
|
return dExt.ClosePopupsHandler()
|
||||||
|
case ACTION_EndToEndDelay:
|
||||||
|
dExt.CollectEndToEndDelay(action.GetOptions()...)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/funplugin/myexec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
||||||
|
UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adbDriver struct {
|
type adbDriver struct {
|
||||||
@@ -84,7 +98,14 @@ func (ad *adbDriver) WindowSize() (size Size, err error) {
|
|||||||
return Size{Width: width, Height: height}, nil
|
return Size{Width: width, Height: height}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
orientation, err := ad.Orientation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
if orientation != OrientationPortrait {
|
||||||
|
size.Width, size.Height = size.Height, size.Width
|
||||||
|
}
|
||||||
err = errors.New("physical window size not found by adb")
|
err = errors.New("physical window size not found by adb")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -178,6 +199,24 @@ func (ad *adbDriver) StopCamera() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) Orientation() (orientation Orientation, err error) {
|
||||||
|
output, err := ad.adbClient.RunShellCommand("dumpsys", "input", "|", "grep", "'SurfaceOrientation'")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`SurfaceOrientation: (\d)`)
|
||||||
|
matches := re.FindStringSubmatch(output)
|
||||||
|
if len(matches) > 1 { // 确保找到了匹配项
|
||||||
|
if matches[1] == "0" || matches[1] == "2" {
|
||||||
|
return OrientationPortrait, nil
|
||||||
|
} else if matches[1] == "1" || matches[1] == "3" {
|
||||||
|
return OrientationLandscapeLeft, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("not found SurfaceOrientation value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) Homescreen() (err error) {
|
func (ad *adbDriver) Homescreen() (err error) {
|
||||||
return ad.PressKeyCode(KCHome, KMEmpty)
|
return ad.PressKeyCode(KCHome, KMEmpty)
|
||||||
}
|
}
|
||||||
@@ -319,6 +358,15 @@ func (ad *adbDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||||
|
err = ad.SendUnicodeKeys(text, options...)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ad.InputText(text, options...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) InputText(text string, options ...ActionOption) (err error) {
|
||||||
// adb shell input text <text>
|
// adb shell input text <text>
|
||||||
_, err = ad.adbClient.RunShellCommand("input", "text", text)
|
_, err = ad.adbClient.RunShellCommand("input", "text", text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -327,6 +375,85 @@ func (ad *adbDriver) SendKeys(text string, options ...ActionOption) (err error)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
|
||||||
|
// If the Unicode IME is not installed, fall back to the old interface.
|
||||||
|
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
|
||||||
|
// In release version: without the Unicode IME installed, the test cannot execute.
|
||||||
|
if !ad.IsUnicodeIMEInstalled() {
|
||||||
|
return fmt.Errorf("appium unicode ime not installed")
|
||||||
|
}
|
||||||
|
currentIme, err := ad.GetIme()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentIme != UnicodeImePackageName {
|
||||||
|
defer func() {
|
||||||
|
_ = ad.SetIme(currentIme)
|
||||||
|
}()
|
||||||
|
err = ad.SetIme(UnicodeImePackageName)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("set Unicode Ime failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ad.InputText("\""+strings.ReplaceAll(encodedStr, "\"", "\\\"")+"\"", options...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) IsAdbKeyBoardInstalled() bool {
|
||||||
|
output, err := ad.adbClient.RunShellCommand("ime", "list", "-a")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(output, AdbKeyBoardPackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) IsUnicodeIMEInstalled() bool {
|
||||||
|
output, err := ad.adbClient.RunShellCommand("ime", "list", "-s")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(output, UnicodeImePackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) SendKeysByAdbKeyBoard(text string) (err error) {
|
||||||
|
defer func() {
|
||||||
|
// Reset to default, don't care which keyboard was chosen before switch:
|
||||||
|
if _, resetErr := ad.adbClient.RunShellCommand("ime", "reset"); resetErr != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to reset ime")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Enable ADBKeyBoard from adb
|
||||||
|
if _, err = ad.adbClient.RunShellCommand("ime", "enable", AdbKeyBoardPackageName); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to enable adbKeyBoard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Switch to ADBKeyBoard from adb
|
||||||
|
if _, err = ad.adbClient.RunShellCommand("ime", "set", AdbKeyBoardPackageName); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to set adbKeyBoard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
// input Quoted text
|
||||||
|
text = strings.ReplaceAll(text, " ", "\\ ")
|
||||||
|
if _, err = ad.adbClient.RunShellCommand("am", "broadcast", "-a", "ADB_INPUT_TEXT", "--es", "msg", text); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to input by adbKeyBoard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = ad.adbClient.RunShellCommand("input", "keyevent", fmt.Sprintf("%d", KCEnter)); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to input keyevent enter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) Input(text string, options ...ActionOption) (err error) {
|
func (ad *adbDriver) Input(text string, options ...ActionOption) (err error) {
|
||||||
return ad.SendKeys(text, options...)
|
return ad.SendKeys(text, options...)
|
||||||
}
|
}
|
||||||
@@ -356,10 +483,103 @@ func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||||
err = errDriverNotImplemented
|
_, err = ad.adbClient.RunShellCommand("rm", "-rf", "/sdcard/window_dump.xml")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 高版本报错 ERROR: null root node returned by UiTestAutomationBridge.
|
||||||
|
_, err = ad.adbClient.RunShellCommand("uiautomator", "dump")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source, err = ad.adbClient.RunShellCommand("cat", "/sdcard/window_dump.xml")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||||
|
source, err := ad.Source()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceTree = new(Hierarchy)
|
||||||
|
err = xml.Unmarshal([]byte(source), sourceTree)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
sourceTree, err := ad.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ad.tapByTextUsingHierarchy(sourceTree, text, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) tapByTextUsingHierarchy(hierarchy *Hierarchy, text string, options ...ActionOption) error {
|
||||||
|
bounds := ad.searchNodes(hierarchy.Layout, text, options...)
|
||||||
|
actionOptions := NewActionOptions(options...)
|
||||||
|
if len(bounds) == 0 {
|
||||||
|
if actionOptions.IgnoreNotFoundError {
|
||||||
|
log.Info().Msg("not found element by text " + text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not found element by text " + text)
|
||||||
|
}
|
||||||
|
for _, bound := range bounds {
|
||||||
|
width, height := bound.Center()
|
||||||
|
err := ad.TapFloat(width, height, options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
sourceTree, err := ad.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
err := ad.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) searchNodes(nodes []Layout, text string, options ...ActionOption) []Bounds {
|
||||||
|
actionOptions := NewActionOptions(options...)
|
||||||
|
var results []Bounds
|
||||||
|
for _, node := range nodes {
|
||||||
|
result := ad.searchNodes(node.Layout, text, options...)
|
||||||
|
results = append(results, result...)
|
||||||
|
if actionOptions.Regex {
|
||||||
|
// regex on, check if match regex
|
||||||
|
if !regexp.MustCompile(text).MatchString(node.Text) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// regex off, check if match exactly
|
||||||
|
if node.Text != text {
|
||||||
|
ad.searchNodes(node.Layout, text, options...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Bounds != nil {
|
||||||
|
results = append(results, *node.Bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) AccessibleSource() (source string, err error) {
|
func (ad *adbDriver) AccessibleSource() (source string, err error) {
|
||||||
err = errDriverNotImplemented
|
err = errDriverNotImplemented
|
||||||
return
|
return
|
||||||
@@ -387,14 +607,8 @@ func (ad *adbDriver) IsHealthy() (healthy bool, err error) {
|
|||||||
|
|
||||||
func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
||||||
log.Info().Msg("start adb log recording")
|
log.Info().Msg("start adb log recording")
|
||||||
|
|
||||||
// clear logcat
|
|
||||||
if _, err = ad.adbClient.RunShellCommand("logcat", "-c"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start logcat
|
// start logcat
|
||||||
err = ad.logcat.CatchLogcat()
|
err = ad.logcat.CatchLogcat("iesqaMonitor:V")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
err = errors.Wrap(code.AndroidCaptureLogError,
|
||||||
fmt.Sprintf("start adb log recording failed: %v", err))
|
fmt.Sprintf("start adb log recording failed: %v", err))
|
||||||
@@ -404,17 +618,66 @@ func (ad *adbDriver) StartCaptureLog(identifier ...string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
func (ad *adbDriver) StopCaptureLog() (result interface{}, err error) {
|
||||||
log.Info().Msg("stop adb log recording")
|
defer func() {
|
||||||
err = ad.logcat.Stop()
|
log.Info().Msg("stop adb log recording")
|
||||||
|
err = ad.logcat.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to get adb log recording")
|
||||||
|
}
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to get adb log recording")
|
log.Error().Err(err).Msg("failed to close adb log writer")
|
||||||
err = errors.Wrap(code.AndroidCaptureLogError,
|
|
||||||
fmt.Sprintf("get adb log recording failed: %v", err))
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
content := ad.logcat.logBuffer.String()
|
pointRes := ConvertPoints(ad.logcat.logs)
|
||||||
log.Info().Str("logcat content", content).Msg("display logcat content")
|
|
||||||
return ConvertPoints(content), nil
|
// 没有解析到打点日志,走兜底逻辑
|
||||||
|
if len(pointRes) == 0 {
|
||||||
|
log.Info().Msg("action log is null, use action file >>>")
|
||||||
|
logFilePathPrefix := fmt.Sprintf("%v/data", env.ActionLogFilePath)
|
||||||
|
files := []string{}
|
||||||
|
myexec.RunCommand("adb", "-s", ad.adbClient.Serial(), "pull", env.DeviceActionLogFilePath, env.ActionLogFilePath)
|
||||||
|
err = filepath.Walk(env.ActionLogFilePath, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
// 只是需要日志文件
|
||||||
|
if ok := strings.Contains(path, logFilePathPrefix); ok {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// 先保持原有状态码不变,这里不return error
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("read log file fail")
|
||||||
|
return pointRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) != 1 {
|
||||||
|
log.Error().Err(err).Msg("log file count error")
|
||||||
|
return pointRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := os.Open(files[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Info().Msg("open File error")
|
||||||
|
return pointRes, nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = reader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var lines []string // 创建一个空的字符串数组来存储文件的每一行
|
||||||
|
|
||||||
|
// 使用 bufio.NewScanner 读取文件
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text()) // 将每行文本添加到字符串数组
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return pointRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pointRes = ConvertPoints(lines)
|
||||||
|
}
|
||||||
|
return pointRes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
||||||
@@ -452,6 +715,30 @@ func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
|
|||||||
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
|
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) SetIme(ime string) error {
|
||||||
|
_, err := ad.adbClient.RunShellCommand("ime", "set", ime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// even if the shell command has returned,
|
||||||
|
// as there might be a situation where the input method has not been completely switched yet
|
||||||
|
// Listen to the following message.
|
||||||
|
// InputMethodManagerService: onServiceConnected, name:ComponentInfo{io.appium.settings/io.appium.settings.UnicodeIME}, token:android.os.Binder@44f825
|
||||||
|
// But there is no such log on Vivo.
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ad *adbDriver) GetIme() (ime string, err error) {
|
||||||
|
currentIme, err := ad.adbClient.RunShellCommand("settings", "get", "secure", "default_input_method")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("get default ime failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentIme = strings.TrimSpace(currentIme)
|
||||||
|
return currentIme, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...string) error {
|
func (ad *adbDriver) AssertForegroundApp(packageName string, activityType ...string) error {
|
||||||
log.Debug().Str("package_name", packageName).
|
log.Debug().Str("package_name", packageName).
|
||||||
Strs("activity_type", activityType).
|
Strs("activity_type", activityType).
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ var (
|
|||||||
AdbServerPort = gadb.AdbServerPort // 5037
|
AdbServerPort = gadb.AdbServerPort // 5037
|
||||||
UIA2ServerHost = "localhost"
|
UIA2ServerHost = "localhost"
|
||||||
UIA2ServerPort = 6790
|
UIA2ServerPort = 6790
|
||||||
DeviceTempPath = "/data/local/tmp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const forwardToPrefix = "forward-to-"
|
const forwardToPrefix = "forward-to-"
|
||||||
@@ -88,10 +87,13 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
|
|||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(device)
|
option(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceList, err := GetAndroidDevices(device.SerialNumber)
|
deviceList, err := GetAndroidDevices(device.SerialNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.SerialNumber == "" && len(deviceList) > 1 {
|
||||||
|
return nil, errors.Wrap(code.AndroidDeviceConnectionError, "more than one device connected, please specify the serial")
|
||||||
}
|
}
|
||||||
|
|
||||||
dev := deviceList[0]
|
dev := deviceList[0]
|
||||||
@@ -199,12 +201,8 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
|
|||||||
|
|
||||||
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
|
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
|
||||||
func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) {
|
func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) {
|
||||||
var localPort int
|
localPort, err := dev.d.Forward(UIA2ServerPort)
|
||||||
if localPort, err = getFreePort(); err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
|
||||||
fmt.Sprintf("get free port failed: %v", err))
|
|
||||||
}
|
|
||||||
if err = dev.d.Forward(localPort, UIA2ServerPort); err != nil {
|
|
||||||
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
|
||||||
fmt.Sprintf("forward port %d->%d failed: %v",
|
fmt.Sprintf("forward port %d->%d failed: %v",
|
||||||
localPort, UIA2ServerPort, err))
|
localPort, UIA2ServerPort, err))
|
||||||
@@ -263,41 +261,43 @@ func (dev *AndroidDevice) StopPcap() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFreePort() (int, error) {
|
type LineCallback func(string)
|
||||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.Wrap(err, "resolve tcp addr failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := net.ListenTCP("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.Wrap(err, "listen tcp addr failed")
|
|
||||||
}
|
|
||||||
defer func() { _ = l.Close() }()
|
|
||||||
return l.Addr().(*net.TCPAddr).Port, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdbLogcat struct {
|
type AdbLogcat struct {
|
||||||
serial string
|
serial string
|
||||||
logBuffer *bytes.Buffer
|
// logBuffer *bytes.Buffer
|
||||||
errs []error
|
errs []error
|
||||||
stopping chan struct{}
|
stopping chan struct{}
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
|
callback LineCallback
|
||||||
|
logs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdbLogcatWithCallback(serial string, callback LineCallback) *AdbLogcat {
|
||||||
|
return &AdbLogcat{
|
||||||
|
serial: serial,
|
||||||
|
// logBuffer: new(bytes.Buffer),
|
||||||
|
stopping: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
callback: callback,
|
||||||
|
logs: make([]string, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdbLogcat(serial string) *AdbLogcat {
|
func NewAdbLogcat(serial string) *AdbLogcat {
|
||||||
return &AdbLogcat{
|
return &AdbLogcat{
|
||||||
serial: serial,
|
serial: serial,
|
||||||
logBuffer: new(bytes.Buffer),
|
// logBuffer: new(bytes.Buffer),
|
||||||
stopping: make(chan struct{}),
|
stopping: make(chan struct{}),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
|
logs: make([]string, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CatchLogcatContext starts logcat with timeout context
|
// CatchLogcatContext starts logcat with timeout context
|
||||||
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
|
func (l *AdbLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
|
||||||
if err = l.CatchLogcat(); err != nil {
|
if err = l.CatchLogcat(""); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -332,7 +332,7 @@ func (l *AdbLogcat) Errors() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AdbLogcat) CatchLogcat() (err error) {
|
func (l *AdbLogcat) CatchLogcat(filter string) (err error) {
|
||||||
if l.cmd != nil {
|
if l.cmd != nil {
|
||||||
log.Warn().Msg("logcat already start")
|
log.Warn().Msg("logcat already start")
|
||||||
return nil
|
return nil
|
||||||
@@ -342,33 +342,43 @@ func (l *AdbLogcat) CatchLogcat() (err error) {
|
|||||||
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
if err = myexec.RunCommand("adb", "-s", l.serial, "shell", "logcat", "-c"); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
args := []string{"-s", l.serial, "logcat", "--format", "time"}
|
||||||
|
if filter != "" {
|
||||||
|
args = append(args, "-s", filter)
|
||||||
|
}
|
||||||
// start logcat
|
// start logcat
|
||||||
l.cmd = myexec.Command("adb", "-s", l.serial,
|
l.cmd = myexec.Command("adb", args...)
|
||||||
"logcat", "--format", "time", "-s", "iesqaMonitor:V")
|
// l.cmd.Stderr = l.logBuffer
|
||||||
l.cmd.Stderr = l.logBuffer
|
// l.cmd.Stdout = l.logBuffer
|
||||||
l.cmd.Stdout = l.logBuffer
|
reader, err := l.cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err = l.cmd.Start(); err != nil {
|
if err = l.cmd.Start(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if l.callback != nil {
|
||||||
|
l.callback(line) // Process each line with callback
|
||||||
|
} else {
|
||||||
|
l.logs = append(l.logs, line) // Store line if no callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
<-l.stopping
|
<-l.stopping
|
||||||
|
if e := reader.Close(); e != nil {
|
||||||
|
log.Error().Err(e).Msg("close logcat reader failed")
|
||||||
|
}
|
||||||
if e := myexec.KillProcessesByGpid(l.cmd); e != nil {
|
if e := myexec.KillProcessesByGpid(l.cmd); e != nil {
|
||||||
log.Error().Err(e).Msg("kill logcat process failed")
|
log.Error().Err(e).Msg("kill logcat process failed")
|
||||||
}
|
}
|
||||||
l.done <- struct{}{}
|
l.done <- struct{}{}
|
||||||
}()
|
}()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AdbLogcat) BufferedLogcat() (err error) {
|
|
||||||
// -d: dump the current buffered logcat result and exits
|
|
||||||
cmd := myexec.Command("adb", "-s", l.serial, "logcat", "-d")
|
|
||||||
cmd.Stdout = l.logBuffer
|
|
||||||
cmd.Stderr = l.logBuffer
|
|
||||||
if err = cmd.Run(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,8 +392,9 @@ type ExportPoint struct {
|
|||||||
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
|
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertPoints(data string) (eps []ExportPoint) {
|
func ConvertPoints(lines []string) (eps []ExportPoint) {
|
||||||
lines := strings.Split(data, "\n")
|
log.Info().Msg("ConvertPoints")
|
||||||
|
log.Info().Msg(strings.Join(lines, "\n"))
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.Contains(line, "ext") {
|
if strings.Contains(line, "ext") {
|
||||||
idx := strings.Index(line, "{")
|
idx := strings.Index(line, "{")
|
||||||
@@ -397,6 +408,7 @@ func ConvertPoints(data string) (eps []ExportPoint) {
|
|||||||
log.Error().Msg("failed to parse point data")
|
log.Error().Msg("failed to parse point data")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log.Info().Msg(line)
|
||||||
eps = append(eps, p)
|
eps = append(eps, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,7 +575,7 @@ func (s UiSelectorHelper) Index(index int) UiSelectorHelper {
|
|||||||
//
|
//
|
||||||
// For example, to simulate a user click on
|
// For example, to simulate a user click on
|
||||||
// the third image that is enabled in a UI screen, you
|
// the third image that is enabled in a UI screen, you
|
||||||
// could specify a a search criteria where the instance is
|
// could specify a search criteria where the instance is
|
||||||
// 2, the `className(String)` matches the image
|
// 2, the `className(String)` matches the image
|
||||||
// widget class, and `enabled(boolean)` is true.
|
// widget class, and `enabled(boolean)` is true.
|
||||||
// The code would look like this:
|
// The code would look like this:
|
||||||
|
|||||||
62
hrp/pkg/uixt/android_layout.go
Normal file
62
hrp/pkg/uixt/android_layout.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package uixt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Attributes struct {
|
||||||
|
Index int `xml:"index,attr"`
|
||||||
|
Package string `xml:"package,attr"`
|
||||||
|
Class string `xml:"class,attr"`
|
||||||
|
Text string `xml:"text,attr"`
|
||||||
|
ResourceId string `xml:"resource-id,attr"`
|
||||||
|
Checkable bool `xml:"checkable,attr"`
|
||||||
|
Checked bool `xml:"checked,attr"`
|
||||||
|
Clickable bool `xml:"clickable,attr"`
|
||||||
|
Enabled bool `xml:"enabled,attr"`
|
||||||
|
Focusable bool `xml:"focusable,attr"`
|
||||||
|
Focused bool `xml:"focused,attr"`
|
||||||
|
LongClickable bool `xml:"long-clickable,attr"`
|
||||||
|
Password bool `xml:"password,attr"`
|
||||||
|
Scrollable bool `xml:"scrollable,attr"`
|
||||||
|
Selected bool `xml:"selected,attr"`
|
||||||
|
Bounds *Bounds `xml:"bounds,attr"`
|
||||||
|
Displayed bool `xml:"displayed,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hierarchy struct {
|
||||||
|
XMLName xml.Name `xml:"hierarchy"`
|
||||||
|
Attributes
|
||||||
|
Layout []Layout `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layout struct {
|
||||||
|
Attributes
|
||||||
|
Layout []Layout `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bounds struct {
|
||||||
|
X1, Y1, X2, Y2 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bounds) Center() (float64, float64) {
|
||||||
|
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bounds) UnmarshalXMLAttr(attr xml.Attr) error {
|
||||||
|
// 正则表达式用于解析格式为"[x1,y1][x2,y2]"
|
||||||
|
re := regexp.MustCompile(`\[(\d+),(\d+)]\[(\d+),(\d+)]`)
|
||||||
|
matches := re.FindStringSubmatch(attr.Value)
|
||||||
|
if matches == nil {
|
||||||
|
return fmt.Errorf("bounds format is incorrect")
|
||||||
|
}
|
||||||
|
// 转换字符串为整数
|
||||||
|
b.X1, _ = strconv.Atoi(matches[1])
|
||||||
|
b.Y1, _ = strconv.Atoi(matches[2])
|
||||||
|
b.X2, _ = strconv.Atoi(matches[3])
|
||||||
|
b.Y2, _ = strconv.Atoi(matches[4])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,18 +6,23 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
uiaServerURL = "http://localhost:6790/wd/hub"
|
uiaServerURL = "http://forward-to-6790:6790/wd/hub"
|
||||||
driverExt *DriverExt
|
driverExt *DriverExt
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupAndroid(t *testing.T) {
|
func setupAndroid(t *testing.T) {
|
||||||
device, err := NewAndroidDevice()
|
device, err := NewAndroidDevice()
|
||||||
checkErr(t, err)
|
checkErr(t, err)
|
||||||
|
device.UIA2 = false
|
||||||
|
device.LogOn = true
|
||||||
driverExt, err = device.NewDriver()
|
driverExt, err = device.NewDriver()
|
||||||
checkErr(t, err)
|
checkErr(t, err)
|
||||||
}
|
}
|
||||||
@@ -132,6 +137,18 @@ func TestDriver_Source(t *testing.T) {
|
|||||||
t.Log(source)
|
t.Log(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_TapByText(t *testing.T) {
|
||||||
|
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = driver.TapByText("安装")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDriver_BatteryInfo(t *testing.T) {
|
func TestDriver_BatteryInfo(t *testing.T) {
|
||||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,22 +197,21 @@ func TestDriver_DeviceInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_Tap(t *testing.T) {
|
func TestDriver_Tap(t *testing.T) {
|
||||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
setupAndroid(t)
|
||||||
if err != nil {
|
driverExt.Driver.StartCaptureLog("")
|
||||||
t.Fatal(err)
|
err := driverExt.Driver.Tap(150, 340, WithIdentifier("test"))
|
||||||
}
|
|
||||||
|
|
||||||
err = driver.Tap(150, 340)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
err = driver.TapFloat(60.5, 125.5)
|
err = driverExt.Driver.TapFloat(60.5, 125.5, WithIdentifier("test"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
result, _ := driverExt.Driver.StopCaptureLog()
|
||||||
|
t.Log(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_Swipe(t *testing.T) {
|
func TestDriver_Swipe(t *testing.T) {
|
||||||
@@ -204,7 +220,7 @@ func TestDriver_Swipe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.Swipe(400, 1000, 400, 500)
|
err = driver.Swipe(400, 1000, 400, 500, WithPressDuration(2000))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -215,6 +231,14 @@ func TestDriver_Swipe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_Swipe_Relative(t *testing.T) {
|
||||||
|
setupAndroid(t)
|
||||||
|
err := driverExt.SwipeRelative(0.5, 0.7, 0.5, 0.5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDriver_Drag(t *testing.T) {
|
func TestDriver_Drag(t *testing.T) {
|
||||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -235,28 +259,26 @@ func TestDriver_Drag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_SendKeys(t *testing.T) {
|
func TestDriver_SendKeys(t *testing.T) {
|
||||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
setupAndroid(t)
|
||||||
|
|
||||||
|
err := driverExt.Driver.SendKeys("Android\"输入速度测试", WithIdentifier("test"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = driver.SendKeys("abc")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
|
|
||||||
err = driver.SendKeys("def")
|
//err = driver.SendKeys("def")
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
t.Fatal(err)
|
// t.Fatal(err)
|
||||||
}
|
//}
|
||||||
time.Sleep(time.Second * 2)
|
//time.Sleep(time.Second * 2)
|
||||||
|
|
||||||
err = driver.SendKeys("\\n")
|
//err = driver.SendKeys("\\n")
|
||||||
// err = driver.SendKeys(`\n`, false)
|
// err = driver.SendKeys(`\n`, false)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
t.Fatal(err)
|
// t.Fatal(err)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDriver_PressBack(t *testing.T) {
|
func TestDriver_PressBack(t *testing.T) {
|
||||||
@@ -312,7 +334,7 @@ func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_getFreePort(t *testing.T) {
|
func Test_getFreePort(t *testing.T) {
|
||||||
freePort, err := getFreePort()
|
freePort, err := builtin.GetFreePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -421,10 +443,44 @@ func TestDriver_AppTerminate(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertPoints(t *testing.T) {
|
func TestConvertPoints(t *testing.T) {
|
||||||
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}}"
|
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)
|
|
||||||
|
eps := ConvertPoints(strings.Split(data, "\n"))
|
||||||
if len(eps) != 3 {
|
if len(eps) != 3 {
|
||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
jsons, _ := json.Marshal(eps)
|
jsons, _ := json.Marshal(eps)
|
||||||
println(fmt.Sprintf("%v", string(jsons)))
|
println(fmt.Sprintf("%v", string(jsons)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_ShellInputUnicode(t *testing.T) {
|
||||||
|
device, _ := NewAndroidDevice()
|
||||||
|
driver, err := device.NewAdbDriver()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = driver.SendKeys("test中文输入&")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := driver.Screenshot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(os.WriteFile("s1.png", raw.Bytes(), 0o600))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapTexts(t *testing.T) {
|
||||||
|
setupAndroid(t)
|
||||||
|
actions := []TapTextAction{
|
||||||
|
{Text: "^.*无视风险安装$", Options: []ActionOption{WithTapOffset(100, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
{Text: "已了解此应用未经检测.*", Options: []ActionOption{WithTapOffset(-450, 0), WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
{Text: "^(.*无视风险安装|确定|继续|完成|点击继续安装|继续安装旧版本|替换|安装|授权本次安装|继续安装|重新安装)$", Options: []ActionOption{WithRegex(true), WithIgnoreNotFoundError(true)}},
|
||||||
|
}
|
||||||
|
err := driverExt.Driver.TapByTexts(actions...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/utf7"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errDriverNotImplemented = errors.New("driver method not implemented")
|
var errDriverNotImplemented = errors.New("driver method not implemented")
|
||||||
@@ -103,8 +104,8 @@ func (ud *uiaDriver) httpRequest(method string, rawURL string, rawBody []byte, d
|
|||||||
// wait for UIA2 server to resume automatically
|
// wait for UIA2 server to resume automatically
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
oldSessionID := ud.sessionId
|
oldSessionID := ud.sessionId
|
||||||
if err = ud.resetDriver(); err != nil {
|
if err2 := ud.resetDriver(); err2 != nil {
|
||||||
log.Err(err).Msgf("failed to reset uia2 driver, retry count: %v", retryCount)
|
log.Err(err2).Msgf("failed to reset uia2 driver, retry count: %v", retryCount)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Debug().Str("new session", ud.sessionId).Str("old session", oldSessionID).Msgf("successful to reset uia2 driver, retry count: %v", retryCount)
|
log.Debug().Str("new session", ud.sessionId).Str("old session", oldSessionID).Msgf("successful to reset uia2 driver, retry count: %v", retryCount)
|
||||||
@@ -224,6 +225,14 @@ func (ud *uiaDriver) WindowSize() (size Size, err error) {
|
|||||||
return Size{}, err
|
return Size{}, err
|
||||||
}
|
}
|
||||||
size = reply.Value.Size
|
size = reply.Value.Size
|
||||||
|
orientation, err := ud.Orientation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
if orientation != OrientationPortrait {
|
||||||
|
size.Width, size.Height = size.Height, size.Width
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +262,20 @@ func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...K
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) Orientation() (orientation Orientation, err error) {
|
||||||
|
// [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)]
|
||||||
|
var rawResp rawResponse
|
||||||
|
if rawResp, err = ud.httpGET("/session", ud.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 (ud *uiaDriver) Tap(x, y int, options ...ActionOption) error {
|
func (ud *uiaDriver) Tap(x, y int, options ...ActionOption) error {
|
||||||
return ud.TapFloat(float64(x), float64(y), options...)
|
return ud.TapFloat(float64(x), float64(y), options...)
|
||||||
}
|
}
|
||||||
@@ -268,15 +291,31 @@ func (ud *uiaDriver) TapFloat(x, y float64, options ...ActionOption) (err error)
|
|||||||
x += actionOptions.getRandomOffset()
|
x += actionOptions.getRandomOffset()
|
||||||
y += actionOptions.getRandomOffset()
|
y += actionOptions.getRandomOffset()
|
||||||
|
|
||||||
data := map[string]interface{}{
|
duration := 100.0
|
||||||
"x": x,
|
if actionOptions.PressDuration > 0 {
|
||||||
"y": y,
|
duration = actionOptions.PressDuration
|
||||||
}
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"actions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "pointer",
|
||||||
|
"parameters": map[string]string{"pointerType": "touch"},
|
||||||
|
"id": "touch",
|
||||||
|
"actions": []interface{}{
|
||||||
|
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": x, "y": y, "origin": "viewport"},
|
||||||
|
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
|
||||||
|
map[string]interface{}{"type": "pause", "duration": duration},
|
||||||
|
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// update data options in post data for extra uiautomator configurations
|
// update data options in post data for extra uiautomator configurations
|
||||||
actionOptions.updateData(data)
|
actionOptions.updateData(data)
|
||||||
|
|
||||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap")
|
_, err = ud.httpPOST(data, "/session", ud.sessionId, "actions/tap")
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
|
func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
|
||||||
@@ -358,17 +397,30 @@ func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...Actio
|
|||||||
toX += actionOptions.getRandomOffset()
|
toX += actionOptions.getRandomOffset()
|
||||||
toY += actionOptions.getRandomOffset()
|
toY += actionOptions.getRandomOffset()
|
||||||
|
|
||||||
|
duration := 200.0
|
||||||
|
if actionOptions.PressDuration > 0 {
|
||||||
|
duration = actionOptions.PressDuration
|
||||||
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"startX": fromX,
|
"actions": []interface{}{
|
||||||
"startY": fromY,
|
map[string]interface{}{
|
||||||
"endX": toX,
|
"type": "pointer",
|
||||||
"endY": toY,
|
"parameters": map[string]string{"pointerType": "touch"},
|
||||||
|
"id": "touch",
|
||||||
|
"actions": []interface{}{
|
||||||
|
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": fromX, "y": fromY, "origin": "viewport"},
|
||||||
|
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
|
||||||
|
map[string]interface{}{"type": "pointerMove", "duration": duration, "x": toX, "y": toY, "origin": "viewport"},
|
||||||
|
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// update data options in post data for extra uiautomator configurations
|
// update data options in post data for extra uiautomator configurations
|
||||||
actionOptions.updateData(data)
|
actionOptions.updateData(data)
|
||||||
|
|
||||||
_, err := ud.httpPOST(data, "/session", ud.sessionId, "touch/perform")
|
_, err := ud.httpPOST(data, "/session", ud.sessionId, "actions/swipe")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,17 +467,79 @@ func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendKeys Android input does not support setting frequency.
|
||||||
func (ud *uiaDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
func (ud *uiaDriver) SendKeys(text string, options ...ActionOption) (err error) {
|
||||||
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
|
// 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
|
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
|
||||||
actionOptions := NewActionOptions(options...)
|
actionOptions := NewActionOptions(options...)
|
||||||
data := map[string]interface{}{
|
err = ud.SendUnicodeKeys(text, options...)
|
||||||
"text": text,
|
if err != nil {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
|
||||||
|
// new data options in post data for extra uiautomator configurations
|
||||||
|
actionOptions.updateData(data)
|
||||||
|
|
||||||
|
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/keys")
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) SendUnicodeKeys(text string, options ...ActionOption) (err error) {
|
||||||
|
// If the Unicode IME is not installed, fall back to the old interface.
|
||||||
|
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
|
||||||
|
// In release version: without the Unicode IME installed, the test cannot execute.
|
||||||
|
if !ud.IsUnicodeIMEInstalled() {
|
||||||
|
return fmt.Errorf("appium unicode ime not installed")
|
||||||
|
}
|
||||||
|
currentIme, err := ud.adbDriver.GetIme()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentIme != UnicodeImePackageName {
|
||||||
|
defer func() {
|
||||||
|
_ = ud.adbDriver.SetIme(currentIme)
|
||||||
|
}()
|
||||||
|
err = ud.adbDriver.SetIme(UnicodeImePackageName)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("set Unicode Ime failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ud.SendActionKey(encodedStr, options...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) SendActionKey(text string, options ...ActionOption) (err error) {
|
||||||
|
actionOptions := NewActionOptions(options...)
|
||||||
|
var actions []interface{}
|
||||||
|
for i, c := range text {
|
||||||
|
actions = append(actions, map[string]interface{}{"type": "keyDown", "value": string(c)},
|
||||||
|
map[string]interface{}{"type": "keyUp", "value": string(c)})
|
||||||
|
if i != len(text)-1 {
|
||||||
|
actions = append(actions, map[string]interface{}{"type": "pause", "duration": 40})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"actions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "key",
|
||||||
|
"id": "key",
|
||||||
|
"actions": actions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// new data options in post data for extra uiautomator configurations
|
// new data options in post data for extra uiautomator configurations
|
||||||
actionOptions.updateData(data)
|
actionOptions.updateData(data)
|
||||||
|
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions/keys")
|
||||||
_, err = ud.httpPOST(data, "/session", ud.sessionId, "keys")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,25 +563,9 @@ func (ud *uiaDriver) Rotation() (rotation Rotation, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
||||||
// register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot"))
|
// https://bytedance.larkoffice.com/docx/C8qEdmSHnoRvMaxZauocMiYpnLh
|
||||||
var rawResp rawResponse
|
// ui2截图受内存影响,改为adb截图
|
||||||
if rawResp, err = ud.httpGET("/session", ud.sessionId, "screenshot"); err != nil {
|
return ud.adbDriver.Screenshot()
|
||||||
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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var decodeStr []byte
|
|
||||||
if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil {
|
|
||||||
return nil, errors.Wrap(code.AndroidScreenShotError,
|
|
||||||
fmt.Sprintf("decode UIA screenshot data failed: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
raw = bytes.NewBuffer(decodeStr)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||||
@@ -484,3 +582,39 @@ func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
|||||||
source = reply.Value
|
source = reply.Value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||||
|
source, err := ud.Source()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceTree = new(Hierarchy)
|
||||||
|
err = xml.Unmarshal([]byte(source), sourceTree)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
sourceTree, err := ud.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ud.tapByTextUsingHierarchy(sourceTree, text, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
sourceTree, err := ud.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
err := ud.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/gif"
|
_ "image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
_ "image/png"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
@@ -62,9 +62,12 @@ type ScreenResult struct {
|
|||||||
Video *Video `json:"video,omitempty"`
|
Video *Video `json:"video,omitempty"`
|
||||||
Popup *PopupInfo `json:"popup,omitempty"`
|
Popup *PopupInfo `json:"popup,omitempty"`
|
||||||
|
|
||||||
SwipeStartTime int64 `json:"swipe_start_time"` // 滑动开始时间戳
|
SwipeStartTime int64 `json:"swipe_start_time"` // 滑动开始时间戳
|
||||||
SwipeFinishTime int64 `json:"swipe_finish_time"` // 滑动结束时间戳
|
SwipeFinishTime int64 `json:"swipe_finish_time"` // 滑动结束时间戳
|
||||||
|
FetchVideoStartTime int64 `json:"fetch_video_start_time"` // 抓取视频开始时间戳
|
||||||
|
FetchVideoFinishTime int64 `json:"fetch_video_finish_time"` // 抓取视频结束时间戳
|
||||||
|
|
||||||
|
FetchVideoElapsed int64 `json:"fetch_video_elapsed"` // 抓取视频耗时(ms)
|
||||||
ScreenshotTakeElapsed int64 `json:"screenshot_take_elapsed"` // 设备截图耗时(ms)
|
ScreenshotTakeElapsed int64 `json:"screenshot_take_elapsed"` // 设备截图耗时(ms)
|
||||||
ScreenshotCVElapsed int64 `json:"screenshot_cv_elapsed"` // CV 识别耗时(ms)
|
ScreenshotCVElapsed int64 `json:"screenshot_cv_elapsed"` // CV 识别耗时(ms)
|
||||||
|
|
||||||
@@ -93,12 +96,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 {
|
||||||
@@ -190,8 +195,29 @@ func (dExt *DriverExt) takeScreenShot(fileName string) (raw *bytes.Buffer, path
|
|||||||
}
|
}
|
||||||
|
|
||||||
func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) {
|
func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) {
|
||||||
// TODO: compress image data
|
// 解码原始图像数据
|
||||||
return raw, nil
|
img, format, err := image.Decode(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个用来保存压缩后数据的buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
// Convert to jpeg uniformly and compress with a compression rate of 95
|
||||||
|
case "jpeg", "png":
|
||||||
|
jpegOptions := &jpeg.Options{Quality: 95}
|
||||||
|
err = jpeg.Encode(&buf, img, jpegOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported image format: %s", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回压缩后的图像数据
|
||||||
|
return &buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveScreenShot saves image file with file name
|
// saveScreenShot saves image file with file name
|
||||||
@@ -207,7 +233,8 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin
|
|||||||
return "", errors.Wrap(err, "decode screenshot image failed")
|
return "", errors.Wrap(err, "decode screenshot image failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshotPath := filepath.Join(fmt.Sprintf("%s.%s", fileName, format))
|
// The default format uses jpeg for compression
|
||||||
|
screenshotPath := filepath.Join(fmt.Sprintf("%s.%s", fileName, "jpeg"))
|
||||||
file, err := os.Create(screenshotPath)
|
file, err := os.Create(screenshotPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "create screenshot image file failed")
|
return "", errors.Wrap(err, "create screenshot image file failed")
|
||||||
@@ -217,12 +244,9 @@ func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (strin
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case "png":
|
case "jpeg", "png":
|
||||||
err = png.Encode(file, img)
|
jpegOptions := &jpeg.Options{}
|
||||||
case "jpeg":
|
err = jpeg.Encode(file, img, jpegOptions)
|
||||||
err = jpeg.Encode(file, img, nil)
|
|
||||||
case "gif":
|
|
||||||
err = gif.Encode(file, img, nil)
|
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported image format: %s", format)
|
return "", fmt.Errorf("unsupported image format: %s", format)
|
||||||
}
|
}
|
||||||
@@ -242,6 +266,7 @@ func (dExt *DriverExt) GetStepCacheData() map[string]interface{} {
|
|||||||
|
|
||||||
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
cacheData["screenshots_urls"] = dExt.cacheStepData.screenResults.getScreenShotUrls()
|
||||||
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()
|
||||||
|
|||||||
@@ -510,6 +510,10 @@ type WebDriver interface {
|
|||||||
// since the location service needs some time to update the location data.
|
// since the location service needs some time to update the location data.
|
||||||
Location() (Location, error)
|
Location() (Location, error)
|
||||||
BatteryInfo() (BatteryInfo, error)
|
BatteryInfo() (BatteryInfo, error)
|
||||||
|
|
||||||
|
// WindowSize Return the width and height in portrait mode.
|
||||||
|
// when getting the window size in wda/ui2/adb, if the device is in landscape mode,
|
||||||
|
// the width and height will be reversed.
|
||||||
WindowSize() (Size, error)
|
WindowSize() (Size, error)
|
||||||
Screen() (Screen, error)
|
Screen() (Screen, error)
|
||||||
Scale() (float64, error)
|
Scale() (float64, error)
|
||||||
@@ -536,6 +540,8 @@ type WebDriver interface {
|
|||||||
// StopCamera Stops the camera for recording
|
// StopCamera Stops the camera for recording
|
||||||
StopCamera() error
|
StopCamera() error
|
||||||
|
|
||||||
|
Orientation() (orientation Orientation, err error)
|
||||||
|
|
||||||
// Tap Sends a tap event at the coordinate.
|
// Tap Sends a tap event at the coordinate.
|
||||||
Tap(x, y int, options ...ActionOption) error
|
Tap(x, y int, options ...ActionOption) error
|
||||||
TapFloat(x, y float64, options ...ActionOption) error
|
TapFloat(x, y float64, options ...ActionOption) error
|
||||||
@@ -582,6 +588,11 @@ type WebDriver interface {
|
|||||||
|
|
||||||
// Source Return application elements tree
|
// Source Return application elements tree
|
||||||
Source(srcOpt ...SourceOption) (string, error)
|
Source(srcOpt ...SourceOption) (string, error)
|
||||||
|
|
||||||
|
TapByText(text string, options ...ActionOption) error
|
||||||
|
|
||||||
|
TapByTexts(actions ...TapTextAction) error
|
||||||
|
|
||||||
// AccessibleSource Return application elements accessibility tree
|
// AccessibleSource Return application elements accessibility tree
|
||||||
AccessibleSource() (string, error)
|
AccessibleSource() (string, error)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"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/code"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/gidevice"
|
||||||
@@ -237,6 +238,10 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
|
|||||||
return nil, errors.Wrap(code.IOSDeviceConnectionError, err.Error())
|
return nil, errors.Wrap(code.IOSDeviceConnectionError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if device.UDID == "" && len(deviceList) > 1 {
|
||||||
|
return nil, errors.Wrap(code.IOSDeviceConnectionError, "more than one device connected, please specify the udid")
|
||||||
|
}
|
||||||
|
|
||||||
dev := deviceList[0]
|
dev := deviceList[0]
|
||||||
udid := dev.Properties().SerialNumber
|
udid := dev.Properties().SerialNumber
|
||||||
|
|
||||||
@@ -592,7 +597,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver
|
|||||||
var localPort int
|
var localPort int
|
||||||
localPort, err = strconv.Atoi(env.WDA_LOCAL_PORT)
|
localPort, err = strconv.Atoi(env.WDA_LOCAL_PORT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
localPort, err = getFreePort()
|
localPort, err = builtin.GetFreePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.IOSDeviceHTTPDriverError,
|
return nil, errors.Wrap(code.IOSDeviceHTTPDriverError,
|
||||||
fmt.Sprintf("get free port failed: %v", err))
|
fmt.Sprintf("get free port failed: %v", err))
|
||||||
@@ -609,7 +614,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver
|
|||||||
var localMjpegPort int
|
var localMjpegPort int
|
||||||
localMjpegPort, err = strconv.Atoi(env.WDA_LOCAL_MJPEG_PORT)
|
localMjpegPort, err = strconv.Atoi(env.WDA_LOCAL_MJPEG_PORT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
localMjpegPort, err = getFreePort()
|
localMjpegPort, err = builtin.GetFreePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.IOSDeviceHTTPDriverError,
|
return nil, errors.Wrap(code.IOSDeviceHTTPDriverError,
|
||||||
fmt.Sprintf("get free port failed: %v", err))
|
fmt.Sprintf("get free port failed: %v", err))
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte, d
|
|||||||
// TODO: polling WDA to check if resumed automatically
|
// TODO: polling WDA to check if resumed automatically
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
oldSessionID := wd.sessionId
|
oldSessionID := wd.sessionId
|
||||||
if err = wd.resetSession(); err != nil {
|
if err2 := wd.resetSession(); err2 != nil {
|
||||||
log.Err(err).Msgf("failed to reset wda driver, retry count: %v", retryCount)
|
log.Err(err2).Msgf("failed to reset wda driver, retry count: %v", retryCount)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Debug().Str("new session", wd.sessionId).Str("old session", oldSessionID).Msgf("successful to reset wda driver, retry count: %v", retryCount)
|
log.Debug().Str("new session", wd.sessionId).Str("old session", oldSessionID).Msgf("successful to reset wda driver, retry count: %v", retryCount)
|
||||||
@@ -207,6 +207,14 @@ func (wd *wdaDriver) WindowSize() (size Size, err error) {
|
|||||||
}
|
}
|
||||||
size.Height = size.Height * int(scale)
|
size.Height = size.Height * int(scale)
|
||||||
size.Width = size.Width * int(scale)
|
size.Width = size.Width * int(scale)
|
||||||
|
orientation, err := wd.Orientation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
if orientation != OrientationPortrait {
|
||||||
|
size.Width, size.Height = size.Height, size.Width
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,8 +555,8 @@ func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...Action
|
|||||||
|
|
||||||
// update data options in post data for extra WDA configurations
|
// update data options in post data for extra WDA configurations
|
||||||
actionOptions.updateData(data)
|
actionOptions.updateData(data)
|
||||||
|
// wda 43 version
|
||||||
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration")
|
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/drag")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,6 +759,14 @@ func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wd *wdaDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
|
return errDriverNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wd *wdaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
|
return errDriverNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (wd *wdaDriver) 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"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
|
||||||
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
||||||
|
|||||||
@@ -10,17 +10,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bundleId = "com.apple.Preferences"
|
bundleId = "com.apple.Preferences"
|
||||||
driver WebDriver
|
driver WebDriver
|
||||||
|
iOSDriverExt *DriverExt
|
||||||
)
|
)
|
||||||
|
|
||||||
func setup(t *testing.T) {
|
func setup(t *testing.T) {
|
||||||
device, err := NewIOSDevice()
|
device, err := NewIOSDevice(WithWDAPort(8700), WithWDAMjpegPort(8800), WithWDALogOn(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
capabilities := NewCapabilities()
|
||||||
driver, err = device.NewUSBDriver(nil)
|
capabilities.WithDefaultAlertAction(AlertActionAccept)
|
||||||
|
driver, err = device.NewUSBDriver(capabilities)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
iOSDriverExt, err = newDriverExt(device, driver, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -267,6 +273,16 @@ func Test_remoteWD_Drag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Relative_Drag(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
// err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5))
|
||||||
|
err := iOSDriverExt.SwipeRelative(0.5, 0.7, 0.5, 0.5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_remoteWD_SetPasteboard(t *testing.T) {
|
func Test_remoteWD_SetPasteboard(t *testing.T) {
|
||||||
setup(t)
|
setup(t)
|
||||||
|
|
||||||
@@ -305,12 +321,14 @@ func Test_remoteWD_GetPasteboard(t *testing.T) {
|
|||||||
|
|
||||||
func Test_remoteWD_SendKeys(t *testing.T) {
|
func Test_remoteWD_SendKeys(t *testing.T) {
|
||||||
setup(t)
|
setup(t)
|
||||||
|
driver.StartCaptureLog("hrp_wda_log")
|
||||||
err := driver.SendKeys("App Store")
|
err := driver.SendKeys("", WithIdentifier("test"))
|
||||||
|
result, _ := driver.StopCaptureLog()
|
||||||
// err := driver.SendKeys("App Store", WithFrequency(3))
|
// err := driver.SendKeys("App Store", WithFrequency(3))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
t.Log(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_remoteWD_PressButton(t *testing.T) {
|
func Test_remoteWD_PressButton(t *testing.T) {
|
||||||
@@ -374,7 +392,7 @@ func Test_remoteWD_Source(t *testing.T) {
|
|||||||
// t.Fatal(err)
|
// t.Fatal(err)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
source, err = driver.Source(NewSourceOption().WithScope("AppiumAUT"))
|
source, err = driver.Source()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
140
hrp/pkg/uixt/live_e2e.go
Normal file
140
hrp/pkg/uixt/live_e2e.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
)
|
)
|
||||||
@@ -91,6 +90,9 @@ type PopupInfo struct {
|
|||||||
*ClosePopupsResult
|
*ClosePopupsResult
|
||||||
CloseStatus string `json:"close_status"` // found/success/fail
|
CloseStatus string `json:"close_status"` // found/success/fail
|
||||||
ClosePoints []PointF `json:"close_points,omitempty"` // CV 识别的所有关闭按钮(仅关闭按钮,可能存在多个)
|
ClosePoints []PointF `json:"close_points,omitempty"` // CV 识别的所有关闭按钮(仅关闭按钮,可能存在多个)
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
PicName string `json:"pic_name"`
|
||||||
|
PicURL string `json:"pic_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PopupInfo) getClosePoint(lastPopup *PopupInfo) (*PointF, error) {
|
func (p *PopupInfo) getClosePoint(lastPopup *PopupInfo) (*PointF, error) {
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ type ImageResult struct {
|
|||||||
// Media(媒体)
|
// Media(媒体)
|
||||||
// Chat(语音)
|
// Chat(语音)
|
||||||
// Event(赛事)
|
// Event(赛事)
|
||||||
LiveType string `json:"liveType,omitempty"` // 直播间类型
|
LiveType string `json:"liveType,omitempty"` // 直播间类型
|
||||||
UIResult UIResultMap `json:"uiResult,omitempty"` // 图标检测
|
LivePopularity int64 `json:"livePopularity,omitempty"` // 直播间热度
|
||||||
ClosePopupsResult *ClosePopupsResult `json:"closeResult,omitempty"` // 弹窗按钮检测
|
UIResult UIResultMap `json:"uiResult,omitempty"` // 图标检测
|
||||||
|
ClosePopupsResult *ClosePopupsResult `json:"closeResult,omitempty"` // 弹窗按钮检测
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIResponseImage struct {
|
type APIResponseImage struct {
|
||||||
@@ -218,8 +219,19 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...ActionOp
|
|||||||
bodyWriter.WriteField("uiTypes", uiType)
|
bodyWriter.WriteField("uiTypes", uiType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用高精度集群
|
||||||
bodyWriter.WriteField("ocrCluster", "highPrecision")
|
bodyWriter.WriteField("ocrCluster", "highPrecision")
|
||||||
|
|
||||||
|
if actionOptions.ScreenShotWithOCRCluster != "" {
|
||||||
|
bodyWriter.WriteField("ocrCluster", actionOptions.ScreenShotWithOCRCluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actionOptions.Timeout > 0 {
|
||||||
|
bodyWriter.WriteField("timeout", fmt.Sprintf("%v", actionOptions.Timeout))
|
||||||
|
} else {
|
||||||
|
bodyWriter.WriteField("timeout", fmt.Sprintf("%v", 10))
|
||||||
|
}
|
||||||
|
|
||||||
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
|
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(code.CVRequestError,
|
err = errors.Wrap(code.CVRequestError,
|
||||||
@@ -406,18 +418,19 @@ func (dExt *DriverExt) GetScreenResult(options ...ActionOption) (screenResult *S
|
|||||||
screenResult.Texts = imageResult.OCRResult.ToOCRTexts()
|
screenResult.Texts = imageResult.OCRResult.ToOCRTexts()
|
||||||
screenResult.UploadedURL = imageResult.URL
|
screenResult.UploadedURL = imageResult.URL
|
||||||
screenResult.Icons = imageResult.UIResult
|
screenResult.Icons = imageResult.UIResult
|
||||||
|
screenResult.Video = &Video{LiveType: imageResult.LiveType, ViewCount: imageResult.LivePopularity}
|
||||||
|
|
||||||
if actionOptions.ScreenShotWithClosePopups {
|
if actionOptions.ScreenShotWithClosePopups && imageResult.ClosePopupsResult != nil {
|
||||||
popup := &PopupInfo{
|
screenResult.Popup = &PopupInfo{
|
||||||
ClosePopupsResult: imageResult.ClosePopupsResult,
|
ClosePopupsResult: imageResult.ClosePopupsResult,
|
||||||
|
PicName: imagePath,
|
||||||
|
PicURL: imageResult.URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAreas, _ := imageResult.UIResult.FilterUIResults([]string{"close"})
|
closeAreas, _ := imageResult.UIResult.FilterUIResults([]string{"close"})
|
||||||
for _, closeArea := range closeAreas {
|
for _, closeArea := range closeAreas {
|
||||||
popup.ClosePoints = append(popup.ClosePoints, closeArea.Center())
|
screenResult.Popup.ClosePoints = append(screenResult.Popup.ClosePoints, closeArea.Center())
|
||||||
}
|
}
|
||||||
|
|
||||||
screenResult.Popup = popup
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,8 +495,8 @@ func (box Box) IsEmpty() bool {
|
|||||||
func (box Box) IsIdentical(box2 Box) bool {
|
func (box Box) IsIdentical(box2 Box) bool {
|
||||||
// set the coordinate precision to 1 pixel
|
// set the coordinate precision to 1 pixel
|
||||||
return box.Point.IsIdentical(box2.Point) &&
|
return box.Point.IsIdentical(box2.Point) &&
|
||||||
math.Abs(box.Width-box2.Width) < 1 &&
|
builtin.IsZeroFloat64(math.Abs(box.Width-box2.Width)) &&
|
||||||
math.Abs(box.Height-box2.Height) < 1
|
builtin.IsZeroFloat64(math.Abs(box.Height-box2.Height))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (box Box) Center() PointF {
|
func (box Box) Center() PointF {
|
||||||
@@ -538,7 +551,7 @@ func (u UIResultMap) FilterUIResults(uiTypes []string) (uiResults UIResults, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = errors.Errorf("UI types %v not detected", uiTypes)
|
err = errors.Wrap(code.CVResultNotFoundError, fmt.Sprintf("UI types %v not detected", uiTypes))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,25 +11,58 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
directionSlice = [][]float64{
|
||||||
|
{0.85, 0.83, 0.85, 0.1},
|
||||||
|
{0.9, 0.75, 0.9, 0.1},
|
||||||
|
{0.6, 0.5, 0.6, 0.1},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func assertRelative(p float64) bool {
|
func assertRelative(p float64) bool {
|
||||||
return p >= 0 && p <= 1
|
return p >= 0 && p <= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dExt *DriverExt) SwipeUpUtil(count int64, options ...ActionOption) error {
|
||||||
|
width := dExt.windowSize.Width
|
||||||
|
height := dExt.windowSize.Height
|
||||||
|
|
||||||
|
fromX := float64(width) * directionSlice[count%3][0]
|
||||||
|
fromY := float64(height) * directionSlice[count%3][1]
|
||||||
|
toX := float64(width) * directionSlice[count%3][2]
|
||||||
|
toY := float64(height) * directionSlice[count%3][3]
|
||||||
|
|
||||||
|
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...)
|
||||||
|
}
|
||||||
|
|
||||||
// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY]
|
// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY]
|
||||||
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...ActionOption) error {
|
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, options ...ActionOption) error {
|
||||||
width := dExt.windowSize.Width
|
width := dExt.windowSize.Width
|
||||||
height := dExt.windowSize.Height
|
height := dExt.windowSize.Height
|
||||||
|
orientation, err := dExt.Driver.Orientation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("swipe from (%v, %v) to (%v, %v) get orientation failed, use default orientation",
|
||||||
|
fromX, fromY, toX, toY)
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
|
||||||
if !assertRelative(fromX) || !assertRelative(fromY) ||
|
if !assertRelative(fromX) || !assertRelative(fromY) ||
|
||||||
!assertRelative(toX) || !assertRelative(toY) {
|
!assertRelative(toX) || !assertRelative(toY) {
|
||||||
return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1",
|
return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1",
|
||||||
fromX, fromY, toX, toY)
|
fromX, fromY, toX, toY)
|
||||||
}
|
}
|
||||||
|
// 左转和右转都是"LANDSCAPE"
|
||||||
fromX = float64(width) * fromX
|
if orientation == OrientationPortrait {
|
||||||
fromY = float64(height) * fromY
|
fromX = float64(width) * fromX
|
||||||
toX = float64(width) * toX
|
fromY = float64(height) * fromY
|
||||||
toY = float64(height) * toY
|
toX = float64(width) * toX
|
||||||
|
toY = float64(height) * toY
|
||||||
|
} else {
|
||||||
|
fromX = float64(height) * fromX
|
||||||
|
fromY = float64(width) * fromY
|
||||||
|
toX = float64(height) * toX
|
||||||
|
toY = float64(width) * toY
|
||||||
|
}
|
||||||
|
|
||||||
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...)
|
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, options...)
|
||||||
}
|
}
|
||||||
@@ -114,7 +147,7 @@ func (dExt *DriverExt) prepareSwipeAction(options ...ActionOption) func(d *Drive
|
|||||||
log.Error().Err(err).Msgf("swipe %s failed", d)
|
log.Error().Err(err).Msgf("swipe %s failed", d)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if d, ok := swipeDirection.([]float64); ok {
|
} else if d, ok := swipeDirection.([]float64); ok && len(d) == 4 {
|
||||||
// custom direction: [fromX, fromY, toX, toY]
|
// custom direction: [fromX, fromY, toX, toY]
|
||||||
if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3], options...); err != nil {
|
if err := dExt.SwipeRelative(d[0], d[1], d[2], d[3], options...); err != nil {
|
||||||
log.Error().Err(err).Msgf("swipe from (%v, %v) to (%v, %v) failed",
|
log.Error().Err(err).Msgf("swipe from (%v, %v) to (%v, %v) failed",
|
||||||
@@ -178,6 +211,11 @@ func (dExt *DriverExt) swipeToTapApp(appName string, options ...ActionOption) er
|
|||||||
return errors.Wrap(err, "go to home screen failed")
|
return errors.Wrap(err, "go to home screen failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatic handling popups before swipe
|
||||||
|
if err := dExt.ClosePopupsHandler(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("auto handle popup failed")
|
||||||
|
}
|
||||||
|
|
||||||
// swipe to first screen
|
// swipe to first screen
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
dExt.SwipeRight()
|
dExt.SwipeRight()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package uixt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (dExt *DriverExt) TapAbsXY(x, y float64, options ...ActionOption) error {
|
func (dExt *DriverExt) TapAbsXY(x, y float64, options ...ActionOption) error {
|
||||||
@@ -15,9 +17,19 @@ func (dExt *DriverExt) TapXY(x, y float64, options ...ActionOption) error {
|
|||||||
return fmt.Errorf("x, y percentage should be <= 1, got x=%v, y=%v", x, y)
|
return fmt.Errorf("x, y percentage should be <= 1, got x=%v, y=%v", x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
x = x * float64(dExt.windowSize.Width)
|
orientation, err := dExt.Driver.Orientation()
|
||||||
y = y * float64(dExt.windowSize.Height)
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("tap (%v, %v) get orientation failed, use default orientation",
|
||||||
|
x, y)
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
if orientation == OrientationPortrait {
|
||||||
|
x = x * float64(dExt.windowSize.Width)
|
||||||
|
y = y * float64(dExt.windowSize.Height)
|
||||||
|
} else {
|
||||||
|
x = x * float64(dExt.windowSize.Height)
|
||||||
|
y = y * float64(dExt.windowSize.Width)
|
||||||
|
}
|
||||||
return dExt.TapAbsXY(x, y, options...)
|
return dExt.TapAbsXY(x, y, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +98,19 @@ func (dExt *DriverExt) DoubleTapXY(x, y float64) error {
|
|||||||
if x > 1 || y > 1 {
|
if x > 1 || y > 1 {
|
||||||
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
|
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
|
||||||
}
|
}
|
||||||
|
orientation, err := dExt.Driver.Orientation()
|
||||||
x = x * float64(dExt.windowSize.Width)
|
if err != nil {
|
||||||
y = y * float64(dExt.windowSize.Height)
|
log.Warn().Err(err).Msgf("tap (%v, %v) get orientation failed, use default orientation",
|
||||||
|
x, y)
|
||||||
|
orientation = OrientationPortrait
|
||||||
|
}
|
||||||
|
if orientation == OrientationPortrait {
|
||||||
|
x = x * float64(dExt.windowSize.Width)
|
||||||
|
y = y * float64(dExt.windowSize.Height)
|
||||||
|
} else {
|
||||||
|
x = x * float64(dExt.windowSize.Height)
|
||||||
|
y = y * float64(dExt.windowSize.Width)
|
||||||
|
}
|
||||||
return dExt.Driver.DoubleTapFloat(x, y)
|
return dExt.Driver.DoubleTapFloat(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,12 @@ func (vc *VideoCrawler) isTargetAchieved() bool {
|
|||||||
|
|
||||||
func (vc *VideoCrawler) exitLiveRoom() error {
|
func (vc *VideoCrawler) exitLiveRoom() error {
|
||||||
log.Info().Msg("press back to exit live room")
|
log.Info().Msg("press back to exit live room")
|
||||||
return vc.driverExt.Driver.PressBack()
|
err := vc.driverExt.Driver.PressBack()
|
||||||
|
time.Sleep(time.Duration(3) * time.Second)
|
||||||
|
if vc.driverExt.TapByOCR("退出直播间") == nil {
|
||||||
|
log.Info().Msg("clicked the button to exit the live room successfully")
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -149,6 +154,9 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
dExt.cacheStepData.videoCrawler = crawler
|
dExt.cacheStepData.videoCrawler = crawler
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// flag,仅当 flag 为 false 时,并处于内流时,才执行退出直播间逻辑
|
||||||
|
isFeed := true
|
||||||
|
|
||||||
// loop until target count achieved or timeout
|
// loop until target count achieved or timeout
|
||||||
// the main loop is feed crawler
|
// the main loop is feed crawler
|
||||||
crawler.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
|
crawler.timer = time.NewTimer(time.Duration(configs.Timeout) * time.Second)
|
||||||
@@ -168,21 +176,22 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
// swipe to next feed video
|
// swipe to next feed video
|
||||||
log.Info().Msg("swipe to next feed video")
|
log.Info().Msg("swipe to next feed video")
|
||||||
swipeStartTime := time.Now()
|
swipeStartTime := time.Now()
|
||||||
if err = dExt.SwipeRelative(0.9, 0.8, 0.9, 0.1, WithOffsetRandomRange(-10, 10)); err != nil {
|
if err = dExt.SwipeUpUtil(crawler.failedCount, WithOffsetRandomRange(-10, 10)); err != nil {
|
||||||
log.Error().Err(err).Msg("feed swipe up failed")
|
log.Error().Err(err).Msg("feed swipe up failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
swipeFinishTime := time.Now()
|
swipeFinishTime := time.Now()
|
||||||
|
|
||||||
// get app event trackings
|
// get app event trackings
|
||||||
// retry 10 times if get feed failed, abort if fail 10 consecutive times
|
// retry 3 times if get feed failed, abort if fail 3 consecutive times
|
||||||
|
fetchVideoStartTime := time.Now()
|
||||||
currentVideo, err := crawler.getCurrentVideo()
|
currentVideo, err := crawler.getCurrentVideo()
|
||||||
if err != nil || currentVideo.Type == "" {
|
if err != nil || currentVideo.Type == "" {
|
||||||
crawler.failedCount++
|
crawler.failedCount++
|
||||||
if crawler.failedCount >= 10 {
|
if crawler.failedCount >= 3 {
|
||||||
// failed 10 consecutive times
|
// failed 3 consecutive times
|
||||||
return errors.Wrap(code.TrackingGetError,
|
return errors.Wrap(code.TrackingGetError,
|
||||||
"get current feed video failed 10 consecutive times")
|
"get current feed video failed 3 consecutive times")
|
||||||
}
|
}
|
||||||
log.Warn().
|
log.Warn().
|
||||||
Int64("failedCount", crawler.failedCount).
|
Int64("failedCount", crawler.failedCount).
|
||||||
@@ -196,12 +205,14 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
// retry
|
// retry
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fetchVideoFinishTime := time.Now()
|
||||||
|
|
||||||
// 直播预览流线上概率
|
// 直播预览流线上概率
|
||||||
livePreviewProb := crawler.getLivePreviewProb()
|
livePreviewProb := crawler.getLivePreviewProb()
|
||||||
|
|
||||||
switch currentVideo.Type {
|
switch currentVideo.Type {
|
||||||
case VideoType_PreviewLive:
|
case VideoType_PreviewLive:
|
||||||
|
isFeed = true
|
||||||
// 直播预览流
|
// 直播预览流
|
||||||
var skipEnterLive bool
|
var skipEnterLive bool
|
||||||
if crawler.isLiveTargetAchieved() {
|
if crawler.isLiveTargetAchieved() {
|
||||||
@@ -227,13 +238,13 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
log.Error().Err(err).Msg("tap live video failed")
|
log.Error().Err(err).Msg("tap live video failed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
currentVideo.Type = VideoType_Live
|
||||||
} else {
|
} else {
|
||||||
// skip entering live room
|
// skip entering live room
|
||||||
// only mock simulation play duration
|
// only mock simulation play duration
|
||||||
sleepTime := math.Min(float64(currentVideo.SimulationPlayDuration), float64(currentVideo.RandomPlayDuration))
|
sleepTime := math.Min(float64(currentVideo.SimulationPlayDuration), float64(currentVideo.RandomPlayDuration))
|
||||||
currentVideo.PlayDuration = int64(sleepTime)
|
currentVideo.PlayDuration = int64(sleepTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
||||||
case VideoType_Live:
|
case VideoType_Live:
|
||||||
@@ -260,14 +271,18 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
currentVideo.LiveType = screenResult.imageResult.LiveType
|
currentVideo.LiveType = screenResult.imageResult.LiveType
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulation watch feed video
|
// simulation watch live video
|
||||||
sleepStrict(swipeFinishTime, currentVideo.PlayDuration)
|
simulationPlayDuration := math.Min(float64(currentVideo.PlayDuration), 300000)
|
||||||
|
sleepStrict(swipeFinishTime, int64(simulationPlayDuration))
|
||||||
|
|
||||||
screenResult.Video = currentVideo
|
screenResult.Video = currentVideo
|
||||||
screenResult.Resolution = dExt.windowSize
|
screenResult.Resolution = dExt.windowSize
|
||||||
screenResult.SwipeStartTime = swipeStartTime.UnixMilli()
|
screenResult.SwipeStartTime = swipeStartTime.UnixMilli()
|
||||||
screenResult.SwipeFinishTime = swipeFinishTime.UnixMilli()
|
screenResult.SwipeFinishTime = swipeFinishTime.UnixMilli()
|
||||||
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
||||||
|
screenResult.FetchVideoStartTime = fetchVideoStartTime.UnixMilli()
|
||||||
|
screenResult.FetchVideoFinishTime = fetchVideoFinishTime.UnixMilli()
|
||||||
|
screenResult.FetchVideoElapsed = fetchVideoFinishTime.Sub(fetchVideoStartTime).Milliseconds()
|
||||||
|
|
||||||
var exitLive bool
|
var exitLive bool
|
||||||
if crawler.isLiveTargetAchieved() {
|
if crawler.isLiveTargetAchieved() {
|
||||||
@@ -278,7 +293,9 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
log.Info().Interface("livePreviewProb", livePreviewProb).Msg("exit live room by preview live chance")
|
log.Info().Interface("livePreviewProb", livePreviewProb).Msg("exit live room by preview live chance")
|
||||||
exitLive = true
|
exitLive = true
|
||||||
}
|
}
|
||||||
if exitLive && currentVideo.Type == VideoType_Live {
|
|
||||||
|
// isFeed:通过预览流进入内流失败的情况下,防止使用退出直播间逻辑,影响:首次进入内流,至少会消费两个直播间才能退出
|
||||||
|
if !isFeed && exitLive && currentVideo.Type == VideoType_Live {
|
||||||
err = crawler.exitLiveRoom()
|
err = crawler.exitLiveRoom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, code.TimeoutError) || errors.Is(err, code.InterruptError) {
|
if errors.Is(err, code.TimeoutError) || errors.Is(err, code.InterruptError) {
|
||||||
@@ -286,9 +303,12 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
}
|
}
|
||||||
log.Error().Err(err).Msg("run live crawler failed, continue")
|
log.Error().Err(err).Msg("run live crawler failed, continue")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
isFeed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
isFeed = true
|
||||||
// 点播 || 图文 || 广告 || etc.
|
// 点播 || 图文 || 广告 || etc.
|
||||||
crawler.FeedCount++
|
crawler.FeedCount++
|
||||||
log.Info().Interface("video", currentVideo).Msg(FOUND_FEED_SUCCESS)
|
log.Info().Interface("video", currentVideo).Msg(FOUND_FEED_SUCCESS)
|
||||||
@@ -298,13 +318,17 @@ func (dExt *DriverExt) VideoCrawler(configs *VideoCrawlerConfigs) (err error) {
|
|||||||
Video: currentVideo,
|
Video: currentVideo,
|
||||||
|
|
||||||
// log swipe timelines
|
// log swipe timelines
|
||||||
SwipeStartTime: swipeStartTime.UnixMilli(),
|
SwipeStartTime: swipeStartTime.UnixMilli(),
|
||||||
SwipeFinishTime: swipeFinishTime.UnixMilli(),
|
SwipeFinishTime: swipeFinishTime.UnixMilli(),
|
||||||
|
FetchVideoStartTime: fetchVideoStartTime.UnixMilli(),
|
||||||
|
FetchVideoFinishTime: fetchVideoFinishTime.UnixMilli(),
|
||||||
|
FetchVideoElapsed: fetchVideoFinishTime.Sub(fetchVideoStartTime).Milliseconds(),
|
||||||
}
|
}
|
||||||
dExt.cacheStepData.screenResults[time.Now().String()] = screenResult
|
dExt.cacheStepData.screenResults[time.Now().String()] = screenResult
|
||||||
|
|
||||||
// simulation watch feed video
|
// simulation watch feed video
|
||||||
sleepStrict(swipeFinishTime, currentVideo.PlayDuration)
|
simulationPlayDuration := math.Min(float64(currentVideo.PlayDuration), 600000)
|
||||||
|
sleepStrict(swipeFinishTime, int64(simulationPlayDuration))
|
||||||
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
screenResult.TotalElapsed = time.Since(swipeFinishTime).Milliseconds()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +364,9 @@ type Video struct {
|
|||||||
UserName string `json:"user_name"` // 视频作者
|
UserName string `json:"user_name"` // 视频作者
|
||||||
Duration int64 `json:"duration,omitempty"` // 视频时长(ms)
|
Duration int64 `json:"duration,omitempty"` // 视频时长(ms)
|
||||||
Caption string `json:"caption,omitempty"` // 视频文案
|
Caption string `json:"caption,omitempty"` // 视频文案
|
||||||
|
// 作者信息
|
||||||
|
UserID string `json:"user_id"` // 作者用户名
|
||||||
|
FollowerCount int64 `json:"follower_count"` // 作者粉丝数
|
||||||
// 视频热度数据
|
// 视频热度数据
|
||||||
ViewCount int64 `json:"view_count,omitempty"` // feed 观看数
|
ViewCount int64 `json:"view_count,omitempty"` // feed 观看数
|
||||||
LikeCount int64 `json:"like_count,omitempty"` // feed 点赞数
|
LikeCount int64 `json:"like_count,omitempty"` // feed 点赞数
|
||||||
|
|||||||
149
hrp/pkg/utf7/decoder.go
Normal file
149
hrp/pkg/utf7/decoder.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7.
|
||||||
|
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
ascii bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for i := 0; i < len(src); i++ {
|
||||||
|
ch := src[i]
|
||||||
|
|
||||||
|
if ch < min || ch > max { // Illegal code point in ASCII mode
|
||||||
|
err = ErrInvalidUTF7
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch != '&' {
|
||||||
|
if nDst+1 > len(dst) {
|
||||||
|
err = transform.ErrShortDst
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nSrc++
|
||||||
|
|
||||||
|
dst[nDst] = ch
|
||||||
|
nDst++
|
||||||
|
|
||||||
|
d.ascii = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of the Base64 or "&-" segment
|
||||||
|
start := i + 1
|
||||||
|
for i++; i < len(src) && src[i] != '-'; i++ {
|
||||||
|
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
|
||||||
|
err = ErrInvalidUTF7
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(src) { // Implicit shift ("&...")
|
||||||
|
if atEOF {
|
||||||
|
err = ErrInvalidUTF7
|
||||||
|
} else {
|
||||||
|
err = transform.ErrShortSrc
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
if i == start { // Escape sequence "&-"
|
||||||
|
b = []byte{'&'}
|
||||||
|
d.ascii = true
|
||||||
|
} else { // Control or non-ASCII code points in base64
|
||||||
|
if !d.ascii { // Null shift ("&...-&...-")
|
||||||
|
err = ErrInvalidUTF7
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b = decode(src[start:i])
|
||||||
|
d.ascii = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) == 0 { // Bad encoding
|
||||||
|
err = ErrInvalidUTF7
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nDst+len(b) > len(dst) {
|
||||||
|
d.ascii = true
|
||||||
|
err = transform.ErrShortDst
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nSrc = i + 1
|
||||||
|
|
||||||
|
for _, ch := range b {
|
||||||
|
dst[nDst] = ch
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if atEOF {
|
||||||
|
d.ascii = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) Reset() {
|
||||||
|
d.ascii = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
|
||||||
|
// A nil slice is returned if the encoding is invalid.
|
||||||
|
func decode(b64 []byte) []byte {
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
// Allocate a single block of memory large enough to store the Base64 data
|
||||||
|
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
|
||||||
|
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
|
||||||
|
// double the space allocation for UTF-8.
|
||||||
|
if n := len(b64); b64[n-1] == '=' {
|
||||||
|
return nil
|
||||||
|
} else if n&3 == 0 {
|
||||||
|
b = make([]byte, b64Enc.DecodedLen(n)*3)
|
||||||
|
} else {
|
||||||
|
n += 4 - n&3
|
||||||
|
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
|
||||||
|
copy(b[copy(b, b64):n], []byte("=="))
|
||||||
|
b64, b = b[:n], b[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode Base64 into the first 1/3rd of b
|
||||||
|
n, err := b64Enc.Decode(b, b64)
|
||||||
|
if err != nil || n&1 == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode UTF-16-BE into the remaining 2/3rds of b
|
||||||
|
b, s := b[:n], b[n:]
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < n; i += 2 {
|
||||||
|
r := rune(b[i])<<8 | rune(b[i+1])
|
||||||
|
if utf16.IsSurrogate(r) {
|
||||||
|
if i += 2; i == n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r2 := rune(b[i])<<8 | rune(b[i+1])
|
||||||
|
if r = utf16.DecodeRune(r, r2); r == repl {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if min <= r && r <= max {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
j += utf8.EncodeRune(s[j:], r)
|
||||||
|
}
|
||||||
|
return s[:j]
|
||||||
|
}
|
||||||
91
hrp/pkg/utf7/encoder.go
Normal file
91
hrp/pkg/utf7/encoder.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoder struct{}
|
||||||
|
|
||||||
|
func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||||
|
for i := 0; i < len(src); {
|
||||||
|
ch := src[i]
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
if min <= ch && ch <= max {
|
||||||
|
b = []byte{ch}
|
||||||
|
if ch == '&' {
|
||||||
|
b = append(b, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
start := i
|
||||||
|
|
||||||
|
// Find the next printable ASCII code point
|
||||||
|
i++
|
||||||
|
for i < len(src) && (src[i] < min || src[i] > max) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if !atEOF && i == len(src) {
|
||||||
|
err = transform.ErrShortSrc
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b = encode(src[start:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if nDst+len(b) > len(dst) {
|
||||||
|
err = transform.ErrShortDst
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nSrc = i
|
||||||
|
|
||||||
|
for _, ch := range b {
|
||||||
|
dst[nDst] = ch
|
||||||
|
nDst++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) Reset() {}
|
||||||
|
|
||||||
|
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
|
||||||
|
// removes the padding, and adds UTF-7 shifts.
|
||||||
|
func encode(s []byte) []byte {
|
||||||
|
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
|
||||||
|
// control code points (see table below).
|
||||||
|
b := make([]byte, 0, len(s)+4)
|
||||||
|
for len(s) > 0 {
|
||||||
|
r, size := utf8.DecodeRune(s)
|
||||||
|
if r > utf8.MaxRune {
|
||||||
|
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
|
||||||
|
}
|
||||||
|
s = s[size:]
|
||||||
|
if r1, r2 := utf16.EncodeRune(r); r1 != repl {
|
||||||
|
b = append(b, byte(r1>>8), byte(r1))
|
||||||
|
r = r2
|
||||||
|
}
|
||||||
|
b = append(b, byte(r>>8), byte(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode as base64
|
||||||
|
n := b64Enc.EncodedLen(len(b)) + 2
|
||||||
|
b64 := make([]byte, n)
|
||||||
|
b64Enc.Encode(b64[1:], b)
|
||||||
|
|
||||||
|
// Strip padding
|
||||||
|
n -= 2 - (len(b)+2)%3
|
||||||
|
b64 = b64[:n]
|
||||||
|
|
||||||
|
// Add UTF-7 shifts
|
||||||
|
b64[0] = '&'
|
||||||
|
b64[n-1] = '-'
|
||||||
|
return b64
|
||||||
|
}
|
||||||
34
hrp/pkg/utf7/utf7.go
Normal file
34
hrp/pkg/utf7/utf7.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
|
||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
min = 0x20 // Minimum self-representing UTF-7 value
|
||||||
|
max = 0x7E // Maximum self-representing UTF-7 value
|
||||||
|
|
||||||
|
repl = '\uFFFD' // Unicode replacement code point
|
||||||
|
)
|
||||||
|
|
||||||
|
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
|
||||||
|
|
||||||
|
type enc struct{}
|
||||||
|
|
||||||
|
func (e enc) NewDecoder() *encoding.Decoder {
|
||||||
|
return &encoding.Decoder{
|
||||||
|
Transformer: &decoder{true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e enc) NewEncoder() *encoding.Encoder {
|
||||||
|
return &encoding.Encoder{
|
||||||
|
Transformer: &encoder{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoding is the modified UTF-7 encoding.
|
||||||
|
var Encoding encoding.Encoding = enc{}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/httprunner/funplugin"
|
"github.com/httprunner/funplugin"
|
||||||
|
"github.com/httprunner/funplugin/myexec"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
"github.com/httprunner/httprunner/v4/hrp/internal/code"
|
||||||
|
"github.com/httprunner/httprunner/v4/hrp/internal/env"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||||
@@ -240,6 +242,13 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if caseRunner.parsedConfig.PluginSetting != nil {
|
||||||
|
if p, ok := pluginMap.Load(caseRunner.parsedConfig.PluginSetting.Path); ok {
|
||||||
|
log.Info().Msg(fmt.Sprintf("starting to keep live, path: %v, address: %v", caseRunner.parsedConfig.PluginSetting.Path, p))
|
||||||
|
go p.(funplugin.IPlugin).StartHeartbeat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for it := caseRunner.parametersIterator; it.HasNext(); {
|
for it := caseRunner.parametersIterator; it.HasNext(); {
|
||||||
// case runner can run multiple times with different parameters
|
// case runner can run multiple times with different parameters
|
||||||
// each run has its own session runner
|
// each run has its own session runner
|
||||||
@@ -523,6 +532,11 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
|
|||||||
config := r.caseRunner.testCase.Config
|
config := r.caseRunner.testCase.Config
|
||||||
log.Info().Str("testcase", config.Name).Msg("run testcase start")
|
log.Info().Str("testcase", config.Name).Msg("run testcase start")
|
||||||
|
|
||||||
|
// 安卓系统删除打点日志文件
|
||||||
|
if r.caseRunner.testCase.Config.Android != nil {
|
||||||
|
myexec.RunCommand("adb", "-s", r.caseRunner.testCase.Config.Android[0].SerialNumber, "shell", "rm", "-r", env.DeviceActionLogFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
// update config variables with given variables
|
// update config variables with given variables
|
||||||
r.InitWithParameters(givenVars)
|
r.InitWithParameters(givenVars)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const (
|
|||||||
|
|
||||||
type StepResult struct {
|
type StepResult struct {
|
||||||
Name string `json:"name" yaml:"name"` // step name
|
Name string `json:"name" yaml:"name"` // step name
|
||||||
|
Identifier string `json:"identifier" yaml:"identifier"` // step identifier
|
||||||
StartTime int64 `json:"start_time" yaml:"time"` // step start time
|
StartTime int64 `json:"start_time" yaml:"time"` // step start time
|
||||||
StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
|
StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
|
||||||
Success bool `json:"success" yaml:"success"` // step execution result
|
Success bool `json:"success" yaml:"success"` // step execution result
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -593,8 +602,22 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
|||||||
"osType": osType,
|
"osType": osType,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
identifer := mobileStep.Identifier
|
||||||
|
if mobileStep.Options != nil && identifer == "" {
|
||||||
|
identifer = mobileStep.Options.Identifier
|
||||||
|
}
|
||||||
|
if len(mobileStep.Actions) != 0 && identifer == "" {
|
||||||
|
for _, action := range mobileStep.Actions {
|
||||||
|
if action.Identifier != "" {
|
||||||
|
identifer = action.Identifier
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stepResult = &StepResult{
|
stepResult = &StepResult{
|
||||||
Name: step.Name,
|
Name: step.Name,
|
||||||
|
Identifier: identifer,
|
||||||
StepType: StepType(osType),
|
StepType: StepType(osType),
|
||||||
Success: false,
|
Success: false,
|
||||||
ContentSize: 0,
|
ContentSize: 0,
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ func convertCompatMobileStep(mobileStep *MobileStep) {
|
|||||||
if ma.Method == uixt.ACTION_TapByCV {
|
if ma.Method == uixt.ACTION_TapByCV {
|
||||||
uiTypes, _ := builtin.ConvertToStringSlice(ma.Params)
|
uiTypes, _ := builtin.ConvertToStringSlice(ma.Params)
|
||||||
ma.ActionOptions.ScreenShotWithUITypes = append(ma.ActionOptions.ScreenShotWithUITypes, uiTypes...)
|
ma.ActionOptions.ScreenShotWithUITypes = append(ma.ActionOptions.ScreenShotWithUITypes, uiTypes...)
|
||||||
|
ma.ActionOptions.ScreenShotWithUpload = true
|
||||||
}
|
}
|
||||||
// set default max_retry_times to 10 for swipe_to_tap_texts
|
// set default max_retry_times to 10 for swipe_to_tap_texts
|
||||||
if ma.Method == uixt.ACTION_SwipeToTapTexts && actionOptions.MaxRetryTimes == 0 {
|
if ma.Method == uixt.ACTION_SwipeToTapTexts && actionOptions.MaxRetryTimes == 0 {
|
||||||
@@ -384,6 +385,9 @@ func convertCompatMobileStep(mobileStep *MobileStep) {
|
|||||||
if ma.Method == uixt.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
if ma.Method == uixt.ACTION_SwipeToTapText && actionOptions.MaxRetryTimes == 0 {
|
||||||
ma.ActionOptions.MaxRetryTimes = 10
|
ma.ActionOptions.MaxRetryTimes = 10
|
||||||
}
|
}
|
||||||
|
if ma.Method == uixt.ACTION_Swipe {
|
||||||
|
ma.ActionOptions.Direction = ma.Params
|
||||||
|
}
|
||||||
mobileStep.Actions[i] = ma
|
mobileStep.Actions[i] = ma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user